web style
This commit is contained in:
@@ -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 d’avant 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 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)}
|
||||
</CreateLyricsHeader>
|
||||
{sel && (
|
||||
|
||||
{showHoverOutline && (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user