web style

This commit is contained in:
2025-10-01 14:07:13 +02:00
parent 7ee47a1943
commit 2908afc259
4 changed files with 222 additions and 131 deletions
+130 -46
View File
@@ -1,16 +1,17 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import React, { useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { FlatList, Platform, SectionList, Text, View } from "react-native"; import { FlatList, Platform, SectionList, Text, View } from "react-native";
import useLayoutType from "../../hooks/useLayoutType";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { LinearGradient } from "../LinearGradient/LinearGradient";
const BLUE_SELECTION_GRADIENT = ["#4673F9", "#7023F7"];
const buildItemKey = (value, category) =>
category ? `${category}::${value}` : `${value}`;
// Reusable selection list supporting single, multiple and sectioned data.
// Variants supported:
// - "simple": options are strings
// - "titleDescription": options are { title, description }
// - "emotion": options are { title, description, color } (uses gradient colors)
// - "sectioned": options are sections [{ title, data: [] }] (single per section)
const ListSelection = ({ const ListSelection = ({
options = [], options = [],
variant = "simple", variant = "simple",
@@ -27,8 +28,9 @@ const ListSelection = ({
itemOuterStyle, itemOuterStyle,
itemTextStyle, itemTextStyle,
highlightColor = "#F94697", highlightColor = "#F94697",
disableHover = false,
}) => { }) => {
// Internal fallback state when not provided by parent // état interne si non contrôlé
const [internalSelected, setInternalSelected] = useState( const [internalSelected, setInternalSelected] = useState(
variant === "sectioned" ? {} : multiple ? [] : null variant === "sectioned" ? {} : multiple ? [] : null
); );
@@ -36,10 +38,18 @@ const ListSelection = ({
const setSelected = const setSelected =
setSelectedProp !== undefined ? setSelectedProp : setInternalSelected; setSelectedProp !== undefined ? setSelectedProp : setInternalSelected;
const { isWeb } = useLayoutType();
const [hoveredKey, setHoveredKey] = useState(null);
useEffect(() => {
if (disableHover && hoveredKey !== null) setHoveredKey(null);
}, [disableHover, hoveredKey]);
// helpers
const valueOf = useMemo(() => { const valueOf = useMemo(() => {
if (typeof getItemValue === "function") return getItemValue; if (typeof getItemValue === "function") return getItemValue;
if (variant === "simple") return (item) => item; if (variant === "simple") return (item) => item;
return (item) => item?.title ?? item; // default to title when present return (item) => item?.title ?? item;
}, [getItemValue, variant]); }, [getItemValue, variant]);
const formatValue = (item) => const formatValue = (item) =>
@@ -55,9 +65,7 @@ const ListSelection = ({
}; };
const isSelected = (val, category) => { const isSelected = (val, category) => {
if (variant === "sectioned") { if (variant === "sectioned") return selected?.[category] === val;
return selected?.[category] === val;
}
if (multiple) { if (multiple) {
const list = Array.isArray(selected) ? selected : []; const list = Array.isArray(selected) ? selected : [];
return list.some((v) => v === val); return list.some((v) => v === val);
@@ -67,6 +75,7 @@ const ListSelection = ({
const toggleSelect = (item, category) => { const toggleSelect = (item, category) => {
const val = valueOf(item); const val = valueOf(item);
if (variant === "sectioned") { if (variant === "sectioned") {
const current = selected && typeof selected === "object" ? selected : {}; const current = selected && typeof selected === "object" ? selected : {};
const next = { ...current }; const next = { ...current };
@@ -79,19 +88,16 @@ const ListSelection = ({
if (multiple) { if (multiple) {
const list = Array.isArray(selected) ? selected : []; const list = Array.isArray(selected) ? selected : [];
const exists = list.some((v) => v === val); const exists = list.some((v) => v === val);
if (exists) { if (exists) setSelected(list.filter((v) => v !== val));
setSelected(list.filter((v) => v !== val)); else if (!maxSelection || list.length < maxSelection)
} else { setSelected([...list, formatValue(item)]);
if (!maxSelection || list.length < maxSelection) {
setSelected([...list, formatValue(item)]);
}
}
} else { } else {
if (selected === val) setSelected(null); if (extractSelectedComparable(selected) === val) setSelected(null);
else setSelected(formatValue(item)); else setSelected(formatValue(item));
} }
}; };
// contenus élémentaires : toujours encapsuler le texte dans <Text>
const renderSimpleContent = (label) => ( const renderSimpleContent = (label) => (
<View style={{ minHeight: 40, justifyContent: "center" }}> <View style={{ minHeight: 40, justifyContent: "center" }}>
<Text <Text
@@ -103,7 +109,7 @@ const ListSelection = ({
...itemTextStyle, ...itemTextStyle,
}} }}
> >
{label} {typeof label === "string" ? label : String(label)}
</Text> </Text>
</View> </View>
); );
@@ -118,14 +124,36 @@ const ListSelection = ({
...itemTextStyle, ...itemTextStyle,
}} }}
> >
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>{" "} <Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>
: {item?.description} <Text>{` : ${item?.description ?? ""}`}</Text>
</Text> </Text>
</View> </View>
); );
// applique le gradient bleu UNIQUEMENT sur web et quand sélectionné
const withWebBlueGradientIfSelected = (content, sel) => {
if (!isWeb || !sel) return content;
// wrap dans un View pour éviter texte brut direct sous le gradient (compat RN Web)
return (
<LinearGradient
colors={BLUE_SELECTION_GRADIENT}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
locations={[0, 1]}
style={{
borderRadius: 14,
paddingHorizontal: 12,
paddingVertical: 8,
overflow: "hidden",
}}
>
<View>{content}</View>
</LinearGradient>
);
};
// ============ SECTIONED ============
if (variant === "sectioned") { if (variant === "sectioned") {
// Sectioned list: options as [{ title, data: [] }]
return ( return (
<SectionList <SectionList
sections={options} sections={options}
@@ -135,22 +163,49 @@ const ListSelection = ({
const cat = section?.title; const cat = section?.title;
const val = valueOf(item); const val = valueOf(item);
const sel = isSelected(val, cat); const sel = isSelected(val, cat);
const itemKey = buildItemKey(String(val ?? ""), cat);
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
const baseContent = renderSimpleContent(item);
const headerContainerStyle = {
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
};
if (sel && isWeb)
headerContainerStyle.backgroundColor = "transparent";
return ( return (
<View style={[{ position: "relative" }, itemOuterStyle]}> <View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover
? () => setHoveredKey(itemKey)
: undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
>
<CreateLyricsHeader <CreateLyricsHeader
onPress={() => toggleSelect(item, cat)} onPress={() => toggleSelect(item, cat)}
tint={sel ? "default" : "dark"} tint={sel ? "default" : "dark"}
colors={[Palette.tran, Palette.tran]} colors={[Palette.tran, Palette.tran]}
containerStyle={{ showBorder={!sel}
borderWidth: 0, disableBlur={sel && isWeb}
borderColor: "transparent", blurViewStyle={
borderRadius: 14, sel && isWeb
...itemContainerStyle, ? { paddingHorizontal: 0, paddingVertical: 0 }
}} : undefined
}
containerStyle={headerContainerStyle}
> >
{renderSimpleContent(item)} {withWebBlueGradientIfSelected(baseContent, sel)}
</CreateLyricsHeader> </CreateLyricsHeader>
{sel && (
{showHoverOutline && (
<View <View
pointerEvents="none" pointerEvents="none"
style={{ style={{
@@ -198,10 +253,12 @@ const ListSelection = ({
</BlurView> </BlurView>
</View> </View>
)} )}
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
/> />
); );
} }
// ============ FLAT (simple/titleDescription/emotion) ============
return ( return (
<FlatList <FlatList
data={options} data={options}
@@ -211,7 +268,8 @@ const ListSelection = ({
const val = valueOf(item); const val = valueOf(item);
const sel = isSelected(val); const sel = isSelected(val);
const useEmotionColors = variant === "emotion"; const useEmotionColors = variant === "emotion";
// For emotion variant, hide the left color when selected
// pour "emotion", on masque le bord gauche si sélectionné
const borderColors = useEmotionColors const borderColors = useEmotionColors
? sel ? sel
? [Palette.tran, Palette.tran] ? [Palette.tran, Palette.tran]
@@ -219,27 +277,53 @@ const ListSelection = ({
: [Palette.tran, Palette.tran]; : [Palette.tran, Palette.tran];
const tint = useEmotionColors ? "dark" : sel ? "default" : "dark"; const tint = useEmotionColors ? "dark" : sel ? "default" : "dark";
const itemKey = buildItemKey(String(val ?? ""));
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
const baseContent =
variant === "simple"
? renderSimpleContent(item)
: renderTitleDescriptionContent(item);
const headerContainerStyle = {
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
};
if (sel && isWeb) headerContainerStyle.backgroundColor = "transparent";
return ( return (
<View style={[{ position: "relative" }, itemOuterStyle]}> <View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
>
<CreateLyricsHeader <CreateLyricsHeader
colors={borderColors} colors={borderColors} // mobile: rendu davant conservé
tint={tint} tint={tint}
onPress={() => toggleSelect(item)} onPress={() => toggleSelect(item)}
gradientProps={ gradientProps={
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
} }
containerStyle={{ showBorder={!sel} // mobile: bordure dorigine
borderWidth: 0, disableBlur={sel && isWeb} // web: pas de blur si gradient
borderColor: "transparent", blurViewStyle={
borderRadius: 14, sel && isWeb
...itemContainerStyle, ? { paddingHorizontal: 0, paddingVertical: 0 }
}} : undefined
}
containerStyle={headerContainerStyle}
> >
{variant === "simple" {withWebBlueGradientIfSelected(baseContent, sel)}
? renderSimpleContent(item)
: renderTitleDescriptionContent(item)}
</CreateLyricsHeader> </CreateLyricsHeader>
{sel && (
{showHoverOutline && (
<View <View
pointerEvents="none" pointerEvents="none"
style={{ style={{
+4 -3
View File
@@ -1,9 +1,9 @@
import { View } from "react-native";
import React, { useState } from "react"; import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader"; import { View } from "react-native";
import { EMOTION_CONVEY } from "../../data/data";
import ItemContainer from "../../components/ItemContainer/ItemContainer"; import ItemContainer from "../../components/ItemContainer/ItemContainer";
import ListSelection from "../../components/ListSelection/ListSelection"; import ListSelection from "../../components/ListSelection/ListSelection";
import { EMOTION_CONVEY } from "../../data/data";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
const EmotionConvey = ({ const EmotionConvey = ({
selected: selectedProp, selected: selectedProp,
@@ -29,6 +29,7 @@ const EmotionConvey = ({
> >
<ItemContainer height={containerLayout?.height}> <ItemContainer height={containerLayout?.height}>
<ListSelection <ListSelection
disableHover
options={EMOTION_CONVEY} options={EMOTION_CONVEY}
variant="emotion" variant="emotion"
selected={selected} selected={selected}
@@ -66,29 +66,31 @@ const CreateLyricsHeader = ({
...blurViewStyle, ...blurViewStyle,
}} }}
> >
{title && ( <>
<Text {title && (
style={{ <Text
fontSize: 22, style={{
color: Palette.white, fontSize: 22,
fontFamily: FONT_FAMILY.InterSemiBold, color: Palette.white,
}} fontFamily: FONT_FAMILY.InterSemiBold,
> }}
{title} >
</Text> {title}
)} </Text>
{subTitle && ( )}
<Text {subTitle && (
style={{ <Text
fontSize: 12, style={{
color: Palette.white, fontSize: 12,
fontFamily: FONT_FAMILY.InterRegular, color: Palette.white,
}} fontFamily: FONT_FAMILY.InterRegular,
> }}
{subTitle} >
</Text> {subTitle}
)} </Text>
{children} )}
{children}
</>
</View> </View>
) : ( ) : (
<BlurView <BlurView
@@ -104,29 +106,31 @@ const CreateLyricsHeader = ({
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// } // }
> >
{title && ( <>
<Text {title && (
style={{ <Text
fontSize: 22, style={{
color: Palette.white, fontSize: 22,
fontFamily: FONT_FAMILY.InterSemiBold, color: Palette.white,
}} fontFamily: FONT_FAMILY.InterSemiBold,
> }}
{title} >
</Text> {title}
)} </Text>
{subTitle && ( )}
<Text {subTitle && (
style={{ <Text
fontSize: 12, style={{
color: Palette.white, fontSize: 12,
fontFamily: FONT_FAMILY.InterRegular, color: Palette.white,
}} fontFamily: FONT_FAMILY.InterRegular,
> }}
{subTitle} >
</Text> {subTitle}
)} </Text>
{children} )}
{children}
</>
</BlurView> </BlurView>
)} )}
</View> </View>
+38 -36
View File
@@ -1,5 +1,5 @@
import { View, Text, TextInput } from "react-native";
import React from "react"; import React from "react";
import { Text, TextInput, View } from "react-native";
import { Palette } from "../../../styles"; import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts"; import { FONT_FAMILY } from "../../../styles/Fonts";
@@ -17,44 +17,46 @@ const CustomInput = ({
}) => { }) => {
return ( return (
<View style={{ gap: 8 }} onLayout={onLayout}> <View style={{ gap: 8 }} onLayout={onLayout}>
{label && ( <>
<Text {label && (
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}
>
{label}
</Text>
)}
<View
style={{ style={{
fontSize: 16, backgroundColor: Palette.white,
color: Palette.white, height,
fontFamily: FONT_FAMILY.InterMedium, padding: 12,
borderRadius: 12,
}} }}
> >
{label} <TextInput
</Text> placeholder={placeholder}
)} placeholderTextColor={Palette.grayMid}
<View value={value}
style={{ onChangeText={setValue}
backgroundColor: Palette.white, style={{
height, height: "100%",
padding: 12, fontSize: 16,
borderRadius: 12, color: Palette.black,
}} fontFamily: FONT_FAMILY.InterRegularItalic,
> }}
<TextInput multiline={multiline}
placeholder={placeholder} numberOfLines={multiline ? undefined : 1}
placeholderTextColor={Palette.grayMid} maxLength={maxLength}
value={value} textAlignVertical={multiline ? "top" : "center"}
onChangeText={setValue} onFocus={onFocus}
style={{ onBlur={onBlur}
height: "100%", />
fontSize: 16, </View>
color: Palette.black, </>
fontFamily: FONT_FAMILY.InterRegularItalic,
}}
multiline={multiline}
numberOfLines={multiline ? undefined : 1}
maxLength={maxLength}
textAlignVertical={multiline ? "top" : "center"}
onFocus={onFocus}
onBlur={onBlur}
/>
</View>
</View> </View>
); );
}; };