@retray-dev/ui-kit 3.1.0 → 5.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.
Files changed (50) hide show
  1. package/COMPONENTS.md +1792 -659
  2. package/README.md +8 -7
  3. package/dist/index.d.mts +269 -89
  4. package/dist/index.d.ts +269 -89
  5. package/dist/index.js +1034 -312
  6. package/dist/index.mjs +1031 -314
  7. package/package.json +3 -2
  8. package/src/components/Accordion/Accordion.tsx +1 -1
  9. package/src/components/AlertBanner/AlertBanner.tsx +50 -45
  10. package/src/components/Avatar/Avatar.tsx +61 -17
  11. package/src/components/Badge/Badge.tsx +17 -15
  12. package/src/components/Button/Button.tsx +31 -42
  13. package/src/components/Card/Card.tsx +4 -4
  14. package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
  15. package/src/components/CategoryStrip/index.ts +2 -0
  16. package/src/components/Checkbox/Checkbox.tsx +44 -16
  17. package/src/components/Chip/Chip.tsx +1 -1
  18. package/src/components/ConfirmDialog/ConfirmDialog.tsx +4 -4
  19. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
  20. package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
  21. package/src/components/EmptyState/EmptyState.tsx +9 -9
  22. package/src/components/IconButton/IconButton.tsx +74 -34
  23. package/src/components/Input/Input.tsx +15 -13
  24. package/src/components/LabelValue/LabelValue.tsx +1 -1
  25. package/src/components/ListItem/ListItem.tsx +5 -5
  26. package/src/components/MediaCard/MediaCard.tsx +249 -0
  27. package/src/components/MediaCard/index.ts +2 -0
  28. package/src/components/Pressable/Pressable.tsx +100 -0
  29. package/src/components/Pressable/index.ts +1 -0
  30. package/src/components/Progress/Progress.tsx +14 -7
  31. package/src/components/RadioGroup/RadioGroup.tsx +1 -1
  32. package/src/components/Select/Select.tsx +5 -5
  33. package/src/components/Sheet/Sheet.tsx +3 -9
  34. package/src/components/Skeleton/Skeleton.tsx +34 -7
  35. package/src/components/Slider/Slider.tsx +2 -2
  36. package/src/components/Spinner/Spinner.tsx +1 -1
  37. package/src/components/Switch/Switch.tsx +31 -4
  38. package/src/components/Tabs/Tabs.tsx +63 -45
  39. package/src/components/Text/Text.tsx +59 -10
  40. package/src/components/Textarea/Textarea.tsx +4 -3
  41. package/src/components/Toast/Toast.tsx +77 -36
  42. package/src/components/Toggle/Toggle.tsx +3 -3
  43. package/src/index.ts +8 -2
  44. package/src/theme/ThemeProvider.tsx +11 -10
  45. package/src/theme/colorUtils.ts +80 -0
  46. package/src/theme/colors.ts +76 -35
  47. package/src/theme/index.ts +2 -2
  48. package/src/theme/types.ts +27 -13
  49. package/src/tokens.ts +150 -13
  50. package/src/utils/hover.ts +25 -0
@@ -1,25 +1,25 @@
1
1
  import React, { createContext, useContext, useMemo } from 'react'
2
2
  import { useColorScheme } from 'react-native'
3
3
  import { ThemeColors, Theme, ColorScheme, ThemeContextValue } from './types'
4
- import { defaultLight, defaultDark } from './colors'
4
+ import { defaultLight, defaultDark, deriveColors } from './colors'
5
5
 
6
6
  const ThemeContext = createContext<ThemeContextValue>({
7
- colors: defaultLight,
7
+ colors: deriveColors(defaultLight, 'light'),
8
8
  colorScheme: 'light',
9
9
  })
10
10
 
11
11
  export interface ThemeProviderProps {
12
12
  children: React.ReactNode
13
13
  /**
14
- * Optional full-palette overrides per scheme. Supply a partial or full `ThemeColors` object
15
- * for `light` and/or `dark` to override the defaults.
16
- * @example
17
- * { light: { primary: '#6366f1', card: '#fff' }, dark: { primary: '#818cf8' } }
14
+ * Optional token overrides per scheme. Supply any of the 12 public ThemeColors
15
+ * tokens for `light` and/or `dark`. All derived colors are recomputed automatically.
16
+ * @example
17
+ * { light: { primary: '#ff385c' }, dark: { primary: '#ff385c' } }
18
18
  */
19
19
  theme?: Theme
20
20
  /**
21
- * - `'system'` (default): auto-detects device setting and updates when it changes.
22
- * - `'light'` / `'dark'`: forces a specific scheme regardless of device setting.
21
+ * - `'system'` (default): auto-detects device setting.
22
+ * - `'light'` / `'dark'`: forces a specific scheme.
23
23
  */
24
24
  colorScheme?: ColorScheme
25
25
  }
@@ -28,10 +28,11 @@ export function ThemeProvider({ children, theme, colorScheme = 'system' }: Theme
28
28
  const systemScheme = useColorScheme() ?? 'light'
29
29
  const resolvedScheme: 'light' | 'dark' = colorScheme === 'system' ? systemScheme : colorScheme
30
30
 
31
- const colors = useMemo<ThemeColors>(() => {
31
+ const colors = useMemo(() => {
32
32
  const base = resolvedScheme === 'dark' ? defaultDark : defaultLight
33
33
  const override = resolvedScheme === 'dark' ? theme?.dark : theme?.light
34
- return override ? { ...base, ...override } : base
34
+ const merged: ThemeColors = override ? { ...base, ...override } : base
35
+ return deriveColors(merged, resolvedScheme)
35
36
  }, [resolvedScheme, theme])
36
37
 
37
38
  return (
@@ -0,0 +1,80 @@
1
+ // Hex color manipulation utilities for internal theme derivation.
2
+ // All functions are pure — no side effects, no React dependencies.
3
+
4
+ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
5
+ const clean = hex.replace('#', '')
6
+ const full = clean.length === 3
7
+ ? clean.split('').map(c => c + c).join('')
8
+ : clean
9
+ if (full.length !== 6) return null
10
+ return {
11
+ r: parseInt(full.slice(0, 2), 16),
12
+ g: parseInt(full.slice(2, 4), 16),
13
+ b: parseInt(full.slice(4, 6), 16),
14
+ }
15
+ }
16
+
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
+ }
@@ -1,54 +1,95 @@
1
- import { ThemeColors } from './types'
1
+ import { ThemeColors, ResolvedColors } from './types'
2
+ import { mixWithBackground, withAlphaOnWhite, withAlphaOnDark, lighten, darken, isDark } from './colorUtils'
3
+
4
+ // ─── Default palettes (12 tokens each) ───────────────────────────────────────
5
+ // These are the only values consumers need to override.
6
+ // Designed to look great out of the box with no customization.
2
7
 
3
- // Full, explicit theme palettes. No derivation — palettes are direct and fully customizable.
4
8
  export const defaultLight: ThemeColors = {
5
9
  background: '#ffffff',
6
- foreground: '#171717',
10
+ foreground: '#222222', // Airbnb ink — deep near-black, never pure black
7
11
  card: '#ffffff',
8
- cardForeground: '#171717',
9
- primary: '#1a1a1a',
12
+ primary: '#1a1a1a', // Near-black primary — clean, premium default
10
13
  primaryForeground: '#ffffff',
11
- secondary: '#f1f1f1',
12
- secondaryForeground: '#171717',
13
- muted: '#f1f1f1',
14
- mutedForeground: '#a2a2a2',
15
- accent: '#e4e4e4',
16
- accentForeground: '#171717',
17
- destructive: '#ef4444',
14
+ border: '#dddddd', // Airbnb hairline — light, airy
15
+ destructive: '#e53935',
18
16
  destructiveForeground: '#ffffff',
19
- border: '#e5e5e5',
20
- input: '#e5e5e5',
21
- ring: '#1a1a1a',
22
17
  success: '#1a7a45',
23
18
  successForeground: '#ffffff',
24
- destructiveTint: '#fff5f5',
25
- destructiveBorder: '#fecaca',
26
- successTint: '#f0fdf4',
27
- successBorder: '#bbf7d0',
19
+ warning: '#e67e00',
20
+ warningForeground: '#ffffff',
28
21
  }
29
22
 
30
23
  export const defaultDark: ThemeColors = {
31
24
  background: '#0f0f0f',
32
25
  foreground: '#fafafa',
33
26
  card: '#1c1c1c',
34
- cardForeground: '#fafafa',
35
27
  primary: '#fafafa',
36
28
  primaryForeground: '#0f0f0f',
37
- secondary: '#272727',
38
- secondaryForeground: '#fafafa',
39
- muted: '#272727',
40
- mutedForeground: '#9a9a9a',
41
- accent: '#2e2e2e',
42
- accentForeground: '#fafafa',
43
- destructive: '#dc2626',
44
- destructiveForeground: '#ffffff',
45
29
  border: '#303030',
46
- input: '#2a2a2a',
47
- ring: '#fafafa',
48
- success: '#166534',
30
+ destructive: '#ef5350',
31
+ destructiveForeground: '#ffffff',
32
+ success: '#2e7d52',
49
33
  successForeground: '#ffffff',
50
- destructiveTint: '#3b0a0a',
51
- destructiveBorder: '#7f1d1d',
52
- successTint: '#052e16',
53
- successBorder: '#166534',
34
+ warning: '#f57c00',
35
+ warningForeground: '#ffffff',
36
+ }
37
+
38
+ // ─── Color derivation ─────────────────────────────────────────────────────────
39
+ // Takes 12 public tokens → produces full ResolvedColors for component consumption.
40
+ // Dark mode uses bg-blended tints instead of white-blended to stay on-palette.
41
+
42
+ export function deriveColors(t: ThemeColors, scheme: 'light' | 'dark'): ResolvedColors {
43
+ const dark = scheme === 'dark'
44
+ const bg = t.background
45
+
46
+ // Text hierarchy: foreground mixed with background at different opacities
47
+ const foregroundSubtle = mixWithBackground(t.foreground, bg, 0.55)
48
+ const foregroundMuted = mixWithBackground(t.foreground, bg, 0.38)
49
+
50
+ // Surface fills: slight offset from background
51
+ const surface = dark
52
+ ? lighten(bg, -0.06) // slightly lighter than dark bg
53
+ : darken(bg, 0.04) // slightly darker than white → #f7f7f7 equivalent
54
+ const surfaceStrong = dark
55
+ ? lighten(bg, -0.12)
56
+ : darken(bg, 0.08) // #ebebeb equivalent
57
+
58
+ // Semantic tints: color blended toward background
59
+ const destructiveTint = dark
60
+ ? withAlphaOnDark(t.destructive, 0.15, bg)
61
+ : withAlphaOnWhite(t.destructive, 0.08)
62
+ const destructiveBorder = dark
63
+ ? withAlphaOnDark(t.destructive, 0.45, bg)
64
+ : withAlphaOnWhite(t.destructive, 0.30)
65
+
66
+ const successTint = dark
67
+ ? withAlphaOnDark(t.success, 0.15, bg)
68
+ : withAlphaOnWhite(t.success, 0.08)
69
+ const successBorder = dark
70
+ ? withAlphaOnDark(t.success, 0.45, bg)
71
+ : withAlphaOnWhite(t.success, 0.30)
72
+
73
+ const warningTint = dark
74
+ ? withAlphaOnDark(t.warning, 0.15, bg)
75
+ : withAlphaOnWhite(t.warning, 0.08)
76
+ const warningBorder = dark
77
+ ? withAlphaOnDark(t.warning, 0.45, bg)
78
+ : withAlphaOnWhite(t.warning, 0.30)
79
+
80
+ return {
81
+ ...t,
82
+ foregroundSubtle,
83
+ foregroundMuted,
84
+ surface,
85
+ surfaceStrong,
86
+ destructiveTint,
87
+ destructiveBorder,
88
+ successTint,
89
+ successBorder,
90
+ warningTint,
91
+ warningBorder,
92
+ ring: t.primary, // focus ring always = primary
93
+ input: t.border, // input border always = border
94
+ }
54
95
  }
@@ -1,4 +1,4 @@
1
1
  export { ThemeProvider, useTheme } from './ThemeProvider'
2
2
  export type { ThemeProviderProps } from './ThemeProvider'
3
- export type { ThemeColors, Theme, ColorScheme } from './types'
4
- export { defaultLight, defaultDark } from './colors'
3
+ export type { ThemeColors, ResolvedColors, Theme, ColorScheme } from './types'
4
+ export { defaultLight, defaultDark, deriveColors } from './colors'
@@ -1,31 +1,45 @@
1
+ // 12 public tokens — the only values consumers need to supply.
2
+ // All derived colors (tints, borders, surface, text hierarchy) are computed
3
+ // internally by deriveColors() and exposed via useTheme().colors as ResolvedColors.
1
4
  export type ThemeColors = {
2
5
  background: string
3
6
  foreground: string
4
7
  card: string
5
- cardForeground: string
6
8
  primary: string
7
9
  primaryForeground: string
8
- secondary: string
9
- secondaryForeground: string
10
- muted: string
11
- mutedForeground: string
12
- accent: string
13
- accentForeground: string
10
+ border: string
14
11
  destructive: string
15
12
  destructiveForeground: string
16
- border: string
17
- input: string
18
- ring: string
19
13
  success: string
20
14
  successForeground: string
15
+ warning: string
16
+ warningForeground: string
17
+ }
18
+
19
+ // Full resolved palette — what components actually consume via useTheme().
20
+ // Derived from ThemeColors. Never supplied by consumers directly.
21
+ export type ResolvedColors = ThemeColors & {
22
+ // Text hierarchy
23
+ foregroundSubtle: string // ~55% — body text, subtitles
24
+ foregroundMuted: string // ~35% — captions, timestamps, placeholders
25
+
26
+ // Surface fills (chips unselected, input bg, tag bg, skeleton)
27
+ surface: string // background slightly off-canvas
28
+ surfaceStrong: string // slightly stronger fill for pressed/hover states
29
+
30
+ // Semantic tints (light bg for alert banners, toast backgrounds)
21
31
  destructiveTint: string
22
32
  destructiveBorder: string
23
33
  successTint: string
24
34
  successBorder: string
35
+ warningTint: string
36
+ warningBorder: string
37
+
38
+ // Aliases (ring + input always equal primary + border for coherence)
39
+ ring: string // = primary
40
+ input: string // = border
25
41
  }
26
42
 
27
- // Theme overrides: consumers may supply partial or full `ThemeColors` objects
28
- // for `theme.light` / `theme.dark` to override only the tokens they want.
29
43
  export type Theme = {
30
44
  light?: Partial<ThemeColors>
31
45
  dark?: Partial<ThemeColors>
@@ -34,6 +48,6 @@ export type Theme = {
34
48
  export type ColorScheme = 'light' | 'dark' | 'system'
35
49
 
36
50
  export type ThemeContextValue = {
37
- colors: ThemeColors
51
+ colors: ResolvedColors
38
52
  colorScheme: 'light' | 'dark'
39
53
  }
package/src/tokens.ts CHANGED
@@ -1,13 +1,18 @@
1
+ // ─── Spacing ─────────────────────────────────────────────────────────────────
2
+ // 8pt grid with 2pt micro-step. `section` for major band separators (Airbnb: 64px).
1
3
  export const SPACING = {
4
+ xxs: 2,
2
5
  xs: 4,
3
6
  sm: 8,
4
7
  md: 12,
5
- lg: 16,
6
- xl: 24,
7
- '2xl': 32,
8
- '3xl': 48,
8
+ base: 16,
9
+ lg: 24,
10
+ xl: 32,
11
+ xxl: 48,
12
+ section: 64,
9
13
  } as const
10
14
 
15
+ // ─── Icon sizes ───────────────────────────────────────────────────────────────
11
16
  export const ICON_SIZES = {
12
17
  sm: 14,
13
18
  md: 18,
@@ -16,49 +21,178 @@ export const ICON_SIZES = {
16
21
  '2xl': 32,
17
22
  } as const
18
23
 
24
+ // ─── Border radius ────────────────────────────────────────────────────────────
25
+ // Airbnb-aligned shape language — soft everywhere, no hard corners on interactive elements.
26
+ // xs: micro chips/tags sm: inputs/forms md: cards lg: sheet corners xl: primary CTAs (pill)
19
27
  export const RADIUS = {
20
- sm: 4,
21
- md: 8,
22
- lg: 12,
23
- xl: 16,
28
+ none: 0,
29
+ xs: 4,
30
+ sm: 8,
31
+ md: 14,
32
+ lg: 20,
33
+ xl: 32,
24
34
  full: 9999,
25
35
  } as const
26
36
 
37
+ // ─── Shadows ──────────────────────────────────────────────────────────────────
38
+ // Multi-tier. Default card usage = sm. Hover float = md. Modals = lg/xl.
27
39
  export const SHADOWS = {
28
40
  sm: {
29
41
  shadowColor: '#000',
30
42
  shadowOffset: { width: 0, height: 1 },
31
- shadowOpacity: 0.08,
43
+ shadowOpacity: 0.06,
32
44
  shadowRadius: 4,
33
45
  elevation: 2,
34
46
  },
35
47
  md: {
36
48
  shadowColor: '#000',
37
- shadowOffset: { width: 0, height: 3 },
38
- shadowOpacity: 0.12,
49
+ shadowOffset: { width: 0, height: 2 },
50
+ shadowOpacity: 0.10,
39
51
  shadowRadius: 8,
40
52
  elevation: 5,
41
53
  },
42
54
  lg: {
43
55
  shadowColor: '#000',
44
56
  shadowOffset: { width: 0, height: 6 },
45
- shadowOpacity: 0.2,
57
+ shadowOpacity: 0.16,
46
58
  shadowRadius: 16,
47
59
  elevation: 10,
48
60
  },
49
61
  xl: {
50
62
  shadowColor: '#000',
51
63
  shadowOffset: { width: 0, height: 12 },
52
- shadowOpacity: 0.28,
64
+ shadowOpacity: 0.24,
53
65
  shadowRadius: 24,
54
66
  elevation: 18,
55
67
  },
56
68
  } as const
57
69
 
70
+ // ─── Breakpoints ──────────────────────────────────────────────────────────────
58
71
  export const BREAKPOINTS = {
59
72
  wide: 700,
60
73
  } as const
61
74
 
75
+ // ─── Typography ───────────────────────────────────────────────────────────────
76
+ // Airbnb-inspired hierarchy. Modest weights — 500-600 on display, not 700+.
77
+ // Exception: display-hero (large number display) and display-xl always bold.
78
+ // Poppins carries the full scale. All fontFamily values reference loaded font names.
79
+ export const TYPOGRAPHY = {
80
+ 'display-hero': {
81
+ fontFamily: 'Poppins-Bold',
82
+ fontSize: 64,
83
+ fontWeight: '700' as const,
84
+ lineHeight: 70,
85
+ letterSpacing: -1,
86
+ },
87
+ 'display-xl': {
88
+ fontFamily: 'Poppins-Bold',
89
+ fontSize: 28,
90
+ fontWeight: '700' as const,
91
+ lineHeight: 40,
92
+ letterSpacing: 0,
93
+ },
94
+ 'display-lg': {
95
+ fontFamily: 'Poppins-Medium',
96
+ fontSize: 22,
97
+ fontWeight: '500' as const,
98
+ lineHeight: 26,
99
+ letterSpacing: -0.44,
100
+ },
101
+ 'display-md': {
102
+ fontFamily: 'Poppins-Bold',
103
+ fontSize: 21,
104
+ fontWeight: '700' as const,
105
+ lineHeight: 30,
106
+ letterSpacing: 0,
107
+ },
108
+ 'display-sm': {
109
+ fontFamily: 'Poppins-SemiBold',
110
+ fontSize: 20,
111
+ fontWeight: '600' as const,
112
+ lineHeight: 24,
113
+ letterSpacing: -0.18,
114
+ },
115
+ 'title-md': {
116
+ fontFamily: 'Poppins-SemiBold',
117
+ fontSize: 16,
118
+ fontWeight: '600' as const,
119
+ lineHeight: 20,
120
+ letterSpacing: 0,
121
+ },
122
+ 'title-sm': {
123
+ fontFamily: 'Poppins-Medium',
124
+ fontSize: 16,
125
+ fontWeight: '500' as const,
126
+ lineHeight: 20,
127
+ letterSpacing: 0,
128
+ },
129
+ 'body-md': {
130
+ fontFamily: 'Poppins-Regular',
131
+ fontSize: 16,
132
+ fontWeight: '400' as const,
133
+ lineHeight: 24,
134
+ letterSpacing: 0,
135
+ },
136
+ 'body-sm': {
137
+ fontFamily: 'Poppins-Regular',
138
+ fontSize: 14,
139
+ fontWeight: '400' as const,
140
+ lineHeight: 20,
141
+ letterSpacing: 0,
142
+ },
143
+ caption: {
144
+ fontFamily: 'Poppins-Medium',
145
+ fontSize: 14,
146
+ fontWeight: '500' as const,
147
+ lineHeight: 18,
148
+ letterSpacing: 0,
149
+ },
150
+ 'caption-sm': {
151
+ fontFamily: 'Poppins-Regular',
152
+ fontSize: 13,
153
+ fontWeight: '400' as const,
154
+ lineHeight: 16,
155
+ letterSpacing: 0,
156
+ },
157
+ 'badge-text': {
158
+ fontFamily: 'Poppins-SemiBold',
159
+ fontSize: 11,
160
+ fontWeight: '600' as const,
161
+ lineHeight: 13,
162
+ letterSpacing: 0,
163
+ },
164
+ 'micro-label': {
165
+ fontFamily: 'Poppins-Bold',
166
+ fontSize: 12,
167
+ fontWeight: '700' as const,
168
+ lineHeight: 16,
169
+ letterSpacing: 0,
170
+ },
171
+ 'uppercase-tag': {
172
+ fontFamily: 'Poppins-Bold',
173
+ fontSize: 8,
174
+ fontWeight: '700' as const,
175
+ lineHeight: 10,
176
+ letterSpacing: 0.32,
177
+ textTransform: 'uppercase' as const,
178
+ },
179
+ 'button-lg': {
180
+ fontFamily: 'Poppins-Medium',
181
+ fontSize: 16,
182
+ fontWeight: '500' as const,
183
+ lineHeight: 20,
184
+ letterSpacing: 0,
185
+ },
186
+ 'button-sm': {
187
+ fontFamily: 'Poppins-Medium',
188
+ fontSize: 14,
189
+ fontWeight: '500' as const,
190
+ lineHeight: 18,
191
+ letterSpacing: 0,
192
+ },
193
+ } as const
194
+
195
+ // ─── Types ────────────────────────────────────────────────────────────────────
62
196
  export type Spacing = typeof SPACING
63
197
  export type SpacingKey = keyof Spacing
64
198
 
@@ -67,3 +201,6 @@ export type IconSizeKey = keyof IconSize
67
201
 
68
202
  export type Radius = typeof RADIUS
69
203
  export type RadiusKey = keyof Radius
204
+
205
+ export type Typography = typeof TYPOGRAPHY
206
+ export type TypographyKey = keyof Typography
@@ -0,0 +1,25 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { Platform } from 'react-native'
3
+
4
+ export interface HoverHandlers {
5
+ onMouseEnter?: () => void
6
+ onMouseLeave?: () => void
7
+ }
8
+
9
+ /**
10
+ * Web-only hover state hook. Returns `{ hovered, hoverHandlers }`.
11
+ * On native (iOS/Android) `hovered` is always false and handlers are no-ops.
12
+ * Spread `hoverHandlers` onto any View/TouchableOpacity to activate.
13
+ */
14
+ export function useHover(): { hovered: boolean; hoverHandlers: HoverHandlers } {
15
+ const [hovered, setHovered] = useState(false)
16
+
17
+ const onMouseEnter = useCallback(() => setHovered(true), [])
18
+ const onMouseLeave = useCallback(() => setHovered(false), [])
19
+
20
+ if (Platform.OS !== 'web') {
21
+ return { hovered: false, hoverHandlers: {} }
22
+ }
23
+
24
+ return { hovered, hoverHandlers: { onMouseEnter, onMouseLeave } }
25
+ }