From dce738b8ab001927b89661a629578bcc68451782 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Mon, 7 Sep 2026 11:23:56 +0200 Subject: [PATCH] chore(mobile): usun gorny pasek zakladek, zastapiony dolnym --- mobile/src/components/TopTabsBar.tsx | 137 --------------------------- 1 file changed, 137 deletions(-) delete mode 100644 mobile/src/components/TopTabsBar.tsx diff --git a/mobile/src/components/TopTabsBar.tsx b/mobile/src/components/TopTabsBar.tsx deleted file mode 100644 index 7b50899..0000000 --- a/mobile/src/components/TopTabsBar.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Górny pasek zakładek: aktywną wskazuje przesuwane podkreślenie, wzorzec z X/YouTube/ - * Reddit i większości Androida. - * - * Zastąpił „ciekłą kroplę" (SVG morph, 2026-08-14). Kropla była trzema błędami naraz - * (zgłoszenie 2026-08-18): rysunek miał 44 px i wystawał ponad pasek, więc nagłówek - * spuchł i rozjechało się wyrównanie wiersza; morfowanie kształtu nie wyglądało ładniej - * ani jak wzór; a cała technika (przeliczanie ścieżki SVG w JS, ~12 re-renderów na - * przejście) była droga za nic. Tu wskaźnik to jeden mały PRZESUWANY, nie - * morfowany. - * - * Zasady zerżnięte z liderów rynku, bo tam są sprawdzone: - * - STAŁA WYSOKOŚĆ: pasek jest wysoki jak wiersz tekstu, nie rozpycha nagłówka. To - * wprost naprawia wyrównanie, którego zabrakło przy kropli. - * - PRZESUWANIE, nie morfowanie: podkreślenie jedzie w poziomie (translateX, natywny - * sterownik), szerokość dopasowana do tekstu aktywnej zakładki. - * - SZYBKO I DYSKRETNIE: 220 ms ease-out. Ruch jest tłem, nie gwiazdą. - * - Robi to KOLOR: aktywna = accentText + kreska, reszta = wyszarzona. - * - * Pozycje mierzymy `onLayout` (szerokości etykiet różnią się: „JAV" vs „Scenes"), więc - * kreska huguje tekst, a nie całą komórkę. Do pierwszego pomiaru kreski nie rysujemy - * (zero mignięcia z lewej). - */ -import React from 'react'; -import { Animated, Easing, LayoutChangeEvent, Pressable, StyleSheet, Text, View } from 'react-native'; - -import { fonts, theme } from '../theme'; - -interface Props { - tabs: readonly T[]; - current: T; - onSelect: (tab: T) => void; -} - -export function TopTabsBar({ tabs, current, onSelect }: Props) { - const activeIndex = Math.max(0, tabs.indexOf(current)); - - // Pozycja komórki (x, szerokość) i szerokość samej etykiety, per zakładka. Kreska - // huguje etykietę: left = x komórki + wyśrodkowanie, width = szerokość etykiety. - const [tabLayout, setTabLayout] = React.useState>({}); - const [labelW, setLabelW] = React.useState>({}); - const ready = tabs.every((_, i) => tabLayout[i] && labelW[i] != null); - - // Jeden driver = float indeks aktywnej. Bez native drivera, bo animujemy też `width` - // (layout prop), a to i tak jeden 2 px pasek, nie 12 ścieżek SVG jak w kropli. - const anim = React.useRef(new Animated.Value(activeIndex)).current; - React.useEffect(() => { - Animated.timing(anim, { - toValue: activeIndex, - duration: 220, - easing: Easing.out(Easing.cubic), - useNativeDriver: false, - }).start(); - }, [activeIndex, anim]); - - const onTabLayout = (i: number) => (e: LayoutChangeEvent) => { - const { x, width } = e.nativeEvent.layout; - setTabLayout((p) => (p[i] && p[i].x === x && p[i].w === width ? p : { ...p, [i]: { x, w: width } })); - }; - const onLabelLayout = (i: number) => (e: LayoutChangeEvent) => { - const w = e.nativeEvent.layout.width; - setLabelW((p) => (p[i] === w ? p : { ...p, [i]: w })); - }; - - const idxRange = tabs.map((_, i) => i); - // interpolate wymaga >=2 punktów wejścia; przy 1 zakładce kreska stoi statycznie. - const canInterp = ready && idxRange.length >= 2; - // Liczone TYLKO gdy zmierzone: przed pomiarem tabLayout[i] jest undefined i mapowanie - // by się wywaliło (crash 2026-08-18 „Cannot read property 'x' of undefined"). - const leftOut = ready ? tabs.map((_, i) => tabLayout[i].x + (tabLayout[i].w - labelW[i]) / 2) : []; - const widthOut = ready ? tabs.map((_, i) => labelW[i]) : []; - const translateX = canInterp - ? anim.interpolate({ inputRange: idxRange, outputRange: leftOut }) - : ready - ? leftOut[0] - : 0; - const underlineW = canInterp - ? anim.interpolate({ inputRange: idxRange, outputRange: widthOut }) - : ready - ? widthOut[0] - : 0; - - return ( - - - {tabs.map((t, i) => { - const active = i === activeIndex; - return ( - (active ? null : onSelect(t))} - > - - {t} - - - ); - })} - - {ready ? ( - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - wrap: { justifyContent: 'center' }, - row: { flexDirection: 'row', alignItems: 'center' }, - tab: { flex: 1, alignItems: 'center', paddingVertical: 4 }, - label: { - color: theme.muted, - fontSize: 13, - fontFamily: fonts.medium, - letterSpacing: 0.3, - }, - labelActive: { color: theme.accentText, fontFamily: fonts.display }, - underline: { - position: 'absolute', - bottom: 0, - left: 0, - height: 2, - borderRadius: 1, - backgroundColor: theme.accentText, - }, -});