@retray-dev/ui-kit 12.1.0 → 13.0.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 +183 -147
- package/CONSUMER.md +2 -2
- package/DESIGN.md +2 -2
- package/README.md +13 -8
- package/dist/Accordion.d.mts +6 -0
- package/dist/Accordion.d.ts +6 -0
- package/dist/Accordion.js +62 -208
- package/dist/Accordion.mjs +6 -5
- package/dist/AlertBanner.js +29 -151
- package/dist/AlertBanner.mjs +3 -3
- package/dist/AppHeader.js +37 -233
- package/dist/AppHeader.mjs +6 -7
- package/dist/Avatar.d.mts +17 -1
- package/dist/Avatar.d.ts +17 -1
- package/dist/Avatar.js +80 -113
- package/dist/Avatar.mjs +2 -2
- package/dist/Badge.js +24 -147
- package/dist/Badge.mjs +3 -3
- package/dist/Button.js +86 -274
- package/dist/Button.mjs +6 -6
- package/dist/Card.js +15 -198
- package/dist/Card.mjs +4 -5
- package/dist/CategoryStrip.d.mts +0 -5
- package/dist/CategoryStrip.d.ts +0 -5
- package/dist/CategoryStrip.js +47 -263
- package/dist/CategoryStrip.mjs +6 -6
- package/dist/Checkbox.js +15 -198
- package/dist/Checkbox.mjs +5 -5
- package/dist/Chip.js +44 -234
- package/dist/Chip.mjs +7 -6
- package/dist/ConfirmDialog.js +100 -296
- package/dist/ConfirmDialog.mjs +7 -7
- package/dist/CurrencyDisplay.js +1 -112
- package/dist/CurrencyDisplay.mjs +2 -2
- package/dist/CurrencyInput.js +35 -160
- package/dist/CurrencyInput.mjs +5 -5
- package/dist/DetailRow.js +25 -148
- package/dist/DetailRow.mjs +3 -3
- package/dist/EmptyState.js +87 -275
- package/dist/EmptyState.mjs +7 -7
- package/dist/ErrorBoundary.js +32 -197
- package/dist/ErrorBoundary.mjs +4 -4
- package/dist/Form.js +1 -112
- package/dist/Form.mjs +2 -2
- package/dist/HolographicCard.d.mts +0 -28
- package/dist/HolographicCard.d.ts +0 -28
- package/dist/HolographicCard.js +20 -130
- package/dist/HolographicCard.mjs +9 -32
- package/dist/IconButton.js +36 -232
- package/dist/IconButton.mjs +5 -6
- package/dist/IconPicker.js +222 -927
- package/dist/IconPicker.mjs +5 -5
- package/dist/ImageUpload.d.mts +5 -1
- package/dist/ImageUpload.d.ts +5 -1
- package/dist/ImageUpload.js +32 -215
- package/dist/ImageUpload.mjs +5 -6
- package/dist/ImageViewer.js +75 -264
- package/dist/ImageViewer.mjs +8 -8
- package/dist/Input.d.mts +1 -1
- package/dist/Input.d.ts +1 -1
- package/dist/Input.js +35 -160
- package/dist/Input.mjs +4 -4
- package/dist/LabelValue.js +24 -147
- package/dist/LabelValue.mjs +3 -3
- package/dist/ListGroup.js +1 -112
- package/dist/ListGroup.mjs +2 -2
- package/dist/ListItem.js +38 -233
- package/dist/ListItem.mjs +5 -6
- package/dist/MediaCard.d.mts +0 -14
- package/dist/MediaCard.d.ts +0 -14
- package/dist/MediaCard.js +69 -313
- package/dist/MediaCard.mjs +5 -6
- package/dist/MenuGroup.js +1 -112
- package/dist/MenuGroup.mjs +2 -2
- package/dist/MenuItem.js +36 -232
- package/dist/MenuItem.mjs +5 -6
- package/dist/MonthPicker.js +8 -161
- package/dist/MonthPicker.mjs +3 -3
- package/dist/NumberStepper.js +40 -236
- package/dist/NumberStepper.mjs +5 -6
- package/dist/PagerDots.d.mts +1 -1
- package/dist/PagerDots.d.ts +1 -1
- package/dist/PagerDots.js +69 -222
- package/dist/PagerDots.mjs +6 -5
- package/dist/Pressable.js +14 -85
- package/dist/Pressable.mjs +4 -4
- package/dist/PricingCard.js +94 -279
- package/dist/PricingCard.mjs +8 -8
- package/dist/Progress.js +3 -121
- package/dist/Progress.mjs +3 -3
- package/dist/RadioGroup.js +52 -263
- package/dist/RadioGroup.mjs +5 -5
- package/dist/RetrayProvider.d.mts +1 -1
- package/dist/RetrayProvider.d.ts +1 -1
- package/dist/RetrayProvider.js +5 -6
- package/dist/RetrayProvider.mjs +3 -3
- package/dist/Select.d.mts +2 -1
- package/dist/Select.d.ts +2 -1
- package/dist/Select.js +24 -230
- package/dist/Select.mjs +4 -5
- package/dist/SelectableCard.d.mts +27 -0
- package/dist/SelectableCard.d.ts +27 -0
- package/dist/SelectableCard.js +335 -0
- package/dist/SelectableCard.mjs +8 -0
- package/dist/SelectableGrid.d.mts +0 -21
- package/dist/SelectableGrid.d.ts +0 -21
- package/dist/SelectableGrid.js +49 -269
- package/dist/SelectableGrid.mjs +5 -6
- package/dist/Separator.js +1 -112
- package/dist/Separator.mjs +2 -2
- package/dist/Sheet.js +16 -163
- package/dist/Sheet.mjs +3 -3
- package/dist/SheetSelect.js +39 -234
- package/dist/SheetSelect.mjs +6 -6
- package/dist/Skeleton.d.mts +3 -1
- package/dist/Skeleton.d.ts +3 -1
- package/dist/Skeleton.js +7 -124
- package/dist/Skeleton.mjs +3 -3
- package/dist/Slider.js +6 -159
- package/dist/Slider.mjs +3 -3
- package/dist/Spinner.js +3 -114
- package/dist/Spinner.mjs +2 -2
- package/dist/Stats.d.mts +4 -1
- package/dist/Stats.d.ts +4 -1
- package/dist/Stats.js +60 -234
- package/dist/Stats.mjs +5 -6
- package/dist/Switch.js +24 -173
- package/dist/Switch.mjs +5 -4
- package/dist/TabBar.js +43 -198
- package/dist/TabBar.mjs +5 -4
- package/dist/Tabs.js +15 -197
- package/dist/Tabs.mjs +5 -5
- package/dist/Text.js +9 -128
- package/dist/Text.mjs +2 -2
- package/dist/Textarea.d.mts +2 -1
- package/dist/Textarea.d.ts +2 -1
- package/dist/Textarea.js +71 -217
- package/dist/Textarea.mjs +4 -4
- package/dist/Toast.js +1 -112
- package/dist/Toast.mjs +2 -2
- package/dist/Toggle.js +39 -234
- package/dist/Toggle.mjs +6 -6
- package/dist/{chunk-FFTYLPSB.mjs → chunk-2QOHHBJC.mjs} +13 -7
- package/dist/{chunk-BCWEHE34.mjs → chunk-2VIDP72N.mjs} +3 -3
- package/dist/{chunk-PGERH3P7.mjs → chunk-4NQFTHN3.mjs} +13 -7
- package/dist/{chunk-3N2M3WZL.mjs → chunk-4ZO5PTKF.mjs} +4 -4
- package/dist/{chunk-MYZ2EDYU.mjs → chunk-5MYNAAFE.mjs} +13 -17
- package/dist/{chunk-E7NEHHXV.mjs → chunk-62BBSSUF.mjs} +3 -3
- package/dist/{chunk-ISY26JQJ.mjs → chunk-6CR4S6W2.mjs} +3 -3
- package/dist/{chunk-FUVYSVGR.mjs → chunk-6QLBHUEG.mjs} +8 -7
- package/dist/chunk-ARONDO7M.mjs +40 -0
- package/dist/{chunk-3UYAZ7I4.mjs → chunk-AZV7KNJI.mjs} +3 -3
- package/dist/{chunk-HLMPMUK2.mjs → chunk-BTUW5LSG.mjs} +11 -8
- package/dist/chunk-BULKGOIZ.mjs +235 -0
- package/dist/{chunk-265G6A46.mjs → chunk-CBIZLRYH.mjs} +29 -12
- package/dist/chunk-CM2DG4MR.mjs +142 -0
- package/dist/{chunk-2I2AYECM.mjs → chunk-DBHSUUKU.mjs} +2 -2
- package/dist/{chunk-P64WHW4A.mjs → chunk-DE25XTVQ.mjs} +3 -3
- package/dist/{chunk-DI7CBDL6.mjs → chunk-E4EQSCKR.mjs} +5 -5
- package/dist/{chunk-357YO24D.mjs → chunk-EHGBHFMH.mjs} +9 -17
- package/dist/{chunk-GK4VRMNE.mjs → chunk-EROPDCB5.mjs} +24 -27
- package/dist/{chunk-XBAGGKLW.mjs → chunk-ERWJPVX7.mjs} +2 -2
- package/dist/{chunk-LRM4AVYY.mjs → chunk-ESQDPO5E.mjs} +7 -7
- package/dist/{chunk-EFLFRAHD.mjs → chunk-EW2FIDSM.mjs} +1 -1
- package/dist/{chunk-7HSILTC4.mjs → chunk-FTTI6T5Q.mjs} +4 -4
- package/dist/{chunk-X26S5EVZ.mjs → chunk-HUSSF6TF.mjs} +1 -1
- package/dist/chunk-IFYMBOEN.mjs +14 -0
- package/dist/{chunk-S3KJCPEJ.mjs → chunk-IGU223UM.mjs} +80 -4
- package/dist/chunk-IJCMPVW5.mjs +121 -0
- package/dist/{chunk-I4V5XZPS.mjs → chunk-ITG4JQM3.mjs} +4 -4
- package/dist/{chunk-F4V6XLP4.mjs → chunk-K3QX2M26.mjs} +11 -8
- package/dist/{chunk-V6NFJXKO.mjs → chunk-K7TKID3V.mjs} +8 -7
- package/dist/{chunk-ZHMSAYLT.mjs → chunk-KAGADD2O.mjs} +4 -4
- package/dist/{chunk-3GEYJ7I5.mjs → chunk-KC5QDYGZ.mjs} +4 -4
- package/dist/{chunk-HJ46DTJE.mjs → chunk-KPTY7UYQ.mjs} +1 -1
- package/dist/{chunk-EMUWGDWC.mjs → chunk-KSSVIFYR.mjs} +11 -12
- package/dist/chunk-L3YKPTJQ.mjs +119 -0
- package/dist/chunk-M53LC4Q7.mjs +35 -0
- package/dist/{chunk-NXI4YDZ2.mjs → chunk-MP7GLMIR.mjs} +17 -25
- package/dist/chunk-MZ6WRTD2.mjs +40 -0
- package/dist/chunk-NGEN2EES.mjs +581 -0
- package/dist/{chunk-JULSIZDM.mjs → chunk-OBV72JD4.mjs} +1 -1
- package/dist/{chunk-2A2LEFZG.mjs → chunk-PGQ6FMXS.mjs} +6 -5
- package/dist/{chunk-BQZE3HAW.mjs → chunk-PI6RULJX.mjs} +1 -1
- package/dist/{chunk-FA2KMTH5.mjs → chunk-RA6SAAFE.mjs} +9 -8
- package/dist/{chunk-FVTVCJAH.mjs → chunk-RRKM4MKB.mjs} +7 -7
- package/dist/{chunk-AKM4EPOT.mjs → chunk-S2VGME7X.mjs} +1 -1
- package/dist/{chunk-OULVKTWL.mjs → chunk-S44XWTTC.mjs} +35 -25
- package/dist/{chunk-QSFV2P7O.mjs → chunk-SZEKQAOY.mjs} +1 -1
- package/dist/{chunk-N4ZPVCJH.mjs → chunk-TETMEKZE.mjs} +9 -9
- package/dist/{chunk-2CBQKU7H.mjs → chunk-TMH263OK.mjs} +5 -4
- package/dist/{chunk-D3Y2T42P.mjs → chunk-U6DEBYU5.mjs} +10 -9
- package/dist/{chunk-4WFMPFZB.mjs → chunk-UOKFSFNJ.mjs} +2 -2
- package/dist/{chunk-WOEWGSTU.mjs → chunk-URIH43IJ.mjs} +13 -21
- package/dist/{chunk-JCZQOY4O.mjs → chunk-V2ZB2XNS.mjs} +16 -10
- package/dist/{chunk-P73V2EKS.mjs → chunk-WIPEDNSD.mjs} +7 -7
- package/dist/{chunk-BOVUP27T.mjs → chunk-XCIG6HT2.mjs} +6 -5
- package/dist/chunk-Y6YS33GM.mjs +131 -0
- package/dist/{chunk-5OLNXP3S.mjs → chunk-ZKDKKQCE.mjs} +29 -7
- package/dist/{chunk-DF6DU42P.mjs → chunk-ZTPYUU5C.mjs} +5 -5
- package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
- package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
- package/dist/index.d.mts +15 -74
- package/dist/index.d.ts +15 -74
- package/dist/index.js +1055 -1562
- package/dist/index.mjs +81 -84
- package/package.json +8 -10
- package/src/components/Accordion/Accordion.tsx +32 -9
- package/src/components/AlertBanner/AlertBanner.tsx +7 -6
- package/src/components/AppHeader/AppHeader.tsx +1 -1
- package/src/components/Avatar/Avatar.tsx +92 -1
- package/src/components/Avatar/index.ts +2 -2
- package/src/components/Badge/Badge.tsx +2 -2
- package/src/components/Button/Button.tsx +64 -57
- package/src/components/Card/Card.tsx +1 -0
- package/src/components/CategoryStrip/CategoryStrip.tsx +36 -49
- package/src/components/Chip/Chip.tsx +5 -4
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +13 -6
- package/src/components/DetailRow/DetailRow.tsx +3 -3
- package/src/components/EmptyState/EmptyState.tsx +2 -2
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +6 -6
- package/src/components/HolographicCard/HolographicCard.tsx +14 -95
- package/src/components/IconButton/IconButton.tsx +2 -2
- package/src/components/IconPicker/IconPicker.tsx +13 -12
- package/src/components/ImageUpload/ImageUpload.tsx +24 -28
- package/src/components/ImageViewer/ImageViewer.tsx +3 -3
- package/src/components/Input/Input.tsx +11 -5
- package/src/components/LabelValue/LabelValue.tsx +2 -2
- package/src/components/ListItem/ListItem.tsx +4 -4
- package/src/components/MediaCard/MediaCard.tsx +21 -59
- package/src/components/MenuItem/MenuItem.tsx +2 -2
- package/src/components/MonthPicker/MonthPicker.tsx +2 -2
- package/src/components/NumberStepper/NumberStepper.tsx +6 -6
- package/src/components/PagerDots/PagerDots.tsx +38 -28
- package/src/components/PricingCard/PricingCard.tsx +6 -6
- package/src/components/RadioGroup/RadioGroup.tsx +18 -31
- package/src/components/Select/Select.tsx +32 -39
- package/src/components/SelectableCard/SelectableCard.tsx +302 -0
- package/src/components/SelectableCard/index.ts +1 -0
- package/src/components/SelectableGrid/SelectableGrid.tsx +38 -72
- package/src/components/Sheet/Sheet.tsx +11 -4
- package/src/components/SheetSelect/SheetSelect.tsx +3 -3
- package/src/components/Skeleton/Skeleton.tsx +6 -3
- package/src/components/Spinner/Spinner.tsx +2 -2
- package/src/components/Stats/Stats.tsx +36 -8
- package/src/components/Switch/Switch.tsx +9 -6
- package/src/components/TabBar/TabBar.tsx +9 -8
- package/src/components/Text/Text.tsx +12 -1
- package/src/components/Textarea/Textarea.tsx +18 -32
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/hooks/useConfirmDialog.ts +31 -42
- package/src/index.ts +4 -4
- package/src/theme/ThemeProvider.tsx +1 -4
- package/src/theme/colorUtils.ts +1 -72
- package/src/theme/colors.ts +47 -1
- package/src/theme/types.ts +6 -3
- package/src/utils/animations.ts +0 -47
- package/src/utils/curatedIcons.ts +93 -801
- package/src/utils/haptics.ts +13 -208
- package/src/utils/icons.ts +27 -91
- package/src/utils/pressable.ts +10 -61
- package/dist/VirtualList.d.mts +0 -19
- package/dist/VirtualList.d.ts +0 -19
- package/dist/VirtualList.js +0 -38
- package/dist/VirtualList.mjs +0 -2
- package/dist/chunk-3DKJ2GIC.mjs +0 -30
- package/dist/chunk-AQEVCEXV.mjs +0 -164
- package/dist/chunk-DOGIPOF5.mjs +0 -131
- package/dist/chunk-DVK4G2GT.mjs +0 -59
- package/dist/chunk-EJ7ZPXOH.mjs +0 -163
- package/dist/chunk-J6Q2YJEV.mjs +0 -134
- package/dist/chunk-JNVAIDLK.mjs +0 -136
- package/dist/chunk-KA7LTET3.mjs +0 -71
- package/dist/chunk-KHYX4IOM.mjs +0 -1114
- package/dist/chunk-NC5ZTR2Y.mjs +0 -32
- package/dist/chunk-YNROWHQJ.mjs +0 -46
- package/src/components/VirtualList/VirtualList.tsx +0 -60
- package/src/components/VirtualList/index.ts +0 -1
- package/src/utils/fontGuard.ts +0 -35
- package/src/utils/hover.ts +0 -25
- package/src/utils/useColorTransition.ts +0 -40
- package/src/utils/usePressScale.ts +0 -75
|
@@ -4,10 +4,11 @@ import { impactLight } from '../../utils/haptics'
|
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
6
6
|
import { RADIUS } from '../../tokens'
|
|
7
|
-
import {
|
|
7
|
+
import { Icon } from '../../utils/icons'
|
|
8
8
|
import { PressableCard } from '../../utils/pressable'
|
|
9
9
|
|
|
10
10
|
export type StatsVariant = 'elevated' | 'outlined' | 'filled'
|
|
11
|
+
export type StatsSize = 'default' | 'compact'
|
|
11
12
|
|
|
12
13
|
export interface StatsProps {
|
|
13
14
|
value: string
|
|
@@ -17,6 +18,8 @@ export interface StatsProps {
|
|
|
17
18
|
iconName?: string
|
|
18
19
|
iconColor?: string
|
|
19
20
|
variant?: StatsVariant
|
|
21
|
+
/** `'compact'` reduces everything proportionally for tight grids. */
|
|
22
|
+
size?: StatsSize
|
|
20
23
|
onPress?: () => void
|
|
21
24
|
style?: ViewStyle
|
|
22
25
|
accessibilityLabel?: string
|
|
@@ -38,6 +41,7 @@ function StatsComponent({
|
|
|
38
41
|
iconName,
|
|
39
42
|
iconColor,
|
|
40
43
|
variant = 'elevated',
|
|
44
|
+
size = 'default',
|
|
41
45
|
onPress,
|
|
42
46
|
style,
|
|
43
47
|
accessibilityLabel,
|
|
@@ -60,6 +64,30 @@ function StatsComponent({
|
|
|
60
64
|
|
|
61
65
|
const isCompact = containerWidth > 0 && containerWidth < COMPACT_THRESHOLD && !!(icon ?? iconName)
|
|
62
66
|
|
|
67
|
+
const sizeStyles = size === 'compact'
|
|
68
|
+
? {
|
|
69
|
+
valueFontFamily: 'Sohne-SemiBold' as const,
|
|
70
|
+
valueFontSize: ms(16),
|
|
71
|
+
valueLineHeight: mvs(20),
|
|
72
|
+
labelFontSize: ms(11),
|
|
73
|
+
labelLineHeight: mvs(14),
|
|
74
|
+
descriptionFontSize: ms(10),
|
|
75
|
+
descriptionLineHeight: mvs(14),
|
|
76
|
+
iconSize: ms(18),
|
|
77
|
+
padding: s(12),
|
|
78
|
+
}
|
|
79
|
+
: {
|
|
80
|
+
valueFontFamily: 'Sohne-Bold' as const,
|
|
81
|
+
valueFontSize: ms(21),
|
|
82
|
+
valueLineHeight: mvs(25),
|
|
83
|
+
labelFontSize: ms(13),
|
|
84
|
+
labelLineHeight: mvs(18),
|
|
85
|
+
descriptionFontSize: ms(12),
|
|
86
|
+
descriptionLineHeight: mvs(16),
|
|
87
|
+
iconSize: ms(20),
|
|
88
|
+
padding: s(16),
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
const variantStyle: ViewStyle = {
|
|
64
92
|
elevated: {
|
|
65
93
|
backgroundColor: colors.card,
|
|
@@ -86,29 +114,29 @@ function StatsComponent({
|
|
|
86
114
|
|
|
87
115
|
const iconColorResolved = iconColor ?? colors.primary
|
|
88
116
|
|
|
89
|
-
const resolvedIcon = iconName ?
|
|
117
|
+
const resolvedIcon = iconName ? <Icon name={iconName} size={sizeStyles.iconSize} color={iconColorResolved} /> : icon
|
|
90
118
|
|
|
91
119
|
const iconElement = resolvedIcon ? (
|
|
92
120
|
<View style={styles.iconWrapper}>{resolvedIcon}</View>
|
|
93
121
|
) : null
|
|
94
122
|
|
|
95
123
|
const valueElement = (
|
|
96
|
-
<Text style={[styles.value, { color: colors.foreground }]} allowFontScaling={true}>
|
|
124
|
+
<Text style={[styles.value, { color: colors.foreground, fontFamily: sizeStyles.valueFontFamily, fontSize: sizeStyles.valueFontSize, lineHeight: sizeStyles.valueLineHeight }]} allowFontScaling={true}>
|
|
97
125
|
{value}
|
|
98
126
|
</Text>
|
|
99
127
|
)
|
|
100
128
|
|
|
101
129
|
const cardContent = (
|
|
102
|
-
<View style={[styles.card, variantStyle, style]} onLayout={handleLayout}>
|
|
130
|
+
<View style={[styles.card, variantStyle, { padding: sizeStyles.padding }, style]} onLayout={handleLayout}>
|
|
103
131
|
{isCompact ? (
|
|
104
132
|
<>
|
|
105
133
|
{iconElement}
|
|
106
134
|
<View style={styles.compactValue}>{valueElement}</View>
|
|
107
|
-
<Text style={[styles.label, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
|
|
135
|
+
<Text style={[styles.label, { color: colors.foregroundSubtle, fontSize: sizeStyles.labelFontSize, lineHeight: sizeStyles.labelLineHeight }]} allowFontScaling={true}>
|
|
108
136
|
{label}
|
|
109
137
|
</Text>
|
|
110
138
|
{description ? (
|
|
111
|
-
<Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
139
|
+
<Text style={[styles.description, { color: colors.foregroundMuted, fontSize: sizeStyles.descriptionFontSize, lineHeight: sizeStyles.descriptionLineHeight }]} allowFontScaling={true}>
|
|
112
140
|
{description}
|
|
113
141
|
</Text>
|
|
114
142
|
) : null}
|
|
@@ -119,11 +147,11 @@ function StatsComponent({
|
|
|
119
147
|
{iconElement}
|
|
120
148
|
{valueElement}
|
|
121
149
|
</View>
|
|
122
|
-
<Text style={[styles.label, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
|
|
150
|
+
<Text style={[styles.label, { color: colors.foregroundSubtle, fontSize: sizeStyles.labelFontSize, lineHeight: sizeStyles.labelLineHeight }]} allowFontScaling={true}>
|
|
123
151
|
{label}
|
|
124
152
|
</Text>
|
|
125
153
|
{description ? (
|
|
126
|
-
<Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
154
|
+
<Text style={[styles.description, { color: colors.foregroundMuted, fontSize: sizeStyles.descriptionFontSize, lineHeight: sizeStyles.descriptionLineHeight }]} allowFontScaling={true}>
|
|
127
155
|
{description}
|
|
128
156
|
</Text>
|
|
129
157
|
) : null}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { StyleSheet, ViewStyle, View } from 'react-native'
|
|
3
3
|
import { EaseView } from 'react-native-ease'
|
|
4
4
|
import { Feather } from '@expo/vector-icons'
|
|
5
5
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
6
6
|
import { useTheme } from '../../theme'
|
|
7
7
|
import { s } from '../../utils/scaling'
|
|
8
8
|
import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
|
|
9
|
+
import { PressableButton } from '../../utils/pressable'
|
|
9
10
|
|
|
10
11
|
const TRACK_WIDTH = s(52)
|
|
11
12
|
const TRACK_HEIGHT = s(30)
|
|
@@ -30,14 +31,14 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
|
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
33
|
<View style={[{ alignSelf: 'flex-start' }, style]}>
|
|
33
|
-
<
|
|
34
|
+
<PressableButton
|
|
34
35
|
onPress={() => {
|
|
35
36
|
hapticSelection()
|
|
36
37
|
onCheckedChange?.(!checked)
|
|
37
38
|
}}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
touchSoundDisabled
|
|
39
|
+
enabled={!disabled}
|
|
40
|
+
rippleColor="transparent"
|
|
41
|
+
touchSoundDisabled
|
|
41
42
|
accessibilityRole="switch"
|
|
42
43
|
accessibilityLabel={accessibilityLabel}
|
|
43
44
|
accessibilityState={{ checked, disabled: isDisabled }}
|
|
@@ -76,7 +77,7 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
|
|
|
76
77
|
</EaseView>
|
|
77
78
|
</EaseView>
|
|
78
79
|
</View>
|
|
79
|
-
</
|
|
80
|
+
</PressableButton>
|
|
80
81
|
</View>
|
|
81
82
|
)
|
|
82
83
|
}
|
|
@@ -84,6 +85,8 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
|
|
|
84
85
|
const styles = StyleSheet.create({
|
|
85
86
|
touchable: {
|
|
86
87
|
alignSelf: 'flex-start',
|
|
88
|
+
minHeight: 44,
|
|
89
|
+
justifyContent: 'center',
|
|
87
90
|
},
|
|
88
91
|
trackContainer: {
|
|
89
92
|
position: 'relative',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { View, Text,
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
|
-
import {
|
|
5
|
+
import { Icon } from '../../utils/icons'
|
|
6
6
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
7
7
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
8
|
+
import { PressableTab } from '../../utils/pressable'
|
|
8
9
|
|
|
9
10
|
export interface TabBarItem {
|
|
10
11
|
/** Unique key for the tab. */
|
|
@@ -74,23 +75,23 @@ export function TabBar({
|
|
|
74
75
|
{items.map((item) => {
|
|
75
76
|
const active = item.key === activeKey
|
|
76
77
|
const tint = active ? resolvedActive : resolvedInactive
|
|
77
|
-
const iconNode = item.icon ?? (item.iconName ?
|
|
78
|
+
const iconNode = item.icon ?? (item.iconName ? <Icon name={item.iconName} size={ms(24)} color={tint} /> : null)
|
|
78
79
|
const showBadge = item.badge !== undefined && item.badge !== false
|
|
79
80
|
const badgeCount = typeof item.badge === 'number' ? item.badge : undefined
|
|
80
81
|
|
|
81
82
|
return (
|
|
82
|
-
<
|
|
83
|
+
<PressableTab
|
|
83
84
|
key={item.key}
|
|
84
|
-
style={styles.tab}
|
|
85
85
|
onPress={() => {
|
|
86
86
|
if (!active) hapticSelection()
|
|
87
87
|
onTabPress(item.key)
|
|
88
88
|
}}
|
|
89
|
-
|
|
90
|
-
touchSoundDisabled
|
|
89
|
+
rippleColor="transparent"
|
|
90
|
+
touchSoundDisabled
|
|
91
91
|
accessibilityRole="tab"
|
|
92
92
|
accessibilityState={{ selected: active }}
|
|
93
93
|
accessibilityLabel={item.label ?? item.key}
|
|
94
|
+
style={styles.tab}
|
|
94
95
|
>
|
|
95
96
|
<View>
|
|
96
97
|
{iconNode}
|
|
@@ -115,7 +116,7 @@ export function TabBar({
|
|
|
115
116
|
{item.label}
|
|
116
117
|
</Text>
|
|
117
118
|
) : null}
|
|
118
|
-
</
|
|
119
|
+
</PressableTab>
|
|
119
120
|
)
|
|
120
121
|
})}
|
|
121
122
|
</View>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
declare const __DEV__: boolean | undefined
|
|
3
5
|
import { useTheme } from '../../theme'
|
|
4
6
|
import { TYPOGRAPHY } from '../../tokens'
|
|
5
7
|
import { ms, mvs } from '../../utils/scaling'
|
|
6
|
-
import {
|
|
8
|
+
import { isLoaded as expoFontIsLoaded } from 'expo-font'
|
|
7
9
|
|
|
8
10
|
export type TextVariant =
|
|
9
11
|
| 'display-hero'
|
|
@@ -70,6 +72,15 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
|
|
|
70
72
|
'button-sm': 'foreground',
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
let fontWarned = false
|
|
76
|
+
function warnIfFontsMissing(): void {
|
|
77
|
+
if (fontWarned || typeof __DEV__ === 'undefined' || !__DEV__) return
|
|
78
|
+
fontWarned = true
|
|
79
|
+
if (!expoFontIsLoaded('Sohne-Regular')) {
|
|
80
|
+
console.warn('[retray-ui-kit] Sohne fonts not loaded — text falls back to system font.')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
function TextBase({ variant = 'body-md', color, style, uppercase, children, ...props }: TextProps) {
|
|
74
85
|
warnIfFontsMissing()
|
|
75
86
|
const { colors } = useTheme()
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
|
|
3
|
-
import
|
|
4
|
-
useAnimatedStyle,
|
|
5
|
-
interpolateColor,
|
|
6
|
-
interpolate,
|
|
7
|
-
} from 'react-native-reanimated'
|
|
3
|
+
import { EaseView } from 'react-native-ease'
|
|
8
4
|
import { useTheme } from '../../theme'
|
|
9
5
|
import { s, vs, ms } from '../../utils/scaling'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { TIMINGS } from '../../utils/animations'
|
|
6
|
+
import { Icon } from '../../utils/icons'
|
|
7
|
+
import { COLOR_TRANSITION } from '../../utils/animations'
|
|
13
8
|
|
|
14
9
|
const webInputResetStyle: Record<string, unknown> =
|
|
15
10
|
Platform.OS === 'web'
|
|
@@ -20,6 +15,7 @@ export interface TextareaProps extends TextInputProps {
|
|
|
20
15
|
label?: string
|
|
21
16
|
error?: string
|
|
22
17
|
hint?: string
|
|
18
|
+
disabled?: boolean
|
|
23
19
|
rows?: number
|
|
24
20
|
prefixIcon?: string
|
|
25
21
|
prefixIconNode?: React.ReactNode
|
|
@@ -31,6 +27,7 @@ export function Textarea({
|
|
|
31
27
|
label,
|
|
32
28
|
error,
|
|
33
29
|
hint,
|
|
30
|
+
disabled,
|
|
34
31
|
rows = 4,
|
|
35
32
|
prefixIcon,
|
|
36
33
|
prefixIconNode,
|
|
@@ -44,46 +41,36 @@ export function Textarea({
|
|
|
44
41
|
}: TextareaProps) {
|
|
45
42
|
const { colors } = useTheme()
|
|
46
43
|
const [focused, setFocused] = useState(false)
|
|
47
|
-
const focusProgress = useColorTransition(focused, {
|
|
48
|
-
duration: focused ? TIMINGS.focusIn.duration : TIMINGS.focusOut.duration,
|
|
49
|
-
})
|
|
50
44
|
|
|
51
45
|
const resolvedPrefixIcon = prefixIcon
|
|
52
|
-
?
|
|
46
|
+
? <Icon name={prefixIcon} size={ms(16)} color={prefixIconColor ?? colors.foregroundMuted} />
|
|
53
47
|
: prefixIconNode
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
// focus weight change never resizes the box / reflows content.
|
|
57
|
-
const borderAnimStyle = useAnimatedStyle(() => ({
|
|
58
|
-
borderColor: error
|
|
59
|
-
? colors.destructive
|
|
60
|
-
: interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
|
|
61
|
-
borderWidth: error
|
|
62
|
-
? 2
|
|
63
|
-
: interpolate(focusProgress.value, [0, 1], [1, 2]),
|
|
64
|
-
}))
|
|
49
|
+
const borderColor = error ? colors.destructive : (focused ? colors.primary : colors.border)
|
|
65
50
|
|
|
66
51
|
return (
|
|
67
52
|
<View style={[styles.container, containerStyle]}>
|
|
68
53
|
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
styles.
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
<View style={[styles.inputWrapper, { backgroundColor: colors.background }]}>
|
|
55
|
+
<EaseView
|
|
56
|
+
style={[styles.borderOverlay, { borderWidth: error ? 2 : 1 }]}
|
|
57
|
+
animate={{ borderColor }}
|
|
58
|
+
transition={COLOR_TRANSITION}
|
|
59
|
+
pointerEvents="none"
|
|
60
|
+
/>
|
|
76
61
|
{resolvedPrefixIcon ? <View style={styles.prefixIcon}>{resolvedPrefixIcon}</View> : null}
|
|
77
62
|
<TextInput
|
|
78
63
|
multiline
|
|
79
64
|
numberOfLines={rows}
|
|
80
65
|
textAlignVertical="top"
|
|
66
|
+
editable={!disabled}
|
|
81
67
|
style={[
|
|
82
68
|
styles.input,
|
|
83
69
|
{
|
|
84
70
|
color: colors.foreground,
|
|
85
71
|
minHeight: rows * vs(30),
|
|
86
72
|
},
|
|
73
|
+
disabled && { opacity: 0.45 },
|
|
87
74
|
webInputResetStyle,
|
|
88
75
|
style,
|
|
89
76
|
]}
|
|
@@ -98,9 +85,10 @@ export function Textarea({
|
|
|
98
85
|
placeholderTextColor={colors.foregroundMuted}
|
|
99
86
|
allowFontScaling={true}
|
|
100
87
|
accessibilityLabel={accessibilityLabel ?? label}
|
|
88
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
101
89
|
{...props}
|
|
102
90
|
/>
|
|
103
|
-
</
|
|
91
|
+
</View>
|
|
104
92
|
{error ? (
|
|
105
93
|
<Text
|
|
106
94
|
style={[styles.helperText, { color: colors.destructive }]}
|
|
@@ -128,8 +116,6 @@ const styles = StyleSheet.create({
|
|
|
128
116
|
marginBottom: vs(2),
|
|
129
117
|
},
|
|
130
118
|
inputWrapper: {
|
|
131
|
-
// Border lives on borderOverlay (absolute); wrapper carries none so the
|
|
132
|
-
// focus weight change never reflows content.
|
|
133
119
|
borderRadius: 8,
|
|
134
120
|
paddingHorizontal: s(14),
|
|
135
121
|
paddingVertical: vs(11),
|
|
@@ -5,7 +5,7 @@ import { FontAwesome5 } from '@expo/vector-icons'
|
|
|
5
5
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
6
6
|
import { useTheme } from '../../theme'
|
|
7
7
|
import { s, vs, ms } from '../../utils/scaling'
|
|
8
|
-
import {
|
|
8
|
+
import { Icon } from '../../utils/icons'
|
|
9
9
|
import { COLOR_TRANSITION } from '../../utils/animations'
|
|
10
10
|
import { PressableButton } from '../../utils/pressable'
|
|
11
11
|
|
|
@@ -30,13 +30,13 @@ function ToggleIcon({ pressed, iconName, activeIconName, icon, activeIcon, iconC
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (pressed) {
|
|
33
|
-
if (activeIconName) return
|
|
33
|
+
if (activeIconName) return <Icon name={activeIconName} size={iconSize} color={activeIconColor ?? primaryColor} />
|
|
34
34
|
const active = renderProp(activeIcon)
|
|
35
35
|
if (active) return <>{active}</>
|
|
36
36
|
return <FontAwesome5 name="check-circle" size={iconSize} color={primaryColor} />
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if (iconName) return
|
|
39
|
+
if (iconName) return <Icon name={iconName} size={iconSize} color={iconColor ?? mutedColor} />
|
|
40
40
|
const custom = renderProp(icon)
|
|
41
41
|
if (custom) return <>{custom}</>
|
|
42
42
|
|
|
@@ -1,67 +1,56 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react'
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
2
|
|
|
3
3
|
export interface UseConfirmDialogOptions {
|
|
4
4
|
onConfirm: () => void | Promise<void>
|
|
5
5
|
onCancel?: () => void
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export interface UseConfirmDialogResult
|
|
9
|
-
/** Pass to ConfirmDialog `visible` prop. */
|
|
8
|
+
export interface UseConfirmDialogResult {
|
|
10
9
|
visible: boolean
|
|
11
|
-
/** The value passed to `open()` — available during the confirmation flow. */
|
|
12
|
-
target: T | null
|
|
13
|
-
/** Whether `onConfirm` is currently executing. Pass to ConfirmDialog `loading` prop. */
|
|
14
10
|
loading: boolean
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
dialogProps: {
|
|
19
|
-
visible: boolean
|
|
20
|
-
loading: boolean
|
|
21
|
-
onConfirm: () => void
|
|
22
|
-
onCancel: () => void
|
|
23
|
-
}
|
|
11
|
+
open: () => void
|
|
12
|
+
onConfirm: () => void
|
|
13
|
+
onCancel: () => void
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
export function useConfirmDialog
|
|
27
|
-
options: UseConfirmDialogOptions,
|
|
28
|
-
): UseConfirmDialogResult<T> {
|
|
16
|
+
export function useConfirmDialog(options: UseConfirmDialogOptions): UseConfirmDialogResult {
|
|
29
17
|
const [visible, setVisible] = useState(false)
|
|
30
|
-
const [target, setTarget] = useState<T | null>(null)
|
|
31
18
|
const [loading, setLoading] = useState(false)
|
|
19
|
+
const mountedRef = useRef(true)
|
|
20
|
+
const onConfirmRef = useRef(options.onConfirm)
|
|
21
|
+
const onCancelRef = useRef(options.onCancel)
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
onConfirmRef.current = options.onConfirm
|
|
25
|
+
onCancelRef.current = options.onCancel
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
return () => {
|
|
30
|
+
mountedRef.current = false
|
|
31
|
+
}
|
|
36
32
|
}, [])
|
|
37
33
|
|
|
34
|
+
const open = useCallback(() => setVisible(true), [])
|
|
35
|
+
|
|
38
36
|
const handleConfirm = useCallback(async () => {
|
|
39
37
|
setLoading(true)
|
|
40
38
|
try {
|
|
41
|
-
await
|
|
39
|
+
await onConfirmRef.current()
|
|
40
|
+
} catch {
|
|
41
|
+
/* consumer handles error in onConfirm */
|
|
42
42
|
} finally {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
if (mountedRef.current) {
|
|
44
|
+
setLoading(false)
|
|
45
|
+
setVisible(false)
|
|
46
|
+
}
|
|
46
47
|
}
|
|
47
|
-
}, [
|
|
48
|
+
}, [])
|
|
48
49
|
|
|
49
50
|
const handleCancel = useCallback(() => {
|
|
50
51
|
setVisible(false)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}, [options])
|
|
52
|
+
onCancelRef.current?.()
|
|
53
|
+
}, [])
|
|
54
54
|
|
|
55
|
-
return {
|
|
56
|
-
visible,
|
|
57
|
-
target,
|
|
58
|
-
loading,
|
|
59
|
-
open,
|
|
60
|
-
dialogProps: {
|
|
61
|
-
visible,
|
|
62
|
-
loading,
|
|
63
|
-
onConfirm: handleConfirm,
|
|
64
|
-
onCancel: handleCancel,
|
|
65
|
-
},
|
|
66
|
-
}
|
|
55
|
+
return { visible, loading, open, onConfirm: handleConfirm, onCancel: handleCancel }
|
|
67
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,12 +44,12 @@ export * from './components/CategoryStrip'
|
|
|
44
44
|
export * from './components/Pressable'
|
|
45
45
|
export * from './components/DetailRow'
|
|
46
46
|
export * from './components/Form'
|
|
47
|
-
export * from './components/VirtualList'
|
|
48
47
|
export * from './components/RetrayProvider'
|
|
49
48
|
export * from './components/ErrorBoundary'
|
|
50
49
|
export * from './components/PagerDots'
|
|
51
50
|
export * from './components/AppHeader'
|
|
52
51
|
export * from './components/SelectableGrid'
|
|
52
|
+
export * from './components/SelectableCard'
|
|
53
53
|
export * from './components/PricingCard'
|
|
54
54
|
export * from './components/TabBar'
|
|
55
55
|
export * from './components/ImageViewer'
|
|
@@ -63,10 +63,10 @@ export * from './components/Stats'
|
|
|
63
63
|
// barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
|
|
64
64
|
|
|
65
65
|
// Icon utility
|
|
66
|
-
export { Icon
|
|
66
|
+
export { Icon } from './utils/icons'
|
|
67
67
|
|
|
68
68
|
// Color utilities
|
|
69
|
-
export { withAlpha } from './theme/colorUtils'
|
|
69
|
+
export { withAlpha, hexToRgb } from './theme/colorUtils'
|
|
70
70
|
|
|
71
71
|
// Typography utilities
|
|
72
72
|
export { getResponsiveFontSize } from './utils/typography'
|
|
@@ -81,13 +81,13 @@ export {
|
|
|
81
81
|
notificationSuccess,
|
|
82
82
|
notificationError,
|
|
83
83
|
notificationWarning,
|
|
84
|
-
richHaptics,
|
|
85
84
|
} from './utils/haptics'
|
|
86
85
|
|
|
87
86
|
// Hooks
|
|
88
87
|
export { useConfirmDialog } from './hooks/useConfirmDialog'
|
|
89
88
|
export type { UseConfirmDialogOptions, UseConfirmDialogResult } from './hooks/useConfirmDialog'
|
|
90
89
|
|
|
90
|
+
|
|
91
91
|
// Design tokens
|
|
92
92
|
export {
|
|
93
93
|
SPACING,
|
|
@@ -3,10 +3,7 @@ import { useColorScheme } from 'react-native'
|
|
|
3
3
|
import { ThemeColors, Theme, ColorScheme, ThemeContextValue } from './types'
|
|
4
4
|
import { defaultLight, defaultDark, deriveColors } from './colors'
|
|
5
5
|
|
|
6
|
-
const ThemeContext = createContext<ThemeContextValue>(
|
|
7
|
-
colors: deriveColors(defaultLight, 'light'),
|
|
8
|
-
colorScheme: 'light',
|
|
9
|
-
})
|
|
6
|
+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
|
|
10
7
|
|
|
11
8
|
export interface ThemeProviderProps {
|
|
12
9
|
children: React.ReactNode
|
package/src/theme/colorUtils.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// All functions are pure — no side effects, no React dependencies.
|
|
3
|
-
|
|
4
|
-
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
1
|
+
export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
5
2
|
const clean = hex.replace('#', '')
|
|
6
3
|
const full = clean.length === 3
|
|
7
4
|
? clean.split('').map(c => c + c).join('')
|
|
@@ -14,74 +11,6 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
|
14
11
|
}
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
function componentToHex(c: number): string {
|
|
18
|
-
return Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function rgbToHex(r: number, g: number, b: number): string {
|
|
22
|
-
return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Returns hex color with alpha blended onto a white background (for tint derivation)
|
|
26
|
-
export function withAlphaOnWhite(hex: string, alpha: number): string {
|
|
27
|
-
const rgb = hexToRgb(hex)
|
|
28
|
-
if (!rgb) return hex
|
|
29
|
-
const r = rgb.r * alpha + 255 * (1 - alpha)
|
|
30
|
-
const g = rgb.g * alpha + 255 * (1 - alpha)
|
|
31
|
-
const b = rgb.b * alpha + 255 * (1 - alpha)
|
|
32
|
-
return rgbToHex(r, g, b)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Returns hex color with alpha blended onto a dark background (for dark mode tints)
|
|
36
|
-
export function withAlphaOnDark(hex: string, alpha: number, bgHex = '#0f0f0f'): string {
|
|
37
|
-
const rgb = hexToRgb(hex)
|
|
38
|
-
const bg = hexToRgb(bgHex)
|
|
39
|
-
if (!rgb || !bg) return hex
|
|
40
|
-
const r = rgb.r * alpha + bg.r * (1 - alpha)
|
|
41
|
-
const g = rgb.g * alpha + bg.g * (1 - alpha)
|
|
42
|
-
const b = rgb.b * alpha + bg.b * (1 - alpha)
|
|
43
|
-
return rgbToHex(r, g, b)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Mix foreground color with background at given opacity (for text hierarchy)
|
|
47
|
-
export function mixWithBackground(fgHex: string, bgHex: string, opacity: number): string {
|
|
48
|
-
const fg = hexToRgb(fgHex)
|
|
49
|
-
const bg = hexToRgb(bgHex)
|
|
50
|
-
if (!fg || !bg) return fgHex
|
|
51
|
-
const r = fg.r * opacity + bg.r * (1 - opacity)
|
|
52
|
-
const g = fg.g * opacity + bg.g * (1 - opacity)
|
|
53
|
-
const b = fg.b * opacity + bg.b * (1 - opacity)
|
|
54
|
-
return rgbToHex(r, g, b)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Lighten a hex color by mixing with white
|
|
58
|
-
export function lighten(hex: string, amount: number): string {
|
|
59
|
-
return withAlphaOnWhite(hex, 1 - amount)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Darken a hex color by mixing with black
|
|
63
|
-
export function darken(hex: string, amount: number): string {
|
|
64
|
-
const rgb = hexToRgb(hex)
|
|
65
|
-
if (!rgb) return hex
|
|
66
|
-
return rgbToHex(rgb.r * (1 - amount), rgb.g * (1 - amount), rgb.b * (1 - amount))
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Detect if a hex color is "dark" (luminance < 0.5)
|
|
70
|
-
export function isDark(hex: string): boolean {
|
|
71
|
-
const rgb = hexToRgb(hex)
|
|
72
|
-
if (!rgb) return false
|
|
73
|
-
// Relative luminance (WCAG formula)
|
|
74
|
-
const toLinear = (c: number) => {
|
|
75
|
-
const s = c / 255
|
|
76
|
-
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
|
|
77
|
-
}
|
|
78
|
-
const L = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b)
|
|
79
|
-
return L < 0.5
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Convert a hex color to rgba with the given alpha.
|
|
83
|
-
// Returns an rgba() string suitable for use with semi-transparent backgrounds,
|
|
84
|
-
// borders, and overlays.
|
|
85
14
|
export function withAlpha(hex: string, alpha: number): string {
|
|
86
15
|
const rgb = hexToRgb(hex)
|
|
87
16
|
if (!rgb) return hex
|