@bemedev/mind-flow 0.3.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -12
- package/lib/helpers/clamp.d.ts +2 -0
- package/lib/helpers/clamp.d.ts.map +1 -0
- package/lib/helpers/createContext.d.ts +1 -1
- package/lib/helpers/createContext.d.ts.map +1 -1
- package/lib/helpers/index.d.ts +3 -0
- package/lib/helpers/index.d.ts.map +1 -0
- package/lib/index.cjs.js +1 -1
- package/lib/index.d.ts +9 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.es.js +2543 -448
- package/lib/output.css +1 -1
- package/lib/server.cjs.js +1129 -397
- package/lib/server.es.js +1095 -407
- package/lib/services/main.machine.d.ts +2453 -576
- package/lib/services/main.machine.d.ts.map +1 -1
- package/lib/services/main.machine.data.d.ts +14 -1
- package/lib/services/main.machine.data.d.ts.map +1 -1
- package/lib/services/main.machine.helpers.d.ts +1 -1
- package/lib/services/main.machine.helpers.d.ts.map +1 -1
- package/lib/services/main.machine.test.d.ts +2 -0
- package/lib/services/main.machine.test.d.ts.map +1 -0
- package/lib/services/main.machine.typings.d.ts +26 -8
- package/lib/services/main.machine.typings.d.ts.map +1 -1
- package/lib/ui/Flow.d.ts +8 -4
- package/lib/ui/Flow.d.ts.map +1 -1
- package/lib/ui/components/Bounds.d.ts.map +1 -1
- package/lib/ui/components/EdgeComponent.d.ts.map +1 -1
- package/lib/ui/components/EdgesBoard.d.ts.map +1 -1
- package/lib/ui/components/EditPanel.d.ts +19 -0
- package/lib/ui/components/EditPanel.d.ts.map +1 -0
- package/lib/ui/components/EditPanel.hooks.d.ts +29 -0
- package/lib/ui/components/EditPanel.hooks.d.ts.map +1 -0
- package/lib/ui/components/EditPanel.types.d.ts +47 -0
- package/lib/ui/components/EditPanel.types.d.ts.map +1 -0
- package/lib/ui/components/FlowChart.context.d.ts +2452 -575
- package/lib/ui/components/FlowChart.context.d.ts.map +1 -1
- package/lib/ui/components/FlowChart.d.ts +8 -47
- package/lib/ui/components/FlowChart.d.ts.map +1 -1
- package/lib/ui/components/FlowChart.types.d.ts +87 -0
- package/lib/ui/components/FlowChart.types.d.ts.map +1 -0
- package/lib/ui/components/NodeComponent.d.ts +10 -12
- package/lib/ui/components/NodeComponent.d.ts.map +1 -1
- package/lib/ui/components/NodesBoard.d.ts +17 -2
- package/lib/ui/components/NodesBoard.d.ts.map +1 -1
- package/lib/ui/components/Panels.d.ts +12 -0
- package/lib/ui/components/Panels.d.ts.map +1 -0
- package/lib/ui/globals/components/atoms/TypingText.d.ts +29 -0
- package/lib/ui/globals/components/atoms/TypingText.d.ts.map +1 -0
- package/lib/ui/globals/components/molecules/FadingDots.d.ts +21 -0
- package/lib/ui/globals/components/molecules/FadingDots.d.ts.map +1 -0
- package/lib/ui/globals/components/organisms/LoadingFallback.d.ts +46 -0
- package/lib/ui/globals/components/organisms/LoadingFallback.d.ts.map +1 -0
- package/lib/ui/globals/constants/index.d.ts +3 -0
- package/lib/ui/globals/constants/index.d.ts.map +1 -0
- package/lib/ui/globals/constants/links.d.ts +4 -0
- package/lib/ui/globals/constants/links.d.ts.map +1 -0
- package/lib/ui/globals/constants/strings.d.ts +5 -0
- package/lib/ui/globals/constants/strings.d.ts.map +1 -0
- package/lib/ui/globals/directives/clickOutside.d.ts +9 -0
- package/lib/ui/globals/directives/clickOutside.d.ts.map +1 -0
- package/lib/ui/globals/directives/index.d.ts +16 -0
- package/lib/ui/globals/directives/index.d.ts.map +1 -0
- package/lib/ui/globals/directives/mouseOut.d.ts +33 -0
- package/lib/ui/globals/directives/mouseOut.d.ts.map +1 -0
- package/lib/ui/globals/directives/mouseOut.test.d.ts +2 -0
- package/lib/ui/globals/directives/mouseOut.test.d.ts.map +1 -0
- package/lib/ui/globals/directives/resize.d.ts +11 -0
- package/lib/ui/globals/directives/resize.d.ts.map +1 -0
- package/lib/ui/globals/helpers/espace.d.ts +17 -0
- package/lib/ui/globals/helpers/espace.d.ts.map +1 -0
- package/lib/ui/globals/signals/createTyping.d.ts +27 -0
- package/lib/ui/globals/signals/createTyping.d.ts.map +1 -0
- package/lib/ui/globals/types.d.ts +54 -0
- package/lib/ui/globals/types.d.ts.map +1 -0
- package/lib/ui/utils.d.ts +11 -0
- package/lib/ui/utils.d.ts.map +1 -0
- package/package.json +2 -3
- package/src/helpers/clamp.ts +3 -0
- package/src/helpers/createContext.ts +20 -4
- package/src/helpers/index.ts +2 -0
- package/src/index.ts +9 -1
- package/src/services/main.machine.data.ts +13 -2
- package/src/services/main.machine.helpers.ts +12 -4
- package/src/services/main.machine.test.ts +205 -0
- package/src/services/main.machine.ts +253 -75
- package/src/services/main.machine.typings.ts +31 -6
- package/src/ui/Flow.tsx +47 -8
- package/src/ui/components/Bounds.tsx +0 -2
- package/src/ui/components/EdgeComponent.tsx +60 -46
- package/src/ui/components/EdgesBoard.tsx +1 -2
- package/src/ui/components/EditPanel.hooks.ts +69 -0
- package/src/ui/components/EditPanel.tsx +89 -0
- package/src/ui/components/EditPanel.types.ts +49 -0
- package/src/ui/components/FlowChart.context.ts +22 -141
- package/src/ui/components/FlowChart.tsx +27 -64
- package/src/ui/components/FlowChart.types.ts +92 -0
- package/src/ui/components/NodeComponent.tsx +88 -65
- package/src/ui/components/NodesBoard.tsx +61 -56
- package/src/ui/components/Panels.tsx +40 -0
- package/src/ui/globals/components/atoms/TypingText.tsx +63 -0
- package/src/ui/globals/components/molecules/FadingDots.tsx +45 -0
- package/src/ui/globals/components/organisms/LoadingFallback.tsx +116 -0
- package/src/ui/globals/constants/index.ts +2 -0
- package/src/ui/globals/constants/links.ts +15 -0
- package/src/ui/globals/constants/strings.ts +5 -0
- package/src/ui/globals/directives/clickOutside.ts +18 -0
- package/src/ui/globals/directives/index.ts +16 -0
- package/src/ui/globals/directives/mouseOut.test.ts +115 -0
- package/src/ui/globals/directives/mouseOut.ts +98 -0
- package/src/ui/globals/directives/resize.ts +33 -0
- package/src/ui/globals/helpers/espace.ts +26 -0
- package/src/ui/globals/signals/createTyping.ts +82 -0
- package/src/ui/globals/types.ts +83 -0
- package/src/ui/utils.ts +14 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Show, type Component } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
import type { FlowPanels } from './FlowChart.types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Overlay container rendering custom panel slots positioned around the flowchart
|
|
7
|
+
* canvas.
|
|
8
|
+
*
|
|
9
|
+
* @param props - Flow panels layout properties of type {@linkcode FlowPanels}.
|
|
10
|
+
*
|
|
11
|
+
* @returns Rendered overlay panels JSX elements.
|
|
12
|
+
*/
|
|
13
|
+
export const Panels: Component<FlowPanels> = props => {
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<Show when={props.topLeft} keyed>
|
|
17
|
+
{Panel => {
|
|
18
|
+
const Component = typeof Panel === 'function' ? <Panel /> : Panel;
|
|
19
|
+
return <div class='absolute top-4 left-4 z-50'>{Component}</div>;
|
|
20
|
+
}}
|
|
21
|
+
</Show>
|
|
22
|
+
|
|
23
|
+
{/* Top-Right Panel */}
|
|
24
|
+
<Show when={props.topRight} keyed>
|
|
25
|
+
{Panel => {
|
|
26
|
+
const Component = typeof Panel === 'function' ? <Panel /> : Panel;
|
|
27
|
+
return <div class='absolute top-4 right-4 z-50'>{Component}</div>;
|
|
28
|
+
}}
|
|
29
|
+
</Show>
|
|
30
|
+
|
|
31
|
+
{/* Bottom-Left Panel */}
|
|
32
|
+
<Show when={props.bottomLeft} keyed>
|
|
33
|
+
{Panel => {
|
|
34
|
+
const Component = typeof Panel === 'function' ? <Panel /> : Panel;
|
|
35
|
+
return <div class='absolute bottom-4 left-4 z-50'>{Component}</div>;
|
|
36
|
+
}}
|
|
37
|
+
</Show>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
createMemo,
|
|
4
|
+
on,
|
|
5
|
+
type Accessor,
|
|
6
|
+
type Component,
|
|
7
|
+
} from 'solid-js';
|
|
8
|
+
|
|
9
|
+
import { VISIBLE_ESPACE } from '../../constants';
|
|
10
|
+
import { createTyping } from '../../signals/createTyping';
|
|
11
|
+
|
|
12
|
+
/** Base configuration properties for {@linkcode TypingText}. */
|
|
13
|
+
type BaseProps = {
|
|
14
|
+
/** The text string to animate typing out. */
|
|
15
|
+
children: string;
|
|
16
|
+
/** Optional container CSS class name. */
|
|
17
|
+
class?: string;
|
|
18
|
+
/** Keystroke interval delay in milliseconds. Defaults to 62. */
|
|
19
|
+
interval?: number;
|
|
20
|
+
/** Optional boolean accessor to disable typing and display text immediately. */
|
|
21
|
+
disabled?: Accessor<boolean>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Props for the {@linkcode TypingText} component with optional rewind configuration. */
|
|
25
|
+
type Props = BaseProps &
|
|
26
|
+
({ rewind: true; rewindDelay: number } | { rewind?: false });
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Renders animated text with a typewriter keystroke animation effect.
|
|
30
|
+
*
|
|
31
|
+
* @param props - Component configuration properties of type {@linkcode Props}.
|
|
32
|
+
*
|
|
33
|
+
* @returns Rendered animated typing text JSX container.
|
|
34
|
+
*/
|
|
35
|
+
export const TypingText: Component<Props> = props => {
|
|
36
|
+
// #region Default values
|
|
37
|
+
const min = props.interval ?? 62;
|
|
38
|
+
const rewind = props.rewind ?? false;
|
|
39
|
+
const rewindDelay = (props as any).rewindDelay ?? 500;
|
|
40
|
+
const content = props.children;
|
|
41
|
+
const disabled = createMemo(
|
|
42
|
+
() => (props.disabled ? props.disabled() : false),
|
|
43
|
+
false,
|
|
44
|
+
);
|
|
45
|
+
// #endregion
|
|
46
|
+
|
|
47
|
+
const { text, type, setText } = createTyping({
|
|
48
|
+
content,
|
|
49
|
+
min,
|
|
50
|
+
rewind,
|
|
51
|
+
rewindDelay,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
createEffect(on(disabled, () => setText(VISIBLE_ESPACE)));
|
|
55
|
+
|
|
56
|
+
createEffect(() => {
|
|
57
|
+
// If disabled, show full text immediately
|
|
58
|
+
if (disabled()) setText(content);
|
|
59
|
+
else type();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return <div class={props.class}>{text()}</div>;
|
|
63
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { splitProps, type Component, JSX } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../../utils';
|
|
4
|
+
import type { OmitPropsOf } from '../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loading animation component displaying animated pulsing dots.
|
|
8
|
+
*
|
|
9
|
+
* @param props - Fading dots configuration properties.
|
|
10
|
+
* @param props.count - The number of dots to display.
|
|
11
|
+
* @param props.duration - Animation duration in milliseconds. Defaults to 500.
|
|
12
|
+
* @param props.innerProps - Additional properties and CSS styles applied to dot
|
|
13
|
+
* elements.
|
|
14
|
+
*
|
|
15
|
+
* @returns Rendered animated dots container JSX element.
|
|
16
|
+
*/
|
|
17
|
+
export const FadingDots: Component<{
|
|
18
|
+
count: number;
|
|
19
|
+
duration?: number;
|
|
20
|
+
innerProps?: OmitPropsOf<'span', 'children' | 'style'> & {
|
|
21
|
+
style?: JSX.CSSProperties;
|
|
22
|
+
};
|
|
23
|
+
}> = ({ count: length, innerProps = {}, duration = 500 }) => {
|
|
24
|
+
const [local, rest] = splitProps(innerProps, ['class', 'style']);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div class='mt-4 flex items-center justify-center space-x-4'>
|
|
28
|
+
{Array.from({ length }, (_, i) => (
|
|
29
|
+
<span
|
|
30
|
+
class={cn(
|
|
31
|
+
'rounded-full animate-out direction-alternate repeat-infinite fade-out-5 size-4 bg-gray-600',
|
|
32
|
+
local.class,
|
|
33
|
+
)}
|
|
34
|
+
style={{
|
|
35
|
+
...local.style,
|
|
36
|
+
'animation-duration': `${duration}ms`,
|
|
37
|
+
'animation-delay': `${duration / i}ms`,
|
|
38
|
+
}}
|
|
39
|
+
aria-hidden='true'
|
|
40
|
+
{...rest}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Component } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../../utils';
|
|
4
|
+
import { TypingText } from '../atoms/TypingText';
|
|
5
|
+
import { FadingDots } from '../molecules/FadingDots';
|
|
6
|
+
|
|
7
|
+
/** Properties for the {@linkcode LoadingFallback} component. */
|
|
8
|
+
interface LoadingFallbackProps {
|
|
9
|
+
/**
|
|
10
|
+
* Text message to display alongside the loading animation.
|
|
11
|
+
*
|
|
12
|
+
* @default 'Chargement en cours...'
|
|
13
|
+
*/
|
|
14
|
+
message?: string;
|
|
15
|
+
/** Additional CSS class names. */
|
|
16
|
+
class?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Spinner dimension sizing.
|
|
19
|
+
*
|
|
20
|
+
* @default 'lg'
|
|
21
|
+
*/
|
|
22
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
23
|
+
/** Keystroke typing interval in milliseconds. Defaults to 80. */
|
|
24
|
+
interval?: number;
|
|
25
|
+
/** Whether the typing text animation rewinds upon completion. Defaults to true. */
|
|
26
|
+
rewind?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Delay in milliseconds before rewinding or restarting typing animation. Defaults
|
|
29
|
+
* to 2000.
|
|
30
|
+
*/
|
|
31
|
+
rewindDelay?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fallback loading screen component displaying an animated spinner and typewriter
|
|
36
|
+
* text.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Show when={data()} fallback={<LoadingFallback />}>
|
|
41
|
+
* <DataComponent data={data()!} />
|
|
42
|
+
* </Show>;
|
|
43
|
+
* ```;
|
|
44
|
+
*
|
|
45
|
+
* @param props - Loading fallback properties of type
|
|
46
|
+
* {@linkcode LoadingFallbackProps}.
|
|
47
|
+
*
|
|
48
|
+
* @returns Rendered full-screen loading fallback JSX element.
|
|
49
|
+
*/
|
|
50
|
+
export const LoadingFallback: Component<LoadingFallbackProps> = props => {
|
|
51
|
+
const message = props.message ?? 'Chargement en cours...';
|
|
52
|
+
|
|
53
|
+
const spinnerSizes = {
|
|
54
|
+
sm: 'w-8 h-8',
|
|
55
|
+
md: 'w-12 h-12',
|
|
56
|
+
lg: 'w-16 h-16',
|
|
57
|
+
xl: 'w-24 h-24',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const size = props.size ?? 'lg';
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
class={cn(
|
|
65
|
+
'flex flex-col items-center justify-center gap-6',
|
|
66
|
+
'h-screen w-screen overflow-hidden p-4',
|
|
67
|
+
props.class,
|
|
68
|
+
)}
|
|
69
|
+
role='status'
|
|
70
|
+
aria-live='polite'
|
|
71
|
+
aria-label='Chargement en cours'
|
|
72
|
+
>
|
|
73
|
+
{/* Spinner animé */}
|
|
74
|
+
<div class='relative'>
|
|
75
|
+
{/* Cercle extérieur avec rotation */}
|
|
76
|
+
<div
|
|
77
|
+
class={cn(
|
|
78
|
+
spinnerSizes[size],
|
|
79
|
+
'rounded-full border-4 border-[#4B9CAD]/20',
|
|
80
|
+
'animate-spin',
|
|
81
|
+
'border-t-[#4B9CAD]',
|
|
82
|
+
)}
|
|
83
|
+
aria-hidden='true'
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{/* Cercle intérieur pour effet de profondeur */}
|
|
87
|
+
<div
|
|
88
|
+
class={cn(
|
|
89
|
+
'absolute inset-2',
|
|
90
|
+
'rounded-full border-4 border-transparent',
|
|
91
|
+
'border-b-[#4B9CAD]/40',
|
|
92
|
+
'animate-spin',
|
|
93
|
+
'animation-duration-[1.5s]',
|
|
94
|
+
'direction-[reverse]',
|
|
95
|
+
)}
|
|
96
|
+
aria-hidden='true'
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Texte avec effet typing */}
|
|
101
|
+
<div class='text-center' aria-label={message}>
|
|
102
|
+
<TypingText
|
|
103
|
+
class='text-lg font-medium text-gray-700 md:text-xl dark:text-gray-300'
|
|
104
|
+
interval={props.interval ?? 80}
|
|
105
|
+
rewind={props.rewind ?? true}
|
|
106
|
+
rewindDelay={props.rewindDelay ?? 2000}
|
|
107
|
+
>
|
|
108
|
+
{message}
|
|
109
|
+
</TypingText>
|
|
110
|
+
|
|
111
|
+
{/* Points d'animation pour indiquer le chargement */}
|
|
112
|
+
<FadingDots count={5} innerProps={{ class: 'bg-[#3d8091]' }} />
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RootLink } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Preconnect and stylesheet link descriptors for Google Fonts. */
|
|
4
|
+
export const GOOGLE_FONTS_LINKS: RootLink[] = [
|
|
5
|
+
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous' },
|
|
6
|
+
{
|
|
7
|
+
rel: 'preconnect',
|
|
8
|
+
href: 'https://fonts.googleapis.com',
|
|
9
|
+
crossorigin: 'anonymous',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
rel: 'preconnect',
|
|
13
|
+
href: 'https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&family=Open+Sans:wght@300;400;500;600;700;800&family=Poppins:wght@300;400;500;600;700;800&family=Roboto:wght@300;400;500;700;900&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap',
|
|
14
|
+
},
|
|
15
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { onCleanup } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solid custom directive that triggers a callback when a click occurs outside the
|
|
5
|
+
* bound element.
|
|
6
|
+
*
|
|
7
|
+
* @param el - The bound DOM element.
|
|
8
|
+
* @param accessor - Accessor returning the callback function to execute.
|
|
9
|
+
*/
|
|
10
|
+
export const clickOutside = (el: any, accessor: any) => {
|
|
11
|
+
const onClick = (e: any) => {
|
|
12
|
+
if (!el.contains(e.target)) {
|
|
13
|
+
accessor()?.();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
document.body.addEventListener('click', onClick);
|
|
17
|
+
onCleanup(() => document.body.removeEventListener('click', onClick));
|
|
18
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MouseOutParam } from './mouseOut';
|
|
2
|
+
|
|
3
|
+
export * from './clickOutside';
|
|
4
|
+
export * from './mouseOut';
|
|
5
|
+
export * from './resize';
|
|
6
|
+
|
|
7
|
+
declare module 'solid-js' {
|
|
8
|
+
// oxlint-disable-next-line typescript/no-namespace
|
|
9
|
+
namespace JSX {
|
|
10
|
+
interface Directives {
|
|
11
|
+
clickOutside: () => void;
|
|
12
|
+
mouseOut: MouseOutParam;
|
|
13
|
+
draggable: { skipTransform?: boolean };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createRoot } from 'solid-js';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { mouseOut } from './mouseOut';
|
|
5
|
+
|
|
6
|
+
describe('Directive => mouseOut', () => {
|
|
7
|
+
let el: HTMLDivElement;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.useFakeTimers();
|
|
11
|
+
el = document.createElement('div');
|
|
12
|
+
document.body.appendChild(el);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.useRealTimers();
|
|
17
|
+
el.remove();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('#01 => should trigger callback immediately when no delay is provided', () => {
|
|
21
|
+
const callback = vi.fn();
|
|
22
|
+
|
|
23
|
+
createRoot(dispose => {
|
|
24
|
+
mouseOut(el, () => callback);
|
|
25
|
+
|
|
26
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
27
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
28
|
+
|
|
29
|
+
dispose();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('#02 => should wait for the specified delay before executing the callback', () => {
|
|
34
|
+
const callback = vi.fn();
|
|
35
|
+
|
|
36
|
+
createRoot(dispose => {
|
|
37
|
+
mouseOut(el, () => [callback, 300]);
|
|
38
|
+
|
|
39
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
40
|
+
expect(callback).not.toHaveBeenCalled();
|
|
41
|
+
|
|
42
|
+
vi.advanceTimersByTime(200);
|
|
43
|
+
expect(callback).not.toHaveBeenCalled();
|
|
44
|
+
|
|
45
|
+
vi.advanceTimersByTime(100);
|
|
46
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
47
|
+
|
|
48
|
+
dispose();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('#03 => should reset/cancel the timer when user re-enters before delay finishes', () => {
|
|
53
|
+
const callback = vi.fn();
|
|
54
|
+
|
|
55
|
+
createRoot(dispose => {
|
|
56
|
+
mouseOut(el, () => ({ callback, delay: 500 }));
|
|
57
|
+
|
|
58
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
59
|
+
expect(callback).not.toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
vi.advanceTimersByTime(300);
|
|
62
|
+
expect(callback).not.toHaveBeenCalled();
|
|
63
|
+
|
|
64
|
+
// User re-enters
|
|
65
|
+
el.dispatchEvent(new MouseEvent('mouseenter'));
|
|
66
|
+
|
|
67
|
+
// Time passes past the original 500ms
|
|
68
|
+
vi.advanceTimersByTime(500);
|
|
69
|
+
expect(callback).not.toHaveBeenCalled();
|
|
70
|
+
|
|
71
|
+
dispose();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('#04 => should re-arm timer when mouse leaves again after re-entering', () => {
|
|
76
|
+
const callback = vi.fn();
|
|
77
|
+
|
|
78
|
+
createRoot(dispose => {
|
|
79
|
+
mouseOut(el, () => ({ onMouseOut: callback, delay: 400 }));
|
|
80
|
+
|
|
81
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
82
|
+
vi.advanceTimersByTime(200);
|
|
83
|
+
el.dispatchEvent(new MouseEvent('mouseenter'));
|
|
84
|
+
vi.advanceTimersByTime(300);
|
|
85
|
+
expect(callback).not.toHaveBeenCalled();
|
|
86
|
+
|
|
87
|
+
// Leave again
|
|
88
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
89
|
+
vi.advanceTimersByTime(400);
|
|
90
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
91
|
+
|
|
92
|
+
dispose();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('#05 => should cancel pending timer and remove event listeners on cleanup', () => {
|
|
97
|
+
const callback = vi.fn();
|
|
98
|
+
|
|
99
|
+
let disposeRoot!: () => void;
|
|
100
|
+
createRoot(dispose => {
|
|
101
|
+
disposeRoot = dispose;
|
|
102
|
+
mouseOut(el, () => [callback, 300]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
106
|
+
disposeRoot();
|
|
107
|
+
|
|
108
|
+
vi.advanceTimersByTime(400);
|
|
109
|
+
expect(callback).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
// Ensure listeners are removed
|
|
112
|
+
el.dispatchEvent(new MouseEvent('mouseleave'));
|
|
113
|
+
expect(callback).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { onCleanup } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Callback function type invoked when the mouse leaves the bound element.
|
|
5
|
+
*
|
|
6
|
+
* @param event - Optional mouse event that triggered the leave.
|
|
7
|
+
*/
|
|
8
|
+
export type MouseOutCallback = (event?: MouseEvent) => void;
|
|
9
|
+
|
|
10
|
+
/** Configuration options for the {@linkcode mouseOut} directive. */
|
|
11
|
+
export type MouseOutOptions = {
|
|
12
|
+
/** Callback function to trigger after the timer elapses. */
|
|
13
|
+
callback?: MouseOutCallback;
|
|
14
|
+
|
|
15
|
+
/** Alternative callback property name for triggering after the timer elapses. */
|
|
16
|
+
onMouseOut?: MouseOutCallback;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Delay in milliseconds before triggering the callback after the mouse leaves. If
|
|
20
|
+
* the cursor re-enters the element before this duration, the timer is reset.
|
|
21
|
+
*
|
|
22
|
+
* @defaultValue 0
|
|
23
|
+
*/
|
|
24
|
+
delay?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Accepted parameter shapes for the {@linkcode mouseOut} directive. */
|
|
28
|
+
export type MouseOutParam =
|
|
29
|
+
| MouseOutCallback
|
|
30
|
+
| [callback: MouseOutCallback, delay?: number]
|
|
31
|
+
| MouseOutOptions;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Solid custom directive that triggers a callback when the mouse leaves the bound
|
|
35
|
+
* element after a specified timer duration. If the user re-enters the component
|
|
36
|
+
* before the timer expires, the timer is reset/cancelled.
|
|
37
|
+
*
|
|
38
|
+
* @param el - The bound DOM element.
|
|
39
|
+
* @param accessor - Accessor returning a callback, tuple `[callback, delay]`, or
|
|
40
|
+
* type {@linkcode MouseOutOptions}.
|
|
41
|
+
*/
|
|
42
|
+
export const mouseOut = (
|
|
43
|
+
el: HTMLElement | Element,
|
|
44
|
+
accessor: () => MouseOutParam,
|
|
45
|
+
) => {
|
|
46
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
|
|
48
|
+
const clear = () => {
|
|
49
|
+
if (timer !== undefined) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
timer = undefined;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onMouseEnter = () => {
|
|
56
|
+
clear();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onMouseLeave = (event: MouseEvent) => {
|
|
60
|
+
clear();
|
|
61
|
+
|
|
62
|
+
const param = accessor();
|
|
63
|
+
if (!param) return;
|
|
64
|
+
|
|
65
|
+
let callback: MouseOutCallback | undefined;
|
|
66
|
+
let delay = 0;
|
|
67
|
+
|
|
68
|
+
if (typeof param === 'function') {
|
|
69
|
+
callback = param;
|
|
70
|
+
} else if (Array.isArray(param)) {
|
|
71
|
+
callback = param[0];
|
|
72
|
+
delay = param[1] ?? 0;
|
|
73
|
+
} else if (typeof param === 'object') {
|
|
74
|
+
callback = param.callback ?? param.onMouseOut;
|
|
75
|
+
delay = param.delay ?? 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!callback) return;
|
|
79
|
+
|
|
80
|
+
if (delay > 0) {
|
|
81
|
+
timer = setTimeout(() => {
|
|
82
|
+
callback?.(event);
|
|
83
|
+
timer = undefined;
|
|
84
|
+
}, delay);
|
|
85
|
+
} else {
|
|
86
|
+
callback(event);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
el.addEventListener('mouseleave', onMouseLeave as EventListener);
|
|
91
|
+
el.addEventListener('mouseenter', onMouseEnter as EventListener);
|
|
92
|
+
|
|
93
|
+
onCleanup(() => {
|
|
94
|
+
clear();
|
|
95
|
+
el.removeEventListener('mouseleave', onMouseLeave as EventListener);
|
|
96
|
+
el.removeEventListener('mouseenter', onMouseEnter as EventListener);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useFlow } from '../../components/FlowChart.context';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a ResizeObserver hook attached to the node content container element,
|
|
5
|
+
* dispatching RESIZE events to update node dimensions and edge positioning in
|
|
6
|
+
* real-time.
|
|
7
|
+
*
|
|
8
|
+
* @param id - Unique identifier of the flowchart node.
|
|
9
|
+
*
|
|
10
|
+
* @returns Ref callback attaching and cleaning up the observer on unmount.
|
|
11
|
+
*/
|
|
12
|
+
export const resize = (id: string) => (el: HTMLDivElement) => {
|
|
13
|
+
const service = useFlow();
|
|
14
|
+
|
|
15
|
+
const resize = new ResizeObserver(entries => {
|
|
16
|
+
const entry = entries[0].target;
|
|
17
|
+
if (!entry) return;
|
|
18
|
+
|
|
19
|
+
const width = entry.clientWidth;
|
|
20
|
+
const height = entry.clientHeight;
|
|
21
|
+
|
|
22
|
+
if (width > 0 && height > 0) {
|
|
23
|
+
service.send({ type: 'RESIZE', payload: { id, size: { width, height } } });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
resize.observe(el);
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
resize.unobserve(el);
|
|
31
|
+
resize.disconnect();
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NON_BREAKABLE_SPACE, VISIBLE_ESPACE } from '../constants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a zero-width space if string is empty, preserving layout flow.
|
|
5
|
+
*
|
|
6
|
+
* @param newText - The input text string.
|
|
7
|
+
*
|
|
8
|
+
* @returns The input text or zero-width visible space placeholder.
|
|
9
|
+
*/
|
|
10
|
+
export const espace = (newText: string) => {
|
|
11
|
+
if (newText === '') {
|
|
12
|
+
return VISIBLE_ESPACE;
|
|
13
|
+
}
|
|
14
|
+
return newText;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a repeated sequence of non-breakable space characters.
|
|
19
|
+
*
|
|
20
|
+
* @param count - The number of non-breakable spaces to repeat. Defaults to 1.
|
|
21
|
+
*
|
|
22
|
+
* @returns The non-breakable space string sequence.
|
|
23
|
+
*/
|
|
24
|
+
export const repeatSpace = (count = 1) => {
|
|
25
|
+
return NON_BREAKABLE_SPACE.repeat(count);
|
|
26
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
import { VISIBLE_ESPACE } from '../constants';
|
|
4
|
+
import { espace } from '../helpers/espace';
|
|
5
|
+
|
|
6
|
+
/** Typing animation options. */
|
|
7
|
+
type Props = {
|
|
8
|
+
/** The text content to type out. */
|
|
9
|
+
content: string;
|
|
10
|
+
/** Minimum delay interval between keystrokes in milliseconds. */
|
|
11
|
+
min: number;
|
|
12
|
+
/** Whether the typing effect rewinds after completion. */
|
|
13
|
+
rewind?: boolean;
|
|
14
|
+
/** Delay in milliseconds before rewinding or restarting. */
|
|
15
|
+
rewindDelay?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a reactive typewriter effect signal with oscillatory typing delay and
|
|
20
|
+
* optional rewind.
|
|
21
|
+
*
|
|
22
|
+
* @param props - Configuration properties of type {@linkcode Props}.
|
|
23
|
+
*
|
|
24
|
+
* @returns An object containing the current text signal, type trigger, and text
|
|
25
|
+
* setter.
|
|
26
|
+
*/
|
|
27
|
+
export const createTyping = ({ content, min, ...props }: Props) => {
|
|
28
|
+
const rewind = props.rewind ?? false;
|
|
29
|
+
const rewindDelay = (props as any).rewindDelay ?? 500;
|
|
30
|
+
|
|
31
|
+
const OSCILLATION = Math.min(min / 10 + content.length / 10, 10);
|
|
32
|
+
|
|
33
|
+
const [text, setText] = createSignal(VISIBLE_ESPACE);
|
|
34
|
+
let timeoutId: number | NodeJS.Timeout;
|
|
35
|
+
let index = 0;
|
|
36
|
+
let isTyping = true;
|
|
37
|
+
const max = min + OSCILLATION;
|
|
38
|
+
let interval = min;
|
|
39
|
+
let intervalDirection = 1; // 1 for incrementing, -1 for decrementing
|
|
40
|
+
|
|
41
|
+
const type = () => {
|
|
42
|
+
if (isTyping) {
|
|
43
|
+
if (index < content.length) {
|
|
44
|
+
setText(prev => prev + content[index]);
|
|
45
|
+
index++;
|
|
46
|
+
timeoutId = setTimeout(type, interval);
|
|
47
|
+
} else if (rewind) {
|
|
48
|
+
isTyping = false;
|
|
49
|
+
// interval = min;
|
|
50
|
+
intervalDirection = 1;
|
|
51
|
+
timeoutId = setTimeout(type, rewindDelay);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Rewind mode
|
|
55
|
+
if (index > 0) {
|
|
56
|
+
index--;
|
|
57
|
+
setText(espace(content.substring(0, index)));
|
|
58
|
+
timeoutId = setTimeout(type, interval);
|
|
59
|
+
} else {
|
|
60
|
+
isTyping = true;
|
|
61
|
+
// interval = min;
|
|
62
|
+
intervalDirection = 1;
|
|
63
|
+
timeoutId = setTimeout(type, rewindDelay);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update interval with oscillation
|
|
68
|
+
interval += intervalDirection;
|
|
69
|
+
|
|
70
|
+
if (interval >= max) {
|
|
71
|
+
intervalDirection = -1;
|
|
72
|
+
} else if (interval <= min) {
|
|
73
|
+
intervalDirection = 1;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
onCleanup(() => {
|
|
78
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return { text, type, setText };
|
|
82
|
+
};
|