@retray-dev/ui-kit 4.0.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.
- package/COMPONENTS.md +1791 -663
- package/README.md +4 -3
- package/dist/index.d.mts +268 -83
- package/dist/index.d.ts +268 -83
- package/dist/index.js +1032 -309
- package/dist/index.mjs +1029 -311
- package/package.json +3 -2
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/AlertBanner/AlertBanner.tsx +50 -45
- package/src/components/Avatar/Avatar.tsx +61 -17
- package/src/components/Badge/Badge.tsx +17 -15
- package/src/components/Button/Button.tsx +31 -42
- package/src/components/Card/Card.tsx +4 -4
- package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
- package/src/components/CategoryStrip/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.tsx +44 -16
- package/src/components/Chip/Chip.tsx +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -3
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
- package/src/components/EmptyState/EmptyState.tsx +9 -9
- package/src/components/IconButton/IconButton.tsx +74 -34
- package/src/components/Input/Input.tsx +15 -13
- package/src/components/LabelValue/LabelValue.tsx +1 -1
- package/src/components/ListItem/ListItem.tsx +5 -5
- package/src/components/MediaCard/MediaCard.tsx +249 -0
- package/src/components/MediaCard/index.ts +2 -0
- package/src/components/Pressable/Pressable.tsx +100 -0
- package/src/components/Pressable/index.ts +1 -0
- package/src/components/Progress/Progress.tsx +14 -7
- package/src/components/RadioGroup/RadioGroup.tsx +1 -1
- package/src/components/Select/Select.tsx +5 -5
- package/src/components/Sheet/Sheet.tsx +2 -2
- package/src/components/Skeleton/Skeleton.tsx +34 -7
- package/src/components/Slider/Slider.tsx +2 -2
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Switch/Switch.tsx +31 -4
- package/src/components/Tabs/Tabs.tsx +63 -45
- package/src/components/Text/Text.tsx +59 -10
- package/src/components/Textarea/Textarea.tsx +4 -3
- package/src/components/Toast/Toast.tsx +77 -36
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/index.ts +8 -2
- package/src/theme/ThemeProvider.tsx +11 -10
- package/src/theme/colorUtils.ts +80 -0
- package/src/theme/colors.ts +76 -35
- package/src/theme/index.ts +2 -2
- package/src/theme/types.ts +27 -13
- package/src/tokens.ts +150 -13
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
22
|
-
* - `'light'` / `'dark'`: forces a specific scheme
|
|
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
|
|
31
|
+
const colors = useMemo(() => {
|
|
32
32
|
const base = resolvedScheme === 'dark' ? defaultDark : defaultLight
|
|
33
33
|
const override = resolvedScheme === 'dark' ? theme?.dark : theme?.light
|
|
34
|
-
|
|
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
|
+
}
|
package/src/theme/colors.ts
CHANGED
|
@@ -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: '#
|
|
10
|
+
foreground: '#222222', // Airbnb ink — deep near-black, never pure black
|
|
7
11
|
card: '#ffffff',
|
|
8
|
-
|
|
9
|
-
primary: '#1a1a1a',
|
|
12
|
+
primary: '#1a1a1a', // Near-black primary — clean, premium default
|
|
10
13
|
primaryForeground: '#ffffff',
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
success: '#
|
|
30
|
+
destructive: '#ef5350',
|
|
31
|
+
destructiveForeground: '#ffffff',
|
|
32
|
+
success: '#2e7d52',
|
|
49
33
|
successForeground: '#ffffff',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
}
|
package/src/theme/index.ts
CHANGED
|
@@ -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'
|
package/src/theme/types.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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:
|
|
38
|
-
shadowOpacity: 0.
|
|
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.
|
|
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.
|
|
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
|
+
}
|