1- import { Component , mergeComponentProps } from "../../utils" ;
2- import React , { useCallback , useEffect , useRef , useState } from "react" ;
3- import { create } from 'zustand'
1+ import { Component , mergeComponentProps } from "../../utils"
2+ import React , { useCallback , useEffect , useRef , useState } from "react"
43import { motion } from "motion/react"
54import "./Island.style.scss"
6- import { Flex } from "../flex/Flex" ;
5+ import { Flex } from "../flex/Flex"
6+ import { useIsland } from "./Island.hook" ;
77
88export interface IslandProps extends Component < HTMLDivElement > {
99 children ?: React . ReactNode
1010}
1111
12- export interface IslandToast {
13- id ?: number
14- icon : React . ReactNode
15- message : React . ReactNode
16- largeContent ?: React . ReactNode
17- }
18-
1912export const Island : React . FC < IslandProps > = ( props ) => {
20- const { children, ...rest } = props
21-
22- const firstIsland = useIsland ( useCallback ( state => state . toasts [ 0 ] , [ ] ) ) ;
23- const removeToast = useIsland ( useCallback ( state => state . removeToast , [ ] ) ) ;
2413
25- const [ isHovered , setIsHovered ] = useState ( false ) ;
26- const hasLargeContent = ! ! firstIsland ?. largeContent ;
27- const isExpanded = isHovered && hasLargeContent ;
14+ const { children, ...rest } = props
2815
16+ const toasts = useIsland ( state => state . toasts )
17+ const removeToast = useIsland ( state => state . removeToast )
2918
30- const timeoutIdRef = useRef < NodeJS . Timeout | null > ( null ) ;
31- const startTimeRef = useRef < number > ( 0 ) ;
32- const remainingTimeRef = useRef < number > ( 4000 ) ;
19+ const timeoutIdRef = useRef < NodeJS . Timeout | null > ( null )
20+ const startTimeRef = useRef < number > ( 0 )
21+ const remainingTimeRef = useRef < number > ( 4000 )
22+ const [ isHovered , setIsHovered ] = useState ( false )
23+ const containerRef = useRef < HTMLDivElement | null > ( null )
3324
34- const currentIdRef = useRef < number | undefined > ( undefined ) ;
35- currentIdRef . current = firstIsland ?. id ;
25+ const firstIsland = toasts . length > 0
26+ ? toasts . reduce ( ( highest , current ) =>
27+ ( ( current . index ?? 0 ) > ( highest . index ?? 0 ) ) ? current : highest
28+ )
29+ : undefined
3630
3731 const stopTimer = useCallback ( ( ) => {
3832 if ( timeoutIdRef . current ) {
39- clearTimeout ( timeoutIdRef . current ) ;
40- timeoutIdRef . current = null ;
33+ clearTimeout ( timeoutIdRef . current )
34+ timeoutIdRef . current = null
4135 }
42- } , [ ] ) ;
36+ } , [ ] )
4337
4438 const startTimer = useCallback ( ( ) => {
45- const toastId = currentIdRef . current ;
46- if ( ! toastId ) return ;
39+ const toastId = firstIsland ?. id
40+ if ( ! toastId ) return
4741
48- stopTimer ( ) ;
49- startTimeRef . current = Date . now ( ) ;
42+ stopTimer ( )
43+ startTimeRef . current = Date . now ( )
5044
51- timeoutIdRef . current = setTimeout ( ( ) => {
52- removeToast ( toastId ) ;
53- } , remainingTimeRef . current ) ;
54- } , [ removeToast , stopTimer ] ) ;
45+ if ( isFinite ( remainingTimeRef . current ) ) {
46+ timeoutIdRef . current = setTimeout ( ( ) => {
47+ removeToast ( toastId )
48+ } , remainingTimeRef . current )
49+ }
50+ } , [ firstIsland ?. id , removeToast , stopTimer ] )
5551
56- // Triggert den Timer UND die Icon-Zeichenanimation bei neuem Toast
5752 useEffect ( ( ) => {
5853 if ( firstIsland ?. id ) {
59- remainingTimeRef . current = 4000 ;
60- startTimer ( ) ;
54+ remainingTimeRef . current = firstIsland ?. duration ?? 4000
55+ startTimer ( )
6156 } else {
62- stopTimer ( ) ;
63- setIsHovered ( false ) ;
57+ stopTimer ( )
58+ setIsHovered ( false )
6459 }
65- return ( ) => stopTimer ( ) ;
66- } , [ firstIsland ?. id , startTimer , stopTimer ] ) ;
60+ return ( ) => stopTimer ( )
61+ } , [ firstIsland ?. id , firstIsland ?. duration , startTimer , stopTimer ] )
6762
6863 const handleMouseEnter = ( ) => {
69- if ( ! firstIsland ?. id ) return ;
64+ if ( ! firstIsland ?. id ) return
7065
71- stopTimer ( ) ;
72- const elapsedTime = Date . now ( ) - startTimeRef . current ;
73- remainingTimeRef . current = Math . max ( 0 , remainingTimeRef . current - elapsedTime ) ;
66+ stopTimer ( )
67+ const elapsedTime = Date . now ( ) - startTimeRef . current
68+ remainingTimeRef . current = Math . max ( 0 , remainingTimeRef . current - elapsedTime )
7469
75- if ( hasLargeContent ) {
76- setIsHovered ( true ) ;
70+ if ( ! ! firstIsland ?. content ) {
71+ setIsHovered ( true )
7772 }
78- } ;
73+ }
7974
8075 const handleMouseLeave = ( ) => {
81- setIsHovered ( false ) ;
82- if ( ! firstIsland ?. id ) return ;
76+ setIsHovered ( false )
77+ if ( ! firstIsland ?. id ) return
8378
8479 if ( remainingTimeRef . current > 0 ) {
85- startTimer ( ) ;
80+ startTimer ( )
8681 }
87- } ;
82+ }
8883
8984 return (
9085 < motion . div
@@ -94,82 +89,51 @@ export const Island: React.FC<IslandProps> = (props) => {
9489 transition = { {
9590 type : "spring" ,
9691 stiffness : 300 ,
97- damping : 17.5 ,
98- mass : 0.7
92+ damping : 20 ,
93+ mass : 0.8
9994 } }
100- { ...mergeComponentProps ( `island` , {
101- ...rest ,
102- style : {
103- overflow : "hidden" ,
104- // Hier verankern wir die Mindestmaße der Box, wie gewünscht
105- minWidth : "max-content" ,
106- minHeight : "max-content" ,
107- ...rest . style
108- }
109- } ) }
95+ { ...mergeComponentProps ( `island` , rest ) }
11096 >
11197
112- < div >
113- { /* --- STANDARD CONTENT --- */ }
114-
115- < Flex align = { "center" } style = { { gap : "0.35rem" } } key = { firstIsland ?. id } >
116- { firstIsland && (
117- < motion . div
118- key = { firstIsland ?. id }
119- layout
120- initial = { { y : 0 , opacity : 1 , strokeDashoffset : "300%" } }
121- animate = { { y : 0 , opacity : 1 , strokeDashoffset : "0%" } }
122- transition = { { duration : 0.75 , ease : "easeInOut" } }
123- className = { "island__icon" }
124- >
125- { firstIsland . icon }
126- </ motion . div >
127- ) }
128- < motion . div layout >
129- { children }
98+ < Flex align = { "center" } style = { { gap : "0.35rem" } } ref = { containerRef } key = { firstIsland ?. id } >
99+ { firstIsland && (
100+ < motion . div
101+ key = { firstIsland ?. id }
102+ layout
103+ initial = { { y : 0 , opacity : 1 , strokeDashoffset : "300%" } }
104+ animate = { { y : 0 , opacity : 1 , strokeDashoffset : "0%" } }
105+ transition = { { duration : 0.75 , ease : "easeInOut" } }
106+ className = { "island__icon" }
107+ >
108+ { firstIsland . icon }
130109 </ motion . div >
110+ ) }
111+ < motion . div layout >
112+ { children }
113+ </ motion . div >
114+
115+ { firstIsland ?. message && < motion . div layout className = { "island__message" } >
116+ { firstIsland ?. message }
117+ </ motion . div > }
118+ </ Flex >
131119
132- { firstIsland ?. message && < motion . div layout className = { "island__message" } >
133- { firstIsland ?. message }
134- </ motion . div > }
135- </ Flex >
120+ < div style = { {
121+ maxWidth : `${ containerRef . current ?. offsetWidth } px` ,
122+ } } >
136123
137- { /* --- LARGE CONTENT --- */ }
138- { isExpanded && < motion . div
139- initial = { { y : 50 , opacity : 0 } }
124+ { isHovered && ! ! firstIsland ?. content && < motion . div
125+ initial = { { y : 10 , opacity : 0 } }
140126 animate = { {
141- y : isExpanded ? 0 : 50 ,
142- opacity : isExpanded ? 1 : 0
127+ y : isHovered && ! ! firstIsland ?. content ? 0 : 100 ,
128+ opacity : isHovered && ! ! firstIsland ?. content ? 1 : 0
143129 } }
144130 transition = { { duration : 0.25 , ease : "easeInOut" } }
145131 className = { "island__content" }
146- style = { {
147- position : "relative" ,
148- width : "100%" ,
149- height : "100%" ,
150- pointerEvents : isExpanded ? "auto" : "none"
151- } }
152132 >
153- { firstIsland ?. largeContent }
133+ { firstIsland ?. content }
154134 </ motion . div > }
155135 </ div >
156136
157-
158137 </ motion . div >
159- ) ;
160- } ;
161-
162- export const useIsland = create < {
163- toasts : IslandToast [ ] ,
164- addToast : ( toast : IslandToast ) => void ,
165- removeToast : ( id : number ) => void
166- } > ( ( set ) => ( {
167- toasts : [ ] ,
168- addToast : ( toast : IslandToast ) => {
169- const id = Date . now ( ) ;
170- set ( state => ( { toasts : [ ...state . toasts , { id, ...toast } ] } ) ) ;
171- } ,
172- removeToast : ( id : number ) => {
173- set ( state => ( { toasts : state . toasts . filter ( t => t . id !== id ) } ) ) ;
174- }
175- } ) ) ;
138+ )
139+ }
0 commit comments