@bemedev/mind-flow 0.3.2 → 1.1.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.
Files changed (115) hide show
  1. package/README.md +55 -12
  2. package/lib/helpers/clamp.d.ts +2 -0
  3. package/lib/helpers/clamp.d.ts.map +1 -0
  4. package/lib/helpers/createContext.d.ts +1 -1
  5. package/lib/helpers/createContext.d.ts.map +1 -1
  6. package/lib/helpers/index.d.ts +3 -0
  7. package/lib/helpers/index.d.ts.map +1 -0
  8. package/lib/index.cjs.js +1 -1
  9. package/lib/index.d.ts +9 -1
  10. package/lib/index.d.ts.map +1 -1
  11. package/lib/index.es.js +2464 -373
  12. package/lib/output.css +1 -1
  13. package/lib/server.cjs.js +1002 -262
  14. package/lib/server.es.js +968 -272
  15. package/lib/services/main.machine.d.ts +1564 -667
  16. package/lib/services/main.machine.d.ts.map +1 -1
  17. package/lib/services/main.machine.data.d.ts +14 -1
  18. package/lib/services/main.machine.data.d.ts.map +1 -1
  19. package/lib/services/main.machine.helpers.d.ts +1 -1
  20. package/lib/services/main.machine.helpers.d.ts.map +1 -1
  21. package/lib/services/main.machine.test.d.ts +2 -0
  22. package/lib/services/main.machine.test.d.ts.map +1 -0
  23. package/lib/services/main.machine.typings.d.ts +26 -8
  24. package/lib/services/main.machine.typings.d.ts.map +1 -1
  25. package/lib/ui/Flow.d.ts +8 -4
  26. package/lib/ui/Flow.d.ts.map +1 -1
  27. package/lib/ui/components/Bounds.d.ts.map +1 -1
  28. package/lib/ui/components/EdgeComponent.d.ts.map +1 -1
  29. package/lib/ui/components/EdgesBoard.d.ts.map +1 -1
  30. package/lib/ui/components/EditPanel.d.ts +19 -0
  31. package/lib/ui/components/EditPanel.d.ts.map +1 -0
  32. package/lib/ui/components/EditPanel.hooks.d.ts +29 -0
  33. package/lib/ui/components/EditPanel.hooks.d.ts.map +1 -0
  34. package/lib/ui/components/EditPanel.types.d.ts +47 -0
  35. package/lib/ui/components/EditPanel.types.d.ts.map +1 -0
  36. package/lib/ui/components/FlowChart.context.d.ts +1563 -666
  37. package/lib/ui/components/FlowChart.context.d.ts.map +1 -1
  38. package/lib/ui/components/FlowChart.d.ts +8 -47
  39. package/lib/ui/components/FlowChart.d.ts.map +1 -1
  40. package/lib/ui/components/FlowChart.types.d.ts +87 -0
  41. package/lib/ui/components/FlowChart.types.d.ts.map +1 -0
  42. package/lib/ui/components/NodeComponent.d.ts +10 -12
  43. package/lib/ui/components/NodeComponent.d.ts.map +1 -1
  44. package/lib/ui/components/NodesBoard.d.ts +17 -2
  45. package/lib/ui/components/NodesBoard.d.ts.map +1 -1
  46. package/lib/ui/components/Panels.d.ts +12 -0
  47. package/lib/ui/components/Panels.d.ts.map +1 -0
  48. package/lib/ui/globals/components/atoms/TypingText.d.ts +29 -0
  49. package/lib/ui/globals/components/atoms/TypingText.d.ts.map +1 -0
  50. package/lib/ui/globals/components/molecules/FadingDots.d.ts +21 -0
  51. package/lib/ui/globals/components/molecules/FadingDots.d.ts.map +1 -0
  52. package/lib/ui/globals/components/organisms/LoadingFallback.d.ts +46 -0
  53. package/lib/ui/globals/components/organisms/LoadingFallback.d.ts.map +1 -0
  54. package/lib/ui/globals/constants/index.d.ts +3 -0
  55. package/lib/ui/globals/constants/index.d.ts.map +1 -0
  56. package/lib/ui/globals/constants/links.d.ts +4 -0
  57. package/lib/ui/globals/constants/links.d.ts.map +1 -0
  58. package/lib/ui/globals/constants/strings.d.ts +5 -0
  59. package/lib/ui/globals/constants/strings.d.ts.map +1 -0
  60. package/lib/ui/globals/directives/clickOutside.d.ts +9 -0
  61. package/lib/ui/globals/directives/clickOutside.d.ts.map +1 -0
  62. package/lib/ui/globals/directives/index.d.ts +16 -0
  63. package/lib/ui/globals/directives/index.d.ts.map +1 -0
  64. package/lib/ui/globals/directives/mouseOut.d.ts +33 -0
  65. package/lib/ui/globals/directives/mouseOut.d.ts.map +1 -0
  66. package/lib/ui/globals/directives/mouseOut.test.d.ts +2 -0
  67. package/lib/ui/globals/directives/mouseOut.test.d.ts.map +1 -0
  68. package/lib/ui/globals/directives/resize.d.ts +11 -0
  69. package/lib/ui/globals/directives/resize.d.ts.map +1 -0
  70. package/lib/ui/globals/helpers/espace.d.ts +17 -0
  71. package/lib/ui/globals/helpers/espace.d.ts.map +1 -0
  72. package/lib/ui/globals/signals/createTyping.d.ts +27 -0
  73. package/lib/ui/globals/signals/createTyping.d.ts.map +1 -0
  74. package/lib/ui/globals/types.d.ts +54 -0
  75. package/lib/ui/globals/types.d.ts.map +1 -0
  76. package/lib/ui/utils.d.ts +11 -0
  77. package/lib/ui/utils.d.ts.map +1 -0
  78. package/package.json +2 -3
  79. package/src/helpers/clamp.ts +3 -0
  80. package/src/helpers/createContext.ts +20 -4
  81. package/src/helpers/index.ts +2 -0
  82. package/src/index.ts +9 -1
  83. package/src/services/main.machine.data.ts +13 -2
  84. package/src/services/main.machine.helpers.ts +12 -4
  85. package/src/services/main.machine.test.ts +205 -0
  86. package/src/services/main.machine.ts +136 -65
  87. package/src/services/main.machine.typings.ts +31 -6
  88. package/src/ui/Flow.tsx +47 -8
  89. package/src/ui/components/Bounds.tsx +0 -2
  90. package/src/ui/components/EdgeComponent.tsx +60 -46
  91. package/src/ui/components/EdgesBoard.tsx +1 -2
  92. package/src/ui/components/EditPanel.hooks.ts +69 -0
  93. package/src/ui/components/EditPanel.tsx +89 -0
  94. package/src/ui/components/EditPanel.types.ts +49 -0
  95. package/src/ui/components/FlowChart.context.ts +21 -11
  96. package/src/ui/components/FlowChart.tsx +20 -57
  97. package/src/ui/components/FlowChart.types.ts +92 -0
  98. package/src/ui/components/NodeComponent.tsx +81 -58
  99. package/src/ui/components/NodesBoard.tsx +29 -7
  100. package/src/ui/components/Panels.tsx +31 -0
  101. package/src/ui/globals/components/atoms/TypingText.tsx +63 -0
  102. package/src/ui/globals/components/molecules/FadingDots.tsx +45 -0
  103. package/src/ui/globals/components/organisms/LoadingFallback.tsx +116 -0
  104. package/src/ui/globals/constants/index.ts +2 -0
  105. package/src/ui/globals/constants/links.ts +15 -0
  106. package/src/ui/globals/constants/strings.ts +5 -0
  107. package/src/ui/globals/directives/clickOutside.ts +18 -0
  108. package/src/ui/globals/directives/index.ts +16 -0
  109. package/src/ui/globals/directives/mouseOut.test.ts +115 -0
  110. package/src/ui/globals/directives/mouseOut.ts +98 -0
  111. package/src/ui/globals/directives/resize.ts +33 -0
  112. package/src/ui/globals/helpers/espace.ts +26 -0
  113. package/src/ui/globals/signals/createTyping.ts +82 -0
  114. package/src/ui/globals/types.ts +83 -0
  115. package/src/ui/utils.ts +14 -0
@@ -1,43 +1,44 @@
1
- /* eslint-disable @typescript-eslint/no-namespace */
2
1
  import { toArray } from '@bemedev/app';
3
2
  import { createState } from '@bemedev/app-solidjs';
4
3
  import { createDraggable } from '@thisbeyond/solid-dnd';
5
4
  import { dequal } from 'dequal';
6
- import { Component, Show } from 'solid-js';
5
+ import { type Component, For, type JSX, Show } from 'solid-js';
6
+ import { Dynamic } from 'solid-js/web';
7
7
 
8
8
  import {
9
9
  HANDLE_CONTAINER_OFFSET_X,
10
10
  HANDLE_MARGIN_TOP,
11
11
  HANDLE_SIZE,
12
12
  } from '#services/main.machine.data';
13
+ import type { NodeData } from '#services/main.machine.typings';
13
14
 
15
+ import { resize } from '../globals/directives';
14
16
  import { useFlow } from './FlowChart.context';
15
17
 
16
- declare module 'solid-js' {
17
- namespace JSX {
18
- interface Directives {
19
- draggable: any;
20
- }
21
- }
22
- }
23
-
24
18
  /** Properties for rendering an individual flowchart node component. */
25
- type Props = {
19
+ export type NodeComponentProps<D extends NodeData = NodeData> = {
26
20
  /** Unique identifier of the node. */
27
21
  id: string;
22
+ /** Custom node component to render inside the node container. */
23
+ children?: Component<D>;
28
24
  };
29
25
 
30
26
  /**
31
27
  * Interactive flowchart node component supporting dragging, selection, handle
32
28
  * connections, and child/sibling creation.
33
29
  *
34
- * @param props - Node rendering properties of type {@linkcode Props}.
30
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
31
+ * {@linkcode NodeData}.
32
+ *
33
+ * @param props - Node rendering properties of type {@linkcode NodeComponentProps}.
35
34
  *
36
35
  * @returns The rendered Solid component.
37
36
  *
38
37
  * @see {@linkcode useFlow}, {@linkcode HANDLE_CONTAINER_OFFSET_X}, {@linkcode HANDLE_MARGIN_TOP}, {@linkcode HANDLE_SIZE}
39
38
  */
40
- export const NodeComponent: Component<Props> = props => {
39
+ export const NodeComponent = <D extends NodeData = NodeData>(
40
+ props: NodeComponentProps<D>,
41
+ ): JSX.Element => {
41
42
  const service = useFlow();
42
43
  const newEdge = createState(service, { selector: s => s.context.newEdge });
43
44
  const draggable = createDraggable(props.id);
@@ -46,13 +47,11 @@ export const NodeComponent: Component<Props> = props => {
46
47
  const node = createState(service, {
47
48
  selector: ({ context }) => {
48
49
  const list = toArray.typed(context.data?.nodes);
49
- const item = list.find(n => n.id === props.id)!;
50
+ const item = list.find(n => n.id === props.id);
50
51
  return {
51
- x: item.position.x,
52
- y: item.position.y,
53
- label: item.data.label,
54
- content: item.data.content ?? '',
55
- input: item.input,
52
+ x: item?.position.x ?? 0,
53
+ y: item?.position.y ?? 0,
54
+ data: (item?.data ?? {}) as D,
56
55
  };
57
56
  },
58
57
  equals: dequal,
@@ -74,8 +73,8 @@ export const NodeComponent: Component<Props> = props => {
74
73
  <div
75
74
  classList={{
76
75
  'flex flex-col absolute cursor-grab bg-white rounded-md shadow-md select-none transition-[border,box-shadow] duration-200 ease-in-out hover:shadow-lg draggable': true,
77
- 'border border-[#e38c29] z-[100]': selected(),
78
- 'border border-[#e6d4be] z-[1]': !selected(),
76
+ 'border border-[#e38c29] z-100': selected(),
77
+ 'border border-[#e6d4be] z-1': !selected(),
79
78
  }}
80
79
 
81
80
  style={{ top: `${node().y}px`, left: `${node().x}px` }}
@@ -87,6 +86,10 @@ export const NodeComponent: Component<Props> = props => {
87
86
  e.stopPropagation();
88
87
  service.send({ type: 'SELECT', payload: props.id });
89
88
  }}
89
+ onDblClick={e => {
90
+ e.stopPropagation();
91
+ service.send({ type: 'EDIT', payload: props.id });
92
+ }}
90
93
  >
91
94
  <div
92
95
  classList={{
@@ -161,49 +164,69 @@ export const NodeComponent: Component<Props> = props => {
161
164
  </svg>
162
165
  </div>
163
166
 
164
- <Show when={node().label} keyed>
165
- {label => (
166
- <span class='min-w-max border-b border-[#f0f0f0] p-3 whitespace-nowrap text-red-600 select-none'>
167
- {label}
168
- </span>
169
- )}
170
- </Show>
171
-
172
- <Show when={node().content} keyed>
173
- {content => <div class='p-3 select-none'>{content}</div>}
174
- </Show>
167
+ <div ref={resize(props.id)}>
168
+ <Show
169
+ when={props.children}
170
+ fallback={
171
+ <div class='p-3 select-none'>
172
+ <Show when={node().data.label} keyed>
173
+ {label => (
174
+ <span class='mb-2 block min-w-max border-b border-[#f0f0f0] pb-2 font-semibold whitespace-nowrap text-red-600 select-none'>
175
+ {String(label)}
176
+ </span>
177
+ )}
178
+ </Show>
179
+
180
+ <Show when={node().data.content} keyed>
181
+ {content => <div class='select-none'>{String(content)}</div>}
182
+ </Show>
183
+
184
+ <Show when={!node().data.label && !node().data.content}>
185
+ <For each={Object.entries(node().data)}>
186
+ {([key, val]) => (
187
+ <div>
188
+ <span class='font-semibold'>{key}: </span>
189
+ <span>{String(val)}</span>
190
+ </div>
191
+ )}
192
+ </For>
193
+ </Show>
194
+ </div>
195
+ }
196
+ >
197
+ <Dynamic component={props.children} {...node().data} />
198
+ </Show>
199
+ </div>
175
200
 
176
- <Show when={node().input || hasParent()}>
201
+ <div
202
+ id='inputs'
203
+ class='pointer-events-none absolute top-0 z-10 flex cursor-default flex-col'
204
+ style={{ left: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
205
+ >
177
206
  <div
178
- id='inputs'
179
- class='pointer-events-none absolute top-0 z-10 flex cursor-default flex-col'
180
- style={{ left: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
181
- >
182
- <div
183
- class='cursor-default rounded-full bg-[#e38b29] shadow-md'
207
+ class='cursor-default rounded-full bg-[#e38b29] shadow-md'
184
208
 
185
- style={{
186
- width: `${HANDLE_SIZE}px`,
187
- height: `${HANDLE_SIZE}px`,
188
- 'margin-top': `${HANDLE_MARGIN_TOP}px`,
189
- 'pointer-events': 'all',
190
- }}
209
+ style={{
210
+ width: `${HANDLE_SIZE}px`,
211
+ height: `${HANDLE_SIZE}px`,
212
+ 'margin-top': `${HANDLE_MARGIN_TOP}px`,
213
+ 'pointer-events': 'all',
214
+ }}
191
215
 
192
- onMouseDown={event => {
193
- event.stopPropagation();
194
- }}
216
+ onMouseDown={event => {
217
+ event.stopPropagation();
218
+ }}
195
219
 
196
- onMouseUp={event => {
197
- event.stopPropagation();
198
- const from = newEdge()?.from;
220
+ onMouseUp={event => {
221
+ event.stopPropagation();
222
+ const from = newEdge()?.from;
199
223
 
200
- if (from) {
201
- service.send({ type: 'ADD_EDGE', payload: { from, to: props.id } });
202
- } else service.send('CLEAR_NEW_EDGE');
203
- }}
204
- ></div>
205
- </div>
206
- </Show>
224
+ if (from) {
225
+ service.send({ type: 'ADD_EDGE', payload: { from, to: props.id } });
226
+ } else service.send('CLEAR_NEW_EDGE');
227
+ }}
228
+ ></div>
229
+ </div>
207
230
  <div
208
231
  id='outputs'
209
232
  class='pointer-events-none absolute top-0 z-10 flex flex-col'
@@ -7,37 +7,55 @@ import {
7
7
  } from '@thisbeyond/solid-dnd';
8
8
  import { dequal } from 'dequal';
9
9
  import {
10
- Component,
10
+ type Component,
11
11
  createEffect,
12
12
  createSignal,
13
13
  For,
14
+ type JSX,
14
15
  on,
15
16
  onMount,
16
17
  Show,
17
18
  } from 'solid-js';
18
19
 
19
20
  import { CANVAS_FACTOR, SCROLL_MULTIPLIER } from '../../services/main.machine.data';
21
+ import type { NodeData } from '../../services/main.machine.typings';
20
22
  import { DragBounds } from './Bounds';
21
23
  import { EdgesBoard } from './EdgesBoard';
22
24
  import { useFlow } from './FlowChart.context';
25
+ import type { FlowPanels } from './FlowChart.types';
23
26
  import { NodeComponent } from './NodeComponent';
27
+ import { Panels } from './Panels';
28
+
29
+ /** Properties for the {@linkcode NodesBoard} component. */
30
+ type Props<D extends NodeData = NodeData> = {
31
+ /** Optional custom node component. */
32
+ component?: Component<D>;
33
+ /** Optional custom overlay panels. */
34
+ panels?: FlowPanels;
35
+ };
24
36
 
25
37
  /**
26
38
  * Interactive board component containing the drag-drop viewport, zoom controls,
27
39
  * panning gestures, and rendered nodes/edges.
28
40
  *
41
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
42
+ * {@linkcode NodeData}.
43
+ *
44
+ * @param props - Board component properties of type {@linkcode Props}.
45
+ *
29
46
  * @returns The rendered Solid component.
30
47
  *
31
48
  * @see {@linkcode DragBounds}, {@linkcode EdgesBoard}, {@linkcode NodeComponent}, {@linkcode useFlow}, {@linkcode CANVAS_FACTOR}, {@linkcode SCROLL_MULTIPLIER}
32
49
  */
33
- export const NodesBoard: Component = () => {
50
+ export const NodesBoard = <D extends NodeData = NodeData>(
51
+ props: Props<D>,
52
+ ): JSX.Element => {
34
53
  let containerRef: HTMLDivElement;
35
54
  const [isPanning, setIsPanning] = createSignal(false);
36
55
  const [transform, setTransform] = createSignal({ x: 0, y: 0 });
37
56
  const [ref, setRef] = createSignal<HTMLDivElement | undefined>();
38
57
  let percentX = 0;
39
58
  let percentY = 0;
40
-
41
59
  const service = useFlow();
42
60
 
43
61
  const newEdge = createState(service, {
@@ -135,7 +153,7 @@ export const NodesBoard: Component = () => {
135
153
  }
136
154
  }}
137
155
 
138
- class='relative mx-auto h-[calc(100vh-64px)] w-[calc(100vw-32px)] overflow-hidden'
156
+ class='relative mx-auto h-full w-full overflow-hidden'
139
157
  >
140
158
  <DragDropProvider
141
159
  onDragMove={({ draggable: { transform: _transform, node, id } }) => {
@@ -203,7 +221,7 @@ export const NodesBoard: Component = () => {
203
221
  return (containerRef = el);
204
222
  }}
205
223
  onScroll={updateScrollPercentages}
206
- class='relative h-full w-full overflow-auto rounded-lg border-2 border-gray-600'
224
+ class='relative h-full w-full overflow-auto'
207
225
  >
208
226
  <DragDropSensors />
209
227
  <div
@@ -251,12 +269,16 @@ export const NodesBoard: Component = () => {
251
269
  >
252
270
  <DragBounds />
253
271
  <EdgesBoard />
254
- <For each={nodeIds()}>{id => <NodeComponent id={id} />}</For>
272
+ <For each={nodeIds()}>
273
+ {id => <NodeComponent id={id} children={props.component} />}
274
+ </For>
255
275
  </div>
256
276
  </div>
257
277
  </div>
258
278
 
259
- {/* Panel */}
279
+ <Panels {...props.panels} />
280
+
281
+ {/* Bottom-Right Panel (Main Zoom & Creation Controls) */}
260
282
  <div class='absolute right-4 bottom-4 z-50 flex items-center gap-2 rounded-xl border border-gray-200 bg-white/90 p-2 shadow-lg backdrop-blur-md'>
261
283
  <button
262
284
  type='button'
@@ -0,0 +1,31 @@
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 => <div class='absolute top-4 left-4 z-50'>{<Panel />}</div>}
18
+ </Show>
19
+
20
+ {/* Top-Right Panel */}
21
+ <Show when={props.topRight} keyed>
22
+ {Panel => <div class='absolute top-4 right-4 z-50'>{<Panel />}</div>}
23
+ </Show>
24
+
25
+ {/* Bottom-Left Panel */}
26
+ <Show when={props.bottomLeft} keyed>
27
+ {Panel => <div class='absolute bottom-4 left-4 z-50'>{<Panel />}</div>}
28
+ </Show>
29
+ </>
30
+ );
31
+ };
@@ -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,2 @@
1
+ export * from './strings';
2
+ export * from './links';
@@ -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,5 @@
1
+ /** Zero-width space character used for layout preservation in typing animations. */
2
+ export const VISIBLE_ESPACE = '\u200B';
3
+
4
+ /** Non-breakable space character string. */
5
+ export const NON_BREAKABLE_SPACE = '\xa0';
@@ -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
+ }