feat(mobile): custom fonts (General Sans + Geist Mono) + logo rework
FONTY:
- Dodane assets/fonts/: GeneralSans Regular/Medium/Semibold (Fontshare, free
commercial) + GeistMono Regular (Vercel OFL). Pobrane jako .ttf.
- expo-font ~13.0.4 (matchuje SDK 52). Native module jest w APK bo `expo`
ciagnie expo-font jako bezposrednia zaleznosc -> useFonts dziala przez OTA
bez rebuildu.
- App.tsx: useFonts() gate (blokuje render do zaladowania, .ttf z bundla <100ms)
+ globalny Text.defaultProps fontFamily=GeneralSans-Regular dla body.
- theme.ts: fonts = { body, medium, display, mono }. RN nie syntezuje weightow
dla custom fontow, wiec 4 osobne rodziny per-weight (gotcha udokumentowany).
- Jawne fonty na high-traffic: SceneTile (title=display, meta+dur=mono),
MoviePosterCard (j.w.), navigation (taby display/medium, header display).
LOGO:
- GoonWordmark przepisany: zamiast krzywych recznych SVG path (o-ka jako
nachodzace elipsy, zniekształcone n) renderuje PRAWDZIWY tekst w General Sans
Semibold. Dwutonowy twist: "g[oo]n" ze srodkowym "oo" w oxblood.
- GoonMark (monogram): czysty SVG koncentryczny ring + dot (oxblood) — motyw
soczewki/oka. Dla app-icon/splash gdzie font niedostepny.
- Wpiety na AgeGate (wordmark 40), Login (mark 44 + wordmark 44), nav header.
OTA: c986c911-0868-44f7-9f4a-fc2a74e53095 live (23 assets, 4 fonty serwuja 200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a0481060f3
commit
d87263dde9
15 changed files with 140 additions and 159 deletions
|
|
@ -6,6 +6,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import * as Sentry from '@sentry/react-native';
|
import * as Sentry from '@sentry/react-native';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import { registerRootComponent } from 'expo';
|
import { registerRootComponent } from 'expo';
|
||||||
|
import { useFonts } from 'expo-font';
|
||||||
|
import { Text as RNText } from 'react-native';
|
||||||
import * as ScreenCapture from 'expo-screen-capture';
|
import * as ScreenCapture from 'expo-screen-capture';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import * as Updates from 'expo-updates';
|
import * as Updates from 'expo-updates';
|
||||||
|
|
@ -75,7 +77,30 @@ const queryClient = new QueryClient({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Globalny default fontu dla całego <Text> — General Sans Regular jako body.
|
||||||
|
// Komponenty które chcą display/mono nadpisują fontFamily jawnie (bo RN nie
|
||||||
|
// syntezuje weightów dla custom fontów). Bold-ale-bez-fontFamily tekst zostanie
|
||||||
|
// Regular weightem General Sans — wciąż distinctive face, akceptowalne dla
|
||||||
|
// nietkniętych ekranów; high-traffic komponenty mają jawny Semibold.
|
||||||
|
let _textDefaultApplied = false;
|
||||||
|
function applyDefaultFont() {
|
||||||
|
if (_textDefaultApplied) return;
|
||||||
|
_textDefaultApplied = true;
|
||||||
|
const T = RNText as unknown as { defaultProps?: { style?: unknown } };
|
||||||
|
T.defaultProps = T.defaultProps || {};
|
||||||
|
const prev = T.defaultProps.style;
|
||||||
|
T.defaultProps.style = [{ fontFamily: 'GeneralSans-Regular' }, prev].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [fontsLoaded] = useFonts({
|
||||||
|
'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'),
|
||||||
|
'GeneralSans-Medium': require('./assets/fonts/GeneralSans-Medium.ttf'),
|
||||||
|
'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'),
|
||||||
|
'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'),
|
||||||
|
});
|
||||||
|
if (fontsLoaded) applyDefaultFont();
|
||||||
|
|
||||||
const [hydrated, setHydrated] = useState(false);
|
const [hydrated, setHydrated] = useState(false);
|
||||||
const [ageAccepted, setAgeAccepted] = useState(false);
|
const [ageAccepted, setAgeAccepted] = useState(false);
|
||||||
const [client, setClient] = useState<GoonClient | null>(null);
|
const [client, setClient] = useState<GoonClient | null>(null);
|
||||||
|
|
@ -245,7 +270,7 @@ export default function App() {
|
||||||
return () => sub.remove();
|
return () => sub.remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!hydrated || !lockReady) {
|
if (!fontsLoaded || !hydrated || !lockReady) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: theme.bg, justifyContent: 'center' }}>
|
<View style={{ flex: 1, backgroundColor: theme.bg, justifyContent: 'center' }}>
|
||||||
<ActivityIndicator color={theme.fg} />
|
<ActivityIndicator color={theme.fg} />
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,12 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-video",
|
"expo-video",
|
||||||
"@sentry/react-native/expo"
|
"@sentry/react-native/expo",
|
||||||
|
"expo-font"
|
||||||
],
|
],
|
||||||
"extra": {
|
"extra": {
|
||||||
"sentryDsn": "",
|
"sentryDsn": "",
|
||||||
"sentryEnvironment": "production"
|
"sentryEnvironment": "production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
mobile/assets/fonts/GeistMono-Regular.ttf
Normal file
BIN
mobile/assets/fonts/GeistMono-Regular.ttf
Normal file
Binary file not shown.
BIN
mobile/assets/fonts/GeneralSans-Medium.ttf
Normal file
BIN
mobile/assets/fonts/GeneralSans-Medium.ttf
Normal file
Binary file not shown.
BIN
mobile/assets/fonts/GeneralSans-Regular.ttf
Normal file
BIN
mobile/assets/fonts/GeneralSans-Regular.ttf
Normal file
Binary file not shown.
BIN
mobile/assets/fonts/GeneralSans-Semibold.ttf
Normal file
BIN
mobile/assets/fonts/GeneralSans-Semibold.ttf
Normal file
Binary file not shown.
27
mobile/package-lock.json
generated
27
mobile/package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-build-properties": "~0.13.3",
|
"expo-build-properties": "~0.13.3",
|
||||||
"expo-clipboard": "~7.0.1",
|
"expo-clipboard": "~7.0.1",
|
||||||
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-intent-launcher": "~12.0.2",
|
"expo-intent-launcher": "~12.0.2",
|
||||||
|
|
@ -6246,6 +6247,19 @@
|
||||||
"integrity": "sha512-t+1F1tiDocSot8iSnrn/CjTUMvVvPV2DpafSVcticpbSzMGybEN7wcamO1t18fK7WxGXpZE9gxtd80qwv/LLqQ==",
|
"integrity": "sha512-t+1F1tiDocSot8iSnrn/CjTUMvVvPV2DpafSVcticpbSzMGybEN7wcamO1t18fK7WxGXpZE9gxtd80qwv/LLqQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-font": {
|
||||||
|
"version": "13.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
||||||
|
"integrity": "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fontfaceobserver": "^2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-haptics": {
|
"node_modules/expo-haptics": {
|
||||||
"version": "14.0.1",
|
"version": "14.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.0.1.tgz",
|
||||||
|
|
@ -6500,19 +6514,6 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/expo-font": {
|
|
||||||
"version": "13.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
|
||||||
"integrity": "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fontfaceobserver": "^2.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo/node_modules/expo-keep-awake": {
|
"node_modules/expo/node_modules/expo-keep-awake": {
|
||||||
"version": "14.0.3",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-build-properties": "~0.13.3",
|
"expo-build-properties": "~0.13.3",
|
||||||
"expo-clipboard": "~7.0.1",
|
"expo-clipboard": "~7.0.1",
|
||||||
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-intent-launcher": "~12.0.2",
|
"expo-intent-launcher": "~12.0.2",
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,76 @@
|
||||||
/**
|
/**
|
||||||
* Goon wordmark — custom letterform, NOT a typed text in a font.
|
* Goon brand marks.
|
||||||
*
|
*
|
||||||
* 4 litery jako path geometry. Detale:
|
* Rework 2026-05-30: poprzednia wersja rysowała litery ręcznie jako SVG path
|
||||||
* - "g" otwarte u dołu (descender curve cut), ucho descender przedłużone
|
* geometry — wychodziło krzywo (o-ka jako nachodzące elipsy, zniekształcone n).
|
||||||
* - "o" — okrąg z lekkim spłaszczeniem w pionie (1.0×0.95 aspect), counter
|
* Teraz mamy General Sans Semibold jako realny font (useFonts w App.tsx), więc
|
||||||
* z 60% wysokości (cięższe niż typical grotesk → daje "weight")
|
* wordmark renderuje PRAWDZIWY tekst w tej rodzinie — czysto i spójnie z resztą
|
||||||
* - "n" — minimal grotesk, stem łączy się z arc bez serif joint
|
* typografii.
|
||||||
* - Kerning naturalny, ale "oo" lekko ciaśniejsze niż "go"/"on"
|
|
||||||
*
|
*
|
||||||
* Viewbox 200×56 — 4 litery × ~44px width, używamy aspect ratio scale.
|
* Distinctive twist: dwutonowe "g[oo]n" — środkowe "oo" w oxblood (brand accent),
|
||||||
|
* "g"+"n" w foreground. Czytelne nawet w małym headerze, wiąże logo z paletą.
|
||||||
*
|
*
|
||||||
* Użycie:
|
* <GoonWordmark size={26} /> // header
|
||||||
* <GoonWordmark width={120} color={theme.fg} />
|
* <GoonWordmark size={48} mono /> // splash, jednolity kolor
|
||||||
* <GoonWordmark width={80} color={theme.accent} /> // dla splash
|
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Svg, { Path } from 'react-native-svg';
|
import { Text, View } from 'react-native';
|
||||||
|
import Svg, { Circle } from 'react-native-svg';
|
||||||
|
|
||||||
interface Props {
|
import { fonts, theme } from '../theme';
|
||||||
width?: number;
|
|
||||||
|
interface WordmarkProps {
|
||||||
|
/** fontSize wordmarku w px. */
|
||||||
|
size?: number;
|
||||||
|
/** Kolor liter g+n (oo zawsze accent, chyba że `mono`). */
|
||||||
color?: string;
|
color?: string;
|
||||||
|
/** Jednolity kolor (bez dwutonu) — np. na splash gdzie tło = accent. */
|
||||||
|
mono?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GoonWordmark({ width = 120, color = '#F5EDE0' }: Props) {
|
export function GoonWordmark({ size = 26, color = theme.fg, mono = false }: WordmarkProps) {
|
||||||
const height = width * (56 / 200);
|
const base = {
|
||||||
|
fontFamily: fonts.display,
|
||||||
|
fontSize: size,
|
||||||
|
letterSpacing: -size * 0.03,
|
||||||
|
includeFontPadding: false as const,
|
||||||
|
};
|
||||||
|
const accent = mono ? color : theme.accent;
|
||||||
return (
|
return (
|
||||||
<Svg width={width} height={height} viewBox="0 0 200 56" fill="none">
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
{/*
|
<Text style={[base, { color }]}>g</Text>
|
||||||
g — descender extends below baseline. Counter = 12 radius hole.
|
<Text style={[base, { color: accent }]}>oo</Text>
|
||||||
Path: outer ellipse, inner counter (evenodd), descender hook from
|
<Text style={[base, { color }]}>n</Text>
|
||||||
bottom-right going down-left.
|
</View>
|
||||||
*/}
|
|
||||||
<Path
|
|
||||||
d="M 28 14
|
|
||||||
A 14 13 0 1 1 28 40
|
|
||||||
A 14 13 0 1 1 28 14
|
|
||||||
Z
|
|
||||||
M 28 22
|
|
||||||
A 6 5 0 1 0 28 32
|
|
||||||
A 6 5 0 1 0 28 22
|
|
||||||
Z
|
|
||||||
M 38 28
|
|
||||||
L 38 48
|
|
||||||
A 12 8 0 0 1 18 50"
|
|
||||||
fill={color}
|
|
||||||
stroke={color}
|
|
||||||
strokeWidth="0.5"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
{/*
|
|
||||||
o — slightly flat ellipse, heavy counter.
|
|
||||||
*/}
|
|
||||||
<Path
|
|
||||||
d="M 76 14
|
|
||||||
A 14 13 0 1 1 76 40
|
|
||||||
A 14 13 0 1 1 76 14
|
|
||||||
Z
|
|
||||||
M 76 22
|
|
||||||
A 6 5 0 1 0 76 32
|
|
||||||
A 6 5 0 1 0 76 22
|
|
||||||
Z"
|
|
||||||
fill={color}
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
{/* o #2 — tight pair z poprzednim o (kerning -2px) */}
|
|
||||||
<Path
|
|
||||||
d="M 118 14
|
|
||||||
A 14 13 0 1 1 118 40
|
|
||||||
A 14 13 0 1 1 118 14
|
|
||||||
Z
|
|
||||||
M 118 22
|
|
||||||
A 6 5 0 1 0 118 32
|
|
||||||
A 6 5 0 1 0 118 22
|
|
||||||
Z"
|
|
||||||
fill={color}
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
{/*
|
|
||||||
n — grotesk z arc bez serif. Lewa stem prosty, prawa wychodzi z arc.
|
|
||||||
*/}
|
|
||||||
<Path
|
|
||||||
d="M 144 16
|
|
||||||
L 144 40
|
|
||||||
L 150 40
|
|
||||||
L 150 26
|
|
||||||
A 8 10 0 0 1 166 26
|
|
||||||
L 166 40
|
|
||||||
L 172 40
|
|
||||||
L 172 24
|
|
||||||
A 12 14 0 0 0 150 22
|
|
||||||
L 150 16
|
|
||||||
Z"
|
|
||||||
fill={color}
|
|
||||||
/>
|
|
||||||
</Svg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monogram — tylko "g" + ucho descender, jako standalone icon.
|
* Monogram — koncentryczny ring + wypełniona kropka (oxblood). Czyta się jako
|
||||||
* 56×56, używany jako adaptive-icon / splash mark.
|
* "o" z wordmarku, motyw soczewki/oka (watching) bez dosłowności. Czysty SVG
|
||||||
|
* (bez fontu) — używany do generowania app-icon / adaptive-icon / splash PNG,
|
||||||
|
* gdzie custom font nie jest dostępny.
|
||||||
|
*
|
||||||
|
* <GoonMark size={48} />
|
||||||
*/
|
*/
|
||||||
export function GoonMark({ size = 48, color = '#F5EDE0' }: { size?: number; color?: string }) {
|
export function GoonMark({
|
||||||
|
size = 48,
|
||||||
|
ringColor = theme.fg,
|
||||||
|
dotColor = theme.accent,
|
||||||
|
bg,
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
ringColor?: string;
|
||||||
|
dotColor?: string;
|
||||||
|
/** Opcjonalne tło (dla icon — np. theme.bg). Pominięte = przezroczyste. */
|
||||||
|
bg?: string;
|
||||||
|
}) {
|
||||||
|
// viewBox 100×100. Ring: cx50 cy50 r36, stroke 11. Dot: r15 wypełniony.
|
||||||
return (
|
return (
|
||||||
<Svg width={size} height={size} viewBox="0 0 56 56" fill="none">
|
<Svg width={size} height={size} viewBox="0 0 100 100" fill="none">
|
||||||
<Path
|
{bg ? <Circle cx={50} cy={50} r={50} fill={bg} /> : null}
|
||||||
d="M 28 10
|
<Circle cx={50} cy={50} r={36} stroke={ringColor} strokeWidth={11} fill="none" />
|
||||||
A 14 13 0 1 1 28 36
|
<Circle cx={50} cy={50} r={15} fill={dotColor} />
|
||||||
A 14 13 0 1 1 28 10
|
|
||||||
Z
|
|
||||||
M 28 18
|
|
||||||
A 6 5 0 1 0 28 28
|
|
||||||
A 6 5 0 1 0 28 18
|
|
||||||
Z
|
|
||||||
M 38 24
|
|
||||||
L 38 44
|
|
||||||
A 12 8 0 0 1 18 46"
|
|
||||||
fill={color}
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
import { theme } from '../theme';
|
import { fonts, theme } from '../theme';
|
||||||
import type { MovieOut } from '../types';
|
import type { MovieOut } from '../types';
|
||||||
|
|
||||||
export function MoviePosterCard({
|
export function MoviePosterCard({
|
||||||
|
|
@ -101,7 +101,7 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
},
|
},
|
||||||
newBadgeText: { color: theme.fg, fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
|
newBadgeText: { color: theme.fg, fontSize: 9, fontFamily: fonts.mono, fontWeight: '700', letterSpacing: 0.6 },
|
||||||
posterDimmed: { opacity: 0.45 },
|
posterDimmed: { opacity: 0.45 },
|
||||||
watchedBadge: {
|
watchedBadge: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -124,7 +124,7 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
},
|
},
|
||||||
progressFg: { height: 3, backgroundColor: theme.accent },
|
progressFg: { height: 3, backgroundColor: theme.accent },
|
||||||
title: { color: theme.fg, fontSize: 13, fontWeight: '600', marginTop: 6 },
|
title: { color: theme.fg, fontSize: 13, fontFamily: fonts.display, marginTop: 6, letterSpacing: -0.2 },
|
||||||
titleDimmed: { color: theme.muted },
|
titleDimmed: { color: theme.muted },
|
||||||
meta: { color: theme.muted, fontSize: 11, marginTop: 2 },
|
meta: { color: theme.muted, fontSize: 10, fontFamily: fonts.mono, marginTop: 2, letterSpacing: 0.5, textTransform: 'uppercase' },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import React, { useState } from 'react';
|
||||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
|
|
||||||
import type { RootStackParamList } from '../navigation';
|
import type { RootStackParamList } from '../navigation';
|
||||||
import { theme } from '../theme';
|
import { fonts, theme } from '../theme';
|
||||||
import type { SceneOut } from '../types';
|
import type { SceneOut } from '../types';
|
||||||
import { Thumb } from './Thumb';
|
import { Thumb } from './Thumb';
|
||||||
|
|
||||||
|
|
@ -190,22 +190,23 @@ const styles = StyleSheet.create({
|
||||||
durText: {
|
durText: {
|
||||||
color: theme.fg,
|
color: theme.fg,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: '600',
|
fontFamily: fonts.mono,
|
||||||
fontVariant: ['tabular-nums'],
|
fontVariant: ['tabular-nums'],
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: theme.fg,
|
color: theme.fg,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontFamily: fonts.display,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: -0.2,
|
||||||
},
|
},
|
||||||
titleDim: { color: theme.muted },
|
titleDim: { color: theme.muted },
|
||||||
meta: {
|
meta: {
|
||||||
color: theme.muted,
|
color: theme.muted,
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
marginTop: 2,
|
fontFamily: fonts.mono,
|
||||||
letterSpacing: 0.3,
|
marginTop: 3,
|
||||||
|
letterSpacing: 0.5,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { SitesScreen } from './screens/SitesScreen';
|
||||||
import { StudioScenesScreen } from './screens/StudioScenesScreen';
|
import { StudioScenesScreen } from './screens/StudioScenesScreen';
|
||||||
import { TagScenesScreen } from './screens/TagScenesScreen';
|
import { TagScenesScreen } from './screens/TagScenesScreen';
|
||||||
import { TagsScreen } from './screens/TagsScreen';
|
import { TagsScreen } from './screens/TagsScreen';
|
||||||
import { theme } from './theme';
|
import { fonts, theme } from './theme';
|
||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Scenes: undefined;
|
Scenes: undefined;
|
||||||
|
|
@ -90,8 +90,8 @@ function TopTabs({
|
||||||
const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites'];
|
const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites'];
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row', gap: 16, paddingHorizontal: 12, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 16, paddingHorizontal: 12, alignItems: 'center' }}>
|
||||||
{/* Wordmark — branding po lewej, dystans 14px do pierwszego tabu. */}
|
{/* Wordmark — branding po lewej, dystans do pierwszego tabu. */}
|
||||||
<GoonWordmark width={64} color={theme.fg} />
|
<GoonWordmark size={24} />
|
||||||
<View style={{ width: 1, height: 18, backgroundColor: theme.border, marginRight: 2 }} />
|
<View style={{ width: 1, height: 18, backgroundColor: theme.border, marginRight: 2 }} />
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => {
|
||||||
const active = t === current;
|
const active = t === current;
|
||||||
|
|
@ -101,7 +101,7 @@ function TopTabs({
|
||||||
style={{
|
style={{
|
||||||
color: active ? theme.accent : theme.muted,
|
color: active ? theme.accent : theme.muted,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: active ? '700' : '500',
|
fontFamily: active ? fonts.display : fonts.medium,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -145,7 +145,7 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
|
||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerStyle: { backgroundColor: theme.card },
|
headerStyle: { backgroundColor: theme.card },
|
||||||
headerTitleStyle: { color: theme.fg },
|
headerTitleStyle: { color: theme.fg, fontFamily: fonts.display },
|
||||||
headerTintColor: theme.accent,
|
headerTintColor: theme.accent,
|
||||||
contentStyle: { backgroundColor: theme.bg },
|
contentStyle: { backgroundColor: theme.bg },
|
||||||
}}
|
}}
|
||||||
|
|
@ -248,7 +248,7 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AppLockSettings"
|
name="AppLockSettings"
|
||||||
options={{ title: 'Blokada aplikacji' }}
|
options={{ title: 'App lock' }}
|
||||||
>
|
>
|
||||||
{() => <AppLockSettingsScreen />}
|
{() => <AppLockSettingsScreen />}
|
||||||
</Stack.Screen>
|
</Stack.Screen>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { GoonWordmark } from '../components/GoonWordmark';
|
||||||
import { markAccepted } from '../lib/agegate';
|
import { markAccepted } from '../lib/agegate';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ export function AgeGateScreen({ onAccept }: Props) {
|
||||||
contentContainerStyle={styles.scroll}
|
contentContainerStyle={styles.scroll}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<Text style={styles.title}>goon</Text>
|
<GoonWordmark size={40} />
|
||||||
<Text style={styles.subtitle}>Adult content · Self-hosted only</Text>
|
<Text style={styles.subtitle}>Adult content · Self-hosted only</Text>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { GoonClient } from '../api';
|
import { GoonClient } from '../api';
|
||||||
|
import { GoonMark, GoonWordmark } from '../components/GoonWordmark';
|
||||||
import { saveCredentials } from '../storage';
|
import { saveCredentials } from '../storage';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
|
||||||
|
|
@ -57,8 +58,9 @@ export function LoginScreen({ onAuthenticated }: Props) {
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<View style={styles.brandBlock}>
|
<View style={styles.brandBlock}>
|
||||||
<View style={styles.brandDot} />
|
<GoonMark size={44} bg="transparent" />
|
||||||
<Text style={styles.title}>goon</Text>
|
<View style={{ height: 14 }} />
|
||||||
|
<GoonWordmark size={44} />
|
||||||
<Text style={styles.subtitle}>self-hosted scene catalog</Text>
|
<Text style={styles.subtitle}>self-hosted scene catalog</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,29 +41,26 @@ export const theme = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font family — pusty string = system default na iOS/Android (uniknięcie
|
* Font family — General Sans (display + body, Fontshare) + Geist Mono (meta,
|
||||||
* "font not found" warningów). Po zainstalowaniu custom fontów (TODO niżej):
|
* Vercel OFL). Pliki .ttf w `mobile/assets/fonts/`, ładowane przez useFonts()
|
||||||
* 1. `npx expo install expo-font expo-splash-screen`
|
* w App.tsx (runtime load — działa przez OTA bo expo-font native jest w APK).
|
||||||
* 2. Pobierz fonty:
|
|
||||||
* - General Sans (Fontshare, free): https://www.fontshare.com/fonts/general-sans
|
|
||||||
* - Geist Mono (Vercel, free OFL): https://github.com/vercel/geist-font
|
|
||||||
* 3. Skopiuj .ttf do `mobile/assets/fonts/`
|
|
||||||
* 4. W App.tsx:
|
|
||||||
* const [loaded] = useFonts({
|
|
||||||
* 'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'),
|
|
||||||
* 'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'),
|
|
||||||
* 'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'),
|
|
||||||
* });
|
|
||||||
* if (!loaded) return null; // lub SplashScreen.preventAutoHideAsync()
|
|
||||||
* 5. Zmień stałe poniżej na konkretne fontFamily strings.
|
|
||||||
*
|
*
|
||||||
* Do tego momentu: distinct hierarchy przez `type` scale + fontWeight w
|
* WAŻNE (custom-font gotcha): RN NIE syntezuje weightów dla custom fontów —
|
||||||
* komponentach. System font (San Francisco / Roboto) jest OK temporary.
|
* fontWeight:'700' na fontFamily:'GeneralSans-Regular' NIE pogrubi. Trzeba
|
||||||
|
* jawnie wskazać rodzinę per-weight. Stąd 4 osobne sloty:
|
||||||
|
* - body → Regular (400) — domyślny tekst
|
||||||
|
* - medium → Medium (500) — labels, tab inactive
|
||||||
|
* - display → Semibold (600)— headingi, tytuły, bold
|
||||||
|
* - mono → Geist Mono — meta, duration, liczby, kategorie
|
||||||
|
*
|
||||||
|
* Gdy useFonts jeszcze nie ready, App.tsx blokuje render (fonty z bundla
|
||||||
|
* ładują się <100ms), więc te stałe są zawsze valid przy pierwszym paint.
|
||||||
*/
|
*/
|
||||||
export const fonts = {
|
export const fonts = {
|
||||||
display: undefined as string | undefined,
|
body: 'GeneralSans-Regular',
|
||||||
displayRegular: undefined as string | undefined,
|
medium: 'GeneralSans-Medium',
|
||||||
mono: undefined as string | undefined,
|
display: 'GeneralSans-Semibold',
|
||||||
|
mono: 'GeistMono-Regular',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function scoreColor(score: number): string {
|
export function scoreColor(score: number): string {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue