feat : Composant Bouton
This commit is contained in:
178
package-lock.json
generated
178
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo-google-fonts/jetbrains-mono": "^0.4.1",
|
||||
"@expo-google-fonts/space-grotesk": "^0.4.1",
|
||||
"@expo/ui": "~56.0.14",
|
||||
"@react-navigation/bottom-tabs": "^7.18.18",
|
||||
@@ -33,15 +34,17 @@
|
||||
"expo-system-ui": "~56.0.5",
|
||||
"expo-web-browser": "~56.0.5",
|
||||
"lucide-react": "^1.38.0",
|
||||
"lucide-react-native": "^1.38.0",
|
||||
"nativewind": "^4.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.85.3",
|
||||
"react-native-gesture-handler": "~2.31.1",
|
||||
"react-native-reanimated": "^4.3.1",
|
||||
"react-native-reanimated": "4.3.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "4.25.2",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.8.3",
|
||||
"tailwindcss": "^3.4.19"
|
||||
@@ -1288,6 +1291,12 @@
|
||||
"integrity": "sha512-syfiImMaDmq7cFi0of+waE2M4uSCyd16zgyWxdPOY7fN2VBmSLKEzkfbZgeOjJq61kSqPBNNtXjggiQiSD6gMQ==",
|
||||
"license": "MIT AND OFL-1.1"
|
||||
},
|
||||
"node_modules/@expo-google-fonts/jetbrains-mono": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@expo-google-fonts/jetbrains-mono/-/jetbrains-mono-0.4.1.tgz",
|
||||
"integrity": "sha512-CslACrtMOcRwoWXCO7OMEI+9w3fukWSoBtvNz46OqPoogEuuoY0tkDY1O8sFumk8t0pC6Cx0Xr95O0TOQhpkug==",
|
||||
"license": "MIT AND OFL-1.1"
|
||||
},
|
||||
"node_modules/@expo-google-fonts/material-symbols": {
|
||||
"version": "0.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.38.tgz",
|
||||
@@ -3656,6 +3665,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||
@@ -4139,6 +4154,56 @@
|
||||
"hyphenate-style-name": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
@@ -4285,6 +4350,61 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -4312,6 +4432,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-stack-parser": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||
@@ -6655,6 +6787,17 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react-native": {
|
||||
"version": "1.38.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-1.38.0.tgz",
|
||||
"integrity": "sha512-wOgCXC8VZxIkrDK1160JA+7cBQ9njTFnFLQHLvrxLI5Cdhgz/dNN0j9+MS9aUJnYdBi0y3mNj73ZLnuzV6pYPg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-native": "*",
|
||||
"react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
@@ -6680,6 +6823,12 @@
|
||||
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
@@ -7315,6 +7464,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nullthrows": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||
@@ -8585,6 +8746,21 @@
|
||||
"react-native": ">=0.82.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.15.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.4.tgz",
|
||||
"integrity": "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^1.1.3",
|
||||
"warn-once": "0.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo-google-fonts/jetbrains-mono": "^0.4.1",
|
||||
"@expo-google-fonts/space-grotesk": "^0.4.1",
|
||||
"@expo/ui": "~56.0.14",
|
||||
"@react-navigation/bottom-tabs": "^7.18.18",
|
||||
@@ -28,15 +29,17 @@
|
||||
"expo-system-ui": "~56.0.5",
|
||||
"expo-web-browser": "~56.0.5",
|
||||
"lucide-react": "^1.38.0",
|
||||
"lucide-react-native": "^1.38.0",
|
||||
"nativewind": "^4.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.85.3",
|
||||
"react-native-gesture-handler": "~2.31.1",
|
||||
"react-native-reanimated": "^4.3.1",
|
||||
"react-native-reanimated": "4.3.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "4.25.2",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.8.3",
|
||||
"tailwindcss": "^3.4.19"
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
import { ThemeProvider } from '../theme';
|
||||
import { useAppFonts } from '../theme/typography';
|
||||
|
||||
export default function RootLayout() {
|
||||
const [fontsLoaded] = useAppFonts();
|
||||
|
||||
if (!fontsLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Stack />
|
||||
|
||||
@@ -1,48 +1,137 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { ArrowUp, ArrowUpRight, Plus } from 'lucide-react-native';
|
||||
import { useCallback } from 'react';
|
||||
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { Button } from '@/components/Button';
|
||||
import { useTheme } from '@/theme/useTheme';
|
||||
|
||||
import '../global.css';
|
||||
import { useTheme } from '../theme';
|
||||
|
||||
export default function Index() {
|
||||
const { colors, spacing, typography } = useTheme();
|
||||
|
||||
const simulateSubmit = useCallback(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 1300);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: colors.textSecondary,
|
||||
fontFamily: typography.sectionLabel.fontFamily,
|
||||
fontSize: typography.sectionLabel.fontSize,
|
||||
lineHeight: typography.sectionLabel.lineHeight,
|
||||
letterSpacing: typography.sectionLabel.letterSpacing,
|
||||
},
|
||||
]}>
|
||||
GradientButton variants
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.screen, { backgroundColor: colors.deepBackground }]}>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.content,
|
||||
{
|
||||
padding: spacing[24],
|
||||
gap: spacing[24],
|
||||
},
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={{ gap: spacing[8] }}>
|
||||
<Text
|
||||
style={[
|
||||
typography.sectionLabel,
|
||||
{
|
||||
color: colors.textSecondary,
|
||||
letterSpacing: 1.1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
BOUTONS · HAUTEUR 52 PX · RAYON 14 PX
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Publier" variant="primary" />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Primaire</Text>
|
||||
</View>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Publier" variant="primary" />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Pressé (tap)</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Intéressé" variant="secondary" rightIcon={ArrowUpRight} />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Secondaire</Text>
|
||||
</View>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Publier" variant="primary" disabled />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Désactivé</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Tout voir" variant="ghost" fullWidth={false} style={styles.autoWidthButton} />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Texte</Text>
|
||||
</View>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Envoyer" variant="primary" autoLoading loadingLabel="Envoi..." onPress={simulateSubmit} />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Chargement</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.cell}>
|
||||
<Button label="Supprimer" variant="destructive" />
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Destructif</Text>
|
||||
</View>
|
||||
<View style={styles.cell}>
|
||||
<View style={styles.iconRow}>
|
||||
<Button
|
||||
iconOnly
|
||||
variant="secondary"
|
||||
fullWidth={false}
|
||||
icon={ArrowUp}
|
||||
iconOnlyShape="default"
|
||||
accessibilityLabel="Partager"
|
||||
/>
|
||||
<Button
|
||||
iconOnly
|
||||
variant="primary"
|
||||
fullWidth={false}
|
||||
icon={Plus}
|
||||
iconOnlyShape="round"
|
||||
accessibilityLabel="Ajouter"
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.caption, typography.digits, { color: colors.textSecondary }]}>Icône / FAB</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
screen: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
content: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 14,
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 24,
|
||||
gap: 10,
|
||||
},
|
||||
caption: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
iconRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 20,
|
||||
},
|
||||
stack: {
|
||||
width: '100%',
|
||||
},
|
||||
disabledWrap: {
|
||||
marginTop: 24,
|
||||
autoWidthButton: {
|
||||
alignSelf: 'center',
|
||||
width: 140,
|
||||
},
|
||||
});
|
||||
|
||||
261
src/components/Button.tsx
Normal file
261
src/components/Button.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
type GestureResponderEvent,
|
||||
Pressable,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
type TextStyle,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native';
|
||||
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
|
||||
import { useTheme } from '@/theme/useTheme';
|
||||
|
||||
const BUTTON_HEIGHT = 52;
|
||||
const PRESSED_SCALE = 0.97;
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
|
||||
type IconOnlyShape = 'round' | 'default';
|
||||
|
||||
type ButtonProps = {
|
||||
label?: string;
|
||||
variant?: ButtonVariant;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
loadingLabel?: string;
|
||||
autoLoading?: boolean;
|
||||
iconOnly?: boolean;
|
||||
iconOnlyShape?: IconOnlyShape;
|
||||
icon?: LucideIcon;
|
||||
leftIcon?: LucideIcon;
|
||||
rightIcon?: LucideIcon;
|
||||
iconSize?: number;
|
||||
iconStrokeWidth?: number;
|
||||
fullWidth?: boolean;
|
||||
onPress?: (event: GestureResponderEvent) => void | Promise<void>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
accessibilityLabel?: string;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
export function Button({
|
||||
label,
|
||||
variant = 'primary',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
loadingLabel,
|
||||
autoLoading = false,
|
||||
iconOnly = false,
|
||||
iconOnlyShape = 'round',
|
||||
icon,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
iconSize = 20,
|
||||
iconStrokeWidth = 2,
|
||||
fullWidth = true,
|
||||
onPress,
|
||||
style,
|
||||
textStyle,
|
||||
accessibilityLabel,
|
||||
testID,
|
||||
}: ButtonProps) {
|
||||
const { colors, borderRadius, typography, gradients, gradientDirection } = useTheme();
|
||||
const [internalLoading, setInternalLoading] = useState(false);
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
|
||||
const isLoading = loading || internalLoading;
|
||||
const isDisabled = disabled || isLoading;
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
const handlePressIn = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPressed(true);
|
||||
scale.value = withTiming(PRESSED_SCALE, { duration: 120 });
|
||||
}, [isDisabled, scale]);
|
||||
|
||||
const handlePressOut = useCallback(() => {
|
||||
setIsPressed(false);
|
||||
scale.value = withTiming(1, { duration: 140 });
|
||||
}, [scale]);
|
||||
|
||||
const handlePress = useCallback(
|
||||
async (event: GestureResponderEvent) => {
|
||||
if (!onPress || isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = onPress(event);
|
||||
|
||||
if (!autoLoading || !(result instanceof Promise)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInternalLoading(true);
|
||||
try {
|
||||
await result;
|
||||
} finally {
|
||||
setInternalLoading(false);
|
||||
}
|
||||
},
|
||||
[autoLoading, isDisabled, onPress],
|
||||
);
|
||||
|
||||
const labelColor = useMemo(() => {
|
||||
if (isDisabled) {
|
||||
return colors.textDisabled;
|
||||
}
|
||||
|
||||
if (variant === 'primary') {
|
||||
return colors.textPrimary;
|
||||
}
|
||||
|
||||
if (variant === 'destructive') {
|
||||
return colors.danger;
|
||||
}
|
||||
|
||||
if (variant === 'ghost') {
|
||||
return colors.tertiary;
|
||||
}
|
||||
|
||||
return colors.textPrimary;
|
||||
}, [colors, isDisabled, variant]);
|
||||
|
||||
const loadingColor = variant === 'primary' && !isDisabled ? colors.textPrimary : labelColor;
|
||||
const iconOnlyRadius = iconOnlyShape === 'round' ? BUTTON_HEIGHT / 2 : borderRadius.button;
|
||||
const sideIconColor = !isDisabled && variant === 'secondary' ? colors.tertiary : labelColor;
|
||||
const iconOnlyColor = labelColor;
|
||||
|
||||
const IconOnly = icon;
|
||||
const LeftIcon = leftIcon;
|
||||
const RightIcon = rightIcon;
|
||||
|
||||
const containerStyle: ViewStyle = {
|
||||
width: fullWidth && !iconOnly ? '100%' : BUTTON_HEIGHT,
|
||||
height: BUTTON_HEIGHT,
|
||||
borderRadius: iconOnly ? iconOnlyRadius : borderRadius.button,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
const baseFillStyle: ViewStyle = {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
borderRadius: iconOnly ? iconOnlyRadius : borderRadius.button,
|
||||
};
|
||||
|
||||
const variantFillStyle: ViewStyle = (() => {
|
||||
if (isDisabled) {
|
||||
return {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
};
|
||||
}
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return {
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
};
|
||||
}
|
||||
|
||||
if (variant === 'ghost') {
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
};
|
||||
}
|
||||
|
||||
if (variant === 'destructive') {
|
||||
return {
|
||||
backgroundColor: 'rgba(255, 77, 109, 0.08)',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.danger,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundColor: colors.surface,
|
||||
};
|
||||
})();
|
||||
|
||||
const contentPaddingStyle: ViewStyle = {
|
||||
paddingHorizontal: iconOnly ? 0 : 24,
|
||||
};
|
||||
|
||||
const textStyles: TextStyle = {
|
||||
...typography.button,
|
||||
color: labelColor,
|
||||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{isLoading ? <ActivityIndicator color={loadingColor} size="small" /> : null}
|
||||
{!isLoading && iconOnly && IconOnly ? (
|
||||
<IconOnly color={iconOnlyColor} size={iconSize} strokeWidth={iconStrokeWidth} />
|
||||
) : null}
|
||||
{!iconOnly && !isLoading && LeftIcon ? (
|
||||
<LeftIcon color={sideIconColor} size={iconSize} strokeWidth={iconStrokeWidth} />
|
||||
) : null}
|
||||
{!iconOnly && label ? <Text style={[textStyles, textStyle]}>{isLoading && loadingLabel ? loadingLabel : label}</Text> : null}
|
||||
{!iconOnly && !isLoading && RightIcon ? (
|
||||
<RightIcon color={sideIconColor} size={iconSize} strokeWidth={iconStrokeWidth} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const fillNode =
|
||||
variant === 'primary' && !isDisabled ? (
|
||||
<LinearGradient
|
||||
colors={isPressed ? gradients.signaturePressed : gradients.signature}
|
||||
start={gradientDirection.start}
|
||||
end={gradientDirection.end}
|
||||
style={[baseFillStyle, contentPaddingStyle]}
|
||||
>
|
||||
{content}
|
||||
</LinearGradient>
|
||||
) : (
|
||||
<View style={[baseFillStyle, contentPaddingStyle, variantFillStyle]}>{content}</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: isDisabled, busy: isLoading }}
|
||||
accessibilityLabel={accessibilityLabel ?? label}
|
||||
disabled={isDisabled}
|
||||
onPress={handlePress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={[styles.root, containerStyle, animatedStyle, style]}
|
||||
>
|
||||
{fillNode}
|
||||
</AnimatedPressable>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type PropsWithChildren, useMemo } from 'react';
|
||||
|
||||
import colors from './colors';
|
||||
import { colors, glow, gradientDirection, gradients, palette } from './colors';
|
||||
import spacing, { borderRadius } from './spacing';
|
||||
import typography from './typography';
|
||||
import { ThemeContext } from './useTheme';
|
||||
@@ -9,6 +9,10 @@ export function ThemeProvider({ children }: PropsWithChildren) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
colors,
|
||||
gradients,
|
||||
glow,
|
||||
gradientDirection,
|
||||
palette,
|
||||
typography,
|
||||
spacing,
|
||||
borderRadius,
|
||||
|
||||
@@ -1,17 +1,173 @@
|
||||
export const colors = {
|
||||
deepBackground: '#0A0A12',
|
||||
surface: '#14141F',
|
||||
surfaceElevated: '#1E1E2D',
|
||||
border: '#2A2A3C',
|
||||
primary: '#FF2E88',
|
||||
secondary: '#7B3DFF',
|
||||
tertiary: '#28E0F0',
|
||||
success: '#3DDC97',
|
||||
textPrimary: '#F4F4FA',
|
||||
textSecondary: '#9A9AB4',
|
||||
signatureGradient: ['#7B3DFF', '#FF2E88'] as const,
|
||||
import type { ColorValue, ViewStyle } from 'react-native';
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Palette brute — 1:1 avec les tokens :root de la maquette
|
||||
* ------------------------------------------------------------------ */
|
||||
export const palette = {
|
||||
/** Fond profond de l'app */
|
||||
ink900: '#0A0A12',
|
||||
/** Fond du canvas hors écran (planche de style / desktop) */
|
||||
ink950: '#06060C',
|
||||
/** Surface / carte */
|
||||
ink800: '#14141F',
|
||||
/** Surface élevée (champ, bouton secondaire, sheet) */
|
||||
ink700: '#1E1E2D',
|
||||
/** Bordure subtile */
|
||||
ink600: '#2A2A3C',
|
||||
|
||||
magenta: '#FF2E88',
|
||||
/** Magenta pressé — extrémité du dégradé assombri */
|
||||
magentaPressed: '#E5216F',
|
||||
/** Teinte magenta claire (texte sur fond magenta translucide) */
|
||||
magentaTint: '#FFB4D4',
|
||||
|
||||
violet: '#7B3DFF',
|
||||
violetPressed: '#6A2FE6',
|
||||
violetTint: '#C9B6FF',
|
||||
|
||||
cyan: '#28E0F0',
|
||||
green: '#3DDC97',
|
||||
/** Ambre — seule teinte chaude du système (placeholders, alertes) */
|
||||
amber: '#FFC478',
|
||||
|
||||
white: '#F4F4FA',
|
||||
grey100: '#D6D6E6',
|
||||
grey200: '#C9C9DC',
|
||||
grey400: '#9A9AB4',
|
||||
} as const;
|
||||
|
||||
export const signatureGradient = colors.signatureGradient;
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Tokens sémantiques
|
||||
* ------------------------------------------------------------------ */
|
||||
export const colors = {
|
||||
/* --- Fonds & surfaces ------------------------------------------- */
|
||||
deepBackground: palette.ink900,
|
||||
/** Fond hors-écran / derrière les sheets */
|
||||
canvas: palette.ink950,
|
||||
surface: palette.ink800,
|
||||
surfaceElevated: palette.ink700,
|
||||
border: palette.ink600,
|
||||
/** Séparateur intra-carte, plus léger que `border` */
|
||||
divider: 'rgba(255,255,255,0.12)',
|
||||
/** Bloc squelette de chargement */
|
||||
skeleton: 'rgba(244,244,250,0.07)',
|
||||
/** Piste de barre de progression / slider */
|
||||
track: 'rgba(244,244,250,0.28)',
|
||||
|
||||
/* --- Accents ---------------------------------------------------- */
|
||||
primary: palette.magenta,
|
||||
primaryPressed: palette.magentaPressed,
|
||||
/** Fond de badge / chip actif teinté magenta */
|
||||
primarySoft: 'rgba(255,46,136,0.14)',
|
||||
/** Bordure de bouton destructif */
|
||||
primaryBorder: 'rgba(255,46,136,0.55)',
|
||||
onPrimary: '#FFFFFF',
|
||||
|
||||
secondary: palette.violet,
|
||||
secondaryPressed: palette.violetPressed,
|
||||
secondarySoft: 'rgba(123,61,255,0.14)',
|
||||
|
||||
tertiary: palette.cyan,
|
||||
/** Fond de badge teinté cyan (line-up, « en cours ») */
|
||||
tertiarySoft: 'rgba(40,224,240,0.12)',
|
||||
|
||||
/* --- États ------------------------------------------------------ */
|
||||
success: palette.green,
|
||||
successSoft: 'rgba(61,220,151,0.14)',
|
||||
successBorder: 'rgba(61,220,151,0.5)',
|
||||
/** Pas de rouge dans le système : le destructif emprunte le magenta */
|
||||
danger: palette.magenta,
|
||||
dangerSoft: 'rgba(255,46,136,0.14)',
|
||||
warning: palette.amber,
|
||||
warningSoft: 'rgba(255,196,120,0.14)',
|
||||
|
||||
/* --- Texte ------------------------------------------------------ */
|
||||
textPrimary: palette.white,
|
||||
/** Titres et libellés secondaires (le plus utilisé après textPrimary) */
|
||||
textStrong: palette.grey100,
|
||||
/** Corps de texte long — jamais du blanc pur sur fond sombre */
|
||||
textBody: palette.grey200,
|
||||
textSecondary: palette.grey400,
|
||||
textOnAccent: '#FFFFFF',
|
||||
textOnPrimarySoft: palette.magentaTint,
|
||||
textOnSecondarySoft: palette.violetTint,
|
||||
/** Texte désactivé (bouton off) — appliquer aussi opacity 0.45 */
|
||||
textDisabled: palette.grey400,
|
||||
|
||||
/* --- Voiles sur média ------------------------------------------- */
|
||||
/** Header / tab bar en blur */
|
||||
scrimHeader: 'rgba(10,10,18,0.94)',
|
||||
/** Bas d'image de couverture (B4, C3) */
|
||||
scrimHeavy: 'rgba(10,10,18,0.86)',
|
||||
scrimMedium: 'rgba(10,10,18,0.6)',
|
||||
scrimLight: 'rgba(10,10,18,0.15)',
|
||||
/** Bouton flottant sur photo */
|
||||
glassFill: 'rgba(255,255,255,0.14)',
|
||||
|
||||
/* --- Carte (B7) ------------------------------------------------- */
|
||||
mapBackground: '#0C1424',
|
||||
mapWater: '#101A2E',
|
||||
mapLand: '#0A0E1A',
|
||||
} as const;
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Dégradés — tuples pour expo-linear-gradient.
|
||||
* La maquette utilise 135deg (haut-gauche → bas-droite) : d'où
|
||||
* start {0,0} / end {1,1}.
|
||||
* ------------------------------------------------------------------ */
|
||||
export const gradientDirection = {
|
||||
start: { x: 0.2, y: 0 },
|
||||
end: { x: 0.8, y: 1 },
|
||||
} as const;
|
||||
|
||||
export const gradients = {
|
||||
/** Dégradé signature — bouton primaire, FAB, logo */
|
||||
signature: [palette.violet, palette.magenta],
|
||||
/** Même dégradé à l'état pressé */
|
||||
signaturePressed: [palette.violetPressed, palette.magentaPressed],
|
||||
/** Voile vertical sur image de couverture (start {0,0} end {0,1}) */
|
||||
coverScrim: [
|
||||
colors.scrimMedium,
|
||||
colors.scrimLight,
|
||||
'rgba(10,10,18,0.98)',
|
||||
],
|
||||
} as const satisfies Record<string, readonly ColorValue[]>;
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Glow — les box-shadow CSS n'existent pas en RN.
|
||||
* iOS lit shadow*, Android ne lit que elevation (et ignore la couleur
|
||||
* avant API 28) : le glow rose y sera un simple relief.
|
||||
* ------------------------------------------------------------------ */
|
||||
export const glow = {
|
||||
/** --gl · sous un bouton primaire / une carte magenta */
|
||||
primary: {
|
||||
shadowColor: palette.magenta,
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.55,
|
||||
shadowRadius: 25,
|
||||
elevation: 12,
|
||||
},
|
||||
/** --glv · variante violette */
|
||||
violet: {
|
||||
shadowColor: palette.violet,
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.6,
|
||||
shadowRadius: 25,
|
||||
elevation: 12,
|
||||
},
|
||||
/** Ombre neutre de carte élevée */
|
||||
card: {
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 18,
|
||||
elevation: 6,
|
||||
},
|
||||
} as const satisfies Record<string, ViewStyle>;
|
||||
|
||||
export type ColorToken = keyof typeof colors;
|
||||
|
||||
/** @deprecated utiliser `gradients.signature` */
|
||||
export const signatureGradient = gradients.signature;
|
||||
|
||||
export default colors;
|
||||
|
||||
@@ -1,67 +1,226 @@
|
||||
import { Inter_400Regular, Inter_500Medium } from '@expo-google-fonts/inter';
|
||||
import { SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk';
|
||||
import {
|
||||
Inter_400Regular,
|
||||
Inter_500Medium,
|
||||
Inter_600SemiBold,
|
||||
Inter_700Bold,
|
||||
} from '@expo-google-fonts/inter';
|
||||
import {
|
||||
JetBrainsMono_400Regular,
|
||||
JetBrainsMono_500Medium,
|
||||
JetBrainsMono_700Bold,
|
||||
} from '@expo-google-fonts/jetbrains-mono';
|
||||
import {
|
||||
SpaceGrotesk_600SemiBold,
|
||||
SpaceGrotesk_700Bold,
|
||||
} from '@expo-google-fonts/space-grotesk';
|
||||
import { useFonts } from 'expo-font';
|
||||
import type { TextStyle } from 'react-native';
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Familles
|
||||
* Maquette Bahla : Space Grotesk (display) / Inter (UI) / JetBrains Mono
|
||||
* (labels + chiffres). Le mono n'est PAS la mono système : c'est un
|
||||
* choix de marque, il doit être chargé.
|
||||
* ------------------------------------------------------------------ */
|
||||
export const fontFamily = {
|
||||
display: 'SpaceGrotesk_600SemiBold',
|
||||
displayBold: 'SpaceGrotesk_700Bold',
|
||||
body: 'Inter_400Regular',
|
||||
bodyMedium: 'Inter_500Medium',
|
||||
mono: 'monospace',
|
||||
bodySemi: 'Inter_600SemiBold',
|
||||
bodyBold: 'Inter_700Bold',
|
||||
mono: 'JetBrainsMono_400Regular',
|
||||
monoMedium: 'JetBrainsMono_500Medium',
|
||||
monoBold: 'JetBrainsMono_700Bold',
|
||||
} as const;
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Tokens
|
||||
* letterSpacing est en px sur RN (pas en em) : chaque valeur ci-dessous
|
||||
* est la conversion em × fontSize de la maquette.
|
||||
* ------------------------------------------------------------------ */
|
||||
export const typography = {
|
||||
display: {
|
||||
fontFamily: fontFamily.display,
|
||||
fontSize: 40,
|
||||
/* --- Display · Space Grotesk ------------------------------------ */
|
||||
hero: {
|
||||
fontFamily: fontFamily.displayBold,
|
||||
fontSize: 48,
|
||||
lineHeight: 48,
|
||||
letterSpacing: -0.8,
|
||||
letterSpacing: -1.44,
|
||||
},
|
||||
display: {
|
||||
fontFamily: fontFamily.displayBold,
|
||||
fontSize: 40,
|
||||
lineHeight: 42,
|
||||
letterSpacing: -1.2,
|
||||
},
|
||||
/** Grand titre d'écran (« Mes events », dashboard) */
|
||||
screenTitleLg: {
|
||||
fontFamily: fontFamily.displayBold,
|
||||
fontSize: 28,
|
||||
lineHeight: 32,
|
||||
letterSpacing: -0.56,
|
||||
},
|
||||
screenTitle: {
|
||||
fontFamily: fontFamily.displayBold,
|
||||
fontSize: 26,
|
||||
lineHeight: 32,
|
||||
letterSpacing: -0.6,
|
||||
lineHeight: 30,
|
||||
letterSpacing: -0.52,
|
||||
},
|
||||
/** Titre de bloc / nom d'artiste en tête de line-up */
|
||||
sectionTitle: {
|
||||
fontFamily: fontFamily.displayBold,
|
||||
fontSize: 20,
|
||||
lineHeight: 24,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
cardTitle: {
|
||||
fontFamily: fontFamily.display,
|
||||
fontSize: 18,
|
||||
lineHeight: 24,
|
||||
letterSpacing: -0.2,
|
||||
lineHeight: 22,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
sectionLabel: {
|
||||
fontFamily: fontFamily.mono,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0.8,
|
||||
textTransform: 'uppercase' as const,
|
||||
/** Titre de carte compacte (grille, slot de line-up) */
|
||||
cardTitleSm: {
|
||||
fontFamily: fontFamily.display,
|
||||
fontSize: 16,
|
||||
lineHeight: 20,
|
||||
letterSpacing: -0.16,
|
||||
},
|
||||
|
||||
/* --- Corps & UI · Inter ---------------------------------------- */
|
||||
body: {
|
||||
fontFamily: fontFamily.body,
|
||||
fontSize: 15,
|
||||
lineHeight: 24,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
bodySm: {
|
||||
fontFamily: fontFamily.body,
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
/** Libellé de bouton — 52 px de haut, rayon 14 px */
|
||||
button: {
|
||||
fontFamily: fontFamily.bodySemi,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
/** Nom d'organisateur, en-tête de ligne, valeur de champ */
|
||||
label: {
|
||||
fontFamily: fontFamily.bodySemi,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
metadata: {
|
||||
fontFamily: fontFamily.bodyMedium,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
/** Texte de chip / filtre */
|
||||
chip: {
|
||||
fontFamily: fontFamily.bodySemi,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
metadataSm: {
|
||||
fontFamily: fontFamily.bodyMedium,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
caption: {
|
||||
fontFamily: fontFamily.body,
|
||||
fontSize: 11,
|
||||
lineHeight: 15,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
/** Libellé de la tab bar (4 onglets) */
|
||||
tabLabel: {
|
||||
fontFamily: fontFamily.bodyMedium,
|
||||
fontSize: 10,
|
||||
lineHeight: 13,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
|
||||
/* --- Labels & chiffres · JetBrains Mono ------------------------- */
|
||||
/** Overline de rangée / d'écran sur la planche de style */
|
||||
overlineLg: {
|
||||
fontFamily: fontFamily.monoMedium,
|
||||
fontSize: 13,
|
||||
lineHeight: 17,
|
||||
letterSpacing: 1.82,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
/** Libellé de section dans l'app */
|
||||
sectionLabel: {
|
||||
fontFamily: fontFamily.monoMedium,
|
||||
fontSize: 11,
|
||||
lineHeight: 14,
|
||||
letterSpacing: 1.76,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
/** Overline resserré (cartouches, en-têtes de colonne) */
|
||||
overlineSm: {
|
||||
fontFamily: fontFamily.monoMedium,
|
||||
fontSize: 10,
|
||||
lineHeight: 13,
|
||||
letterSpacing: 1.8,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
/** Badge / pastille (« COMPLET », « CE SOIR ») */
|
||||
badge: {
|
||||
fontFamily: fontFamily.monoMedium,
|
||||
fontSize: 9,
|
||||
lineHeight: 12,
|
||||
letterSpacing: 1.62,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
/** Chiffres, horaires, prix, distances — 22:30 · 25 CHF · 18 km */
|
||||
digits: {
|
||||
fontFamily: fontFamily.mono,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0.4,
|
||||
lineHeight: 14,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
} as const;
|
||||
digitsSm: {
|
||||
fontFamily: fontFamily.mono,
|
||||
fontSize: 10,
|
||||
lineHeight: 13,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
/** Grand chiffre de statistique (dashboard organisateur) */
|
||||
statValue: {
|
||||
fontFamily: fontFamily.displayBold,
|
||||
fontSize: 24,
|
||||
lineHeight: 26,
|
||||
letterSpacing: -0.48,
|
||||
},
|
||||
} as const satisfies Record<string, TextStyle>;
|
||||
|
||||
export const loadFonts = () =>
|
||||
export type TypographyToken = keyof typeof typography;
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Chargement — hook, donc préfixe `use` (règle react-hooks).
|
||||
* ------------------------------------------------------------------ */
|
||||
export const useAppFonts = () =>
|
||||
useFonts({
|
||||
SpaceGrotesk_600SemiBold,
|
||||
SpaceGrotesk_700Bold,
|
||||
Inter_400Regular,
|
||||
Inter_500Medium,
|
||||
Inter_600SemiBold,
|
||||
Inter_700Bold,
|
||||
JetBrainsMono_400Regular,
|
||||
JetBrainsMono_500Medium,
|
||||
JetBrainsMono_700Bold,
|
||||
});
|
||||
|
||||
/** @deprecated conservé pour compat — utiliser `useAppFonts` */
|
||||
export const loadFonts = useAppFonts;
|
||||
|
||||
export default typography;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import colors from './colors';
|
||||
import { colors, glow, gradientDirection, gradients, palette } from './colors';
|
||||
import spacing, { borderRadius } from './spacing';
|
||||
import typography from './typography';
|
||||
|
||||
export type ThemeContextValue = {
|
||||
colors: typeof colors;
|
||||
gradients: typeof gradients;
|
||||
glow: typeof glow;
|
||||
gradientDirection: typeof gradientDirection;
|
||||
palette: typeof palette;
|
||||
typography: typeof typography;
|
||||
spacing: typeof spacing;
|
||||
borderRadius: typeof borderRadius;
|
||||
|
||||
Reference in New Issue
Block a user