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 React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { FlatList, Platform, SectionList, Text, View } from "react-native";
import useLayoutType from "../../hooks/useLayoutType";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette } from "../../styles";
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 = ({
options = [],
variant = "simple",
@@ -27,8 +28,9 @@ const ListSelection = ({
itemOuterStyle,
itemTextStyle,
highlightColor = "#F94697",
disableHover = false,
}) => {
// Internal fallback state when not provided by parent
// état interne si non contrôlé
const [internalSelected, setInternalSelected] = useState(
variant === "sectioned" ? {} : multiple ? [] : null
);
@@ -36,10 +38,18 @@ const ListSelection = ({
const setSelected =
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(() => {
if (typeof getItemValue === "function") return getItemValue;
if (variant === "simple") return (item) => item;
return (item) => item?.title ?? item; // default to title when present
return (item) => item?.title ?? item;
}, [getItemValue, variant]);
const formatValue = (item) =>
@@ -55,9 +65,7 @@ const ListSelection = ({
};
const isSelected = (val, category) => {
if (variant === "sectioned") {
return selected?.[category] === val;
}
if (variant === "sectioned") return selected?.[category] === val;
if (multiple) {
const list = Array.isArray(selected) ? selected : [];
return list.some((v) => v === val);
@@ -67,6 +75,7 @@ const ListSelection = ({
const toggleSelect = (item, category) => {
const val = valueOf(item);
if (variant === "sectioned") {
const current = selected && typeof selected === "object" ? selected : {};
const next = { ...current };
@@ -79,19 +88,16 @@ const ListSelection = ({
if (multiple) {
const list = Array.isArray(selected) ? selected : [];
const exists = list.some((v) => v === val);
if (exists) {
setSelected(list.filter((v) => v !== val));
} else {
if (!maxSelection || list.length < maxSelection) {
if (exists) setSelected(list.filter((v) => v !== val));
else if (!maxSelection || list.length < maxSelection)
setSelected([...list, formatValue(item)]);
}
}
} else {
if (selected === val) setSelected(null);
if (extractSelectedComparable(selected) === val) setSelected(null);
else setSelected(formatValue(item));
}
};
// contenus élémentaires : toujours encapsuler le texte dans <Text>
const renderSimpleContent = (label) => (
<View style={{ minHeight: 40, justifyContent: "center" }}>
<Text
@@ -103,7 +109,7 @@ const ListSelection = ({
...itemTextStyle,
}}
>
{label}
{typeof label === "string" ? label : String(label)}
</Text>
</View>
);
@@ -118,14 +124,36 @@ const ListSelection = ({
...itemTextStyle,
}}
>
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>{" "}
: {item?.description}
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>
<Text>{` : ${item?.description ?? ""}`}</Text>
</Text>
</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") {
// Sectioned list: options as [{ title, data: [] }]
return (
<SectionList
sections={options}
@@ -135,22 +163,49 @@ const ListSelection = ({
const cat = section?.title;
const val = valueOf(item);
const sel = isSelected(val, cat);
return (
<View style={[{ position: "relative" }, itemOuterStyle]}>
<CreateLyricsHeader
onPress={() => toggleSelect(item, cat)}
tint={sel ? "default" : "dark"}
colors={[Palette.tran, Palette.tran]}
containerStyle={{
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 (
<View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover
? () => setHoveredKey(itemKey)
: undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
>
{renderSimpleContent(item)}
<CreateLyricsHeader
onPress={() => toggleSelect(item, cat)}
tint={sel ? "default" : "dark"}
colors={[Palette.tran, Palette.tran]}
showBorder={!sel}
disableBlur={sel && isWeb}
blurViewStyle={
sel && isWeb
? { paddingHorizontal: 0, paddingVertical: 0 }
: undefined
}
containerStyle={headerContainerStyle}
>
{withWebBlueGradientIfSelected(baseContent, sel)}
</CreateLyricsHeader>
{sel && (
{showHoverOutline && (
<View
pointerEvents="none"
style={{
@@ -198,10 +253,12 @@ const ListSelection = ({
</BlurView>
</View>
)}
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
/>
);
}
// ============ FLAT (simple/titleDescription/emotion) ============
return (
<FlatList
data={options}
@@ -211,7 +268,8 @@ const ListSelection = ({
const val = valueOf(item);
const sel = isSelected(val);
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
? sel
? [Palette.tran, Palette.tran]
@@ -219,27 +277,53 @@ const ListSelection = ({
: [Palette.tran, Palette.tran];
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 (
<View style={[{ position: "relative" }, itemOuterStyle]}>
<View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
>
<CreateLyricsHeader
colors={borderColors}
colors={borderColors} // mobile: rendu davant conservé
tint={tint}
onPress={() => toggleSelect(item)}
gradientProps={
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
}
containerStyle={{
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
}}
showBorder={!sel} // mobile: bordure dorigine
disableBlur={sel && isWeb} // web: pas de blur si gradient
blurViewStyle={
sel && isWeb
? { paddingHorizontal: 0, paddingVertical: 0 }
: undefined
}
containerStyle={headerContainerStyle}
>
{variant === "simple"
? renderSimpleContent(item)
: renderTitleDescriptionContent(item)}
{withWebBlueGradientIfSelected(baseContent, sel)}
</CreateLyricsHeader>
{sel && (
{showHoverOutline && (
<View
pointerEvents="none"
style={{
+4 -3
View File
@@ -1,9 +1,9 @@
import { View } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { EMOTION_CONVEY } from "../../data/data";
import { View } from "react-native";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import ListSelection from "../../components/ListSelection/ListSelection";
import { EMOTION_CONVEY } from "../../data/data";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
const EmotionConvey = ({
selected: selectedProp,
@@ -29,6 +29,7 @@ const EmotionConvey = ({
>
<ItemContainer height={containerLayout?.height}>
<ListSelection
disableHover
options={EMOTION_CONVEY}
variant="emotion"
selected={selected}
@@ -66,6 +66,7 @@ const CreateLyricsHeader = ({
...blurViewStyle,
}}
>
<>
{title && (
<Text
style={{
@@ -89,6 +90,7 @@ const CreateLyricsHeader = ({
</Text>
)}
{children}
</>
</View>
) : (
<BlurView
@@ -104,6 +106,7 @@ const CreateLyricsHeader = ({
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<>
{title && (
<Text
style={{
@@ -127,6 +130,7 @@ const CreateLyricsHeader = ({
</Text>
)}
{children}
</>
</BlurView>
)}
</View>
@@ -1,5 +1,5 @@
import { View, Text, TextInput } from "react-native";
import React from "react";
import { Text, TextInput, View } from "react-native";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
@@ -17,6 +17,7 @@ const CustomInput = ({
}) => {
return (
<View style={{ gap: 8 }} onLayout={onLayout}>
<>
{label && (
<Text
style={{
@@ -55,6 +56,7 @@ const CustomInput = ({
onBlur={onBlur}
/>
</View>
</>
</View>
);
};