From 2908afc2597e7404a2c9678e181c9d224131d3cc Mon Sep 17 00:00:00 2001 From: leon-morival Date: Wed, 1 Oct 2025 14:07:13 +0200 Subject: [PATCH] web style --- src/components/ListSelection/ListSelection.js | 176 +++++++++++++----- src/screens/Writing/EmotionConvey.js | 7 +- .../Writing/components/CreateLyricsHeader.js | 96 +++++----- src/screens/Writing/components/CustomInput.js | 74 ++++---- 4 files changed, 222 insertions(+), 131 deletions(-) diff --git a/src/components/ListSelection/ListSelection.js b/src/components/ListSelection/ListSelection.js index 0a7536f..60cd01f 100644 --- a/src/components/ListSelection/ListSelection.js +++ b/src/components/ListSelection/ListSelection.js @@ -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) { - setSelected([...list, formatValue(item)]); - } - } + 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 const renderSimpleContent = (label) => ( - {label} + {typeof label === "string" ? label : String(label)} ); @@ -118,14 +124,36 @@ const ListSelection = ({ ...itemTextStyle, }} > - {item?.title}{" "} - : {item?.description} + {item?.title} + {` : ${item?.description ?? ""}`} ); + // 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 ( + + {content} + + ); + }; + + // ============ SECTIONED ============ if (variant === "sectioned") { - // Sectioned list: options as [{ title, data: [] }] return ( + setHoveredKey(itemKey) + : undefined + } + onMouseLeave={ + isWeb && !disableHover ? () => setHoveredKey(null) : undefined + } + > toggleSelect(item, cat)} tint={sel ? "default" : "dark"} colors={[Palette.tran, Palette.tran]} - containerStyle={{ - borderWidth: 0, - borderColor: "transparent", - borderRadius: 14, - ...itemContainerStyle, - }} + showBorder={!sel} + disableBlur={sel && isWeb} + blurViewStyle={ + sel && isWeb + ? { paddingHorizontal: 0, paddingVertical: 0 } + : undefined + } + containerStyle={headerContainerStyle} > - {renderSimpleContent(item)} + {withWebBlueGradientIfSelected(baseContent, sel)} - {sel && ( + + {showHoverOutline && ( )} + keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`} /> ); } + // ============ FLAT (simple/titleDescription/emotion) ============ return ( + setHoveredKey(itemKey) : undefined + } + onMouseLeave={ + isWeb && !disableHover ? () => setHoveredKey(null) : undefined + } + > toggleSelect(item)} gradientProps={ useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined } - containerStyle={{ - borderWidth: 0, - borderColor: "transparent", - borderRadius: 14, - ...itemContainerStyle, - }} + showBorder={!sel} // mobile: bordure d’origine + 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)} - {sel && ( + + {showHoverOutline && ( - {title && ( - - {title} - - )} - {subTitle && ( - - {subTitle} - - )} - {children} + <> + {title && ( + + {title} + + )} + {subTitle && ( + + {subTitle} + + )} + {children} + ) : ( - {title && ( - - {title} - - )} - {subTitle && ( - - {subTitle} - - )} - {children} + <> + {title && ( + + {title} + + )} + {subTitle && ( + + {subTitle} + + )} + {children} + )} diff --git a/src/screens/Writing/components/CustomInput.js b/src/screens/Writing/components/CustomInput.js index 9b18d5c..09198f8 100644 --- a/src/screens/Writing/components/CustomInput.js +++ b/src/screens/Writing/components/CustomInput.js @@ -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,44 +17,46 @@ const CustomInput = ({ }) => { return ( - {label && ( - + {label && ( + + {label} + + )} + - {label} - - )} - - - + + + ); };