chore(mobile): usun gorny pasek zakladek, zastapiony dolnym
This commit is contained in:
parent
b847b300bf
commit
dce738b8ab
1 changed files with 0 additions and 137 deletions
|
|
@ -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 <View> 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<T extends string> {
|
|
||||||
tabs: readonly T[];
|
|
||||||
current: T;
|
|
||||||
onSelect: (tab: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TopTabsBar<T extends string>({ tabs, current, onSelect }: Props<T>) {
|
|
||||||
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<Record<number, { x: number; w: number }>>({});
|
|
||||||
const [labelW, setLabelW] = React.useState<Record<number, number>>({});
|
|
||||||
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 (
|
|
||||||
<View style={styles.wrap}>
|
|
||||||
<View style={styles.row}>
|
|
||||||
{tabs.map((t, i) => {
|
|
||||||
const active = i === activeIndex;
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={t}
|
|
||||||
style={styles.tab}
|
|
||||||
hitSlop={8}
|
|
||||||
accessibilityRole="tab"
|
|
||||||
accessibilityState={{ selected: active }}
|
|
||||||
onLayout={onTabLayout(i)}
|
|
||||||
onPress={() => (active ? null : onSelect(t))}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
onLayout={onLabelLayout(i)}
|
|
||||||
style={[styles.label, active ? styles.labelActive : null]}
|
|
||||||
>
|
|
||||||
{t}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
{ready ? (
|
|
||||||
<Animated.View
|
|
||||||
style={[styles.underline, { width: underlineW, transform: [{ translateX }] }]}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Loading…
Add table
Reference in a new issue