@djangocfg/layouts 2.0.8 → 2.0.10

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.
@@ -1,16 +1,21 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useCallback, useRef } from 'react';
3
+ import { useEffect, useCallback, useRef, useState } from 'react';
4
+ import { useLocalStorage } from '@djangocfg/ui/hooks';
4
5
  import type { ChatDisplayMode } from '../types';
6
+ import { sidebarConfig, fabConfig, storageKeys } from '../config';
7
+
8
+ // Re-export for convenience
9
+ export const MIN_SIDEBAR_WIDTH = sidebarConfig.minWidth;
10
+ export const MAX_SIDEBAR_WIDTH = sidebarConfig.maxWidth;
11
+ export const DEFAULT_SIDEBAR_WIDTH = sidebarConfig.defaultWidth;
5
12
 
6
13
  /**
7
14
  * Configuration for chat layout management
8
15
  */
9
16
  export interface ChatLayoutConfig {
10
- /** Width of sidebar in pixels */
11
- sidebarWidth?: number;
12
- /** Z-index for chat elements */
13
- zIndex?: number;
17
+ /** Initial width of sidebar in pixels */
18
+ initialWidth?: number;
14
19
  /** Animation duration in ms */
15
20
  animationDuration?: number;
16
21
  /** Element to push (defaults to body) */
@@ -21,10 +26,18 @@ export interface ChatLayoutConfig {
21
26
  * Return type for useChatLayout hook
22
27
  */
23
28
  export interface UseChatLayoutReturn {
29
+ /** Current sidebar width */
30
+ sidebarWidth: number;
24
31
  /** Apply layout changes for mode */
25
32
  applyLayout: (mode: ChatDisplayMode) => void;
26
33
  /** Reset layout to default */
27
34
  resetLayout: () => void;
35
+ /** Update sidebar width (for resize) */
36
+ updateWidth: (width: number) => void;
37
+ /** Start resize operation */
38
+ startResize: (e: React.MouseEvent) => void;
39
+ /** Whether currently resizing */
40
+ isResizing: boolean;
28
41
  /** Get CSS for sidebar container */
29
42
  getSidebarStyles: () => React.CSSProperties;
30
43
  /** Get CSS for floating container */
@@ -34,9 +47,8 @@ export interface UseChatLayoutReturn {
34
47
  }
35
48
 
36
49
  const DEFAULT_CONFIG: Required<ChatLayoutConfig> = {
37
- sidebarWidth: 400,
38
- zIndex: 300,
39
- animationDuration: 200,
50
+ initialWidth: sidebarConfig.defaultWidth,
51
+ animationDuration: sidebarConfig.animationDuration,
40
52
  pushTarget: 'body',
41
53
  };
42
54
 
@@ -69,7 +81,22 @@ interface FixedElementOriginalStyles {
69
81
  */
70
82
  export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
71
83
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
72
- const { sidebarWidth, zIndex, animationDuration, pushTarget } = mergedConfig;
84
+ const { initialWidth, animationDuration, pushTarget } = mergedConfig;
85
+
86
+ // Sidebar width with localStorage persistence
87
+ const [storedWidth, setStoredWidth] = useLocalStorage<number>(storageKeys.sidebarWidth, initialWidth);
88
+
89
+ // Clamp stored width to valid range
90
+ const sidebarWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, storedWidth));
91
+ const sidebarWidthRef = useRef(sidebarWidth);
92
+
93
+ // Resizing state (runtime only, not persisted)
94
+ const [isResizing, setIsResizing] = useState(false);
95
+
96
+ // Keep ref in sync
97
+ useEffect(() => {
98
+ sidebarWidthRef.current = sidebarWidth;
99
+ }, [sidebarWidth]);
73
100
 
74
101
  // Store original styles for cleanup
75
102
  const originalStylesRef = useRef<{
@@ -157,6 +184,7 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
157
184
  */
158
185
  const adjustFixedElements = useCallback(
159
186
  (open: boolean) => {
187
+ const currentWidth = sidebarWidthRef.current;
160
188
  if (open) {
161
189
  // Save and adjust fixed elements
162
190
  const fixedElements = getFixedElements();
@@ -168,7 +196,7 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
168
196
 
169
197
  fixedElements.forEach((el) => {
170
198
  el.style.transition = `right ${animationDuration}ms ease`;
171
- el.style.right = `${sidebarWidth}px`;
199
+ el.style.right = `${currentWidth}px`;
172
200
  });
173
201
  } else {
174
202
  // Restore fixed elements
@@ -185,7 +213,7 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
185
213
  fixedElementsRef.current = [];
186
214
  }
187
215
  },
188
- [getFixedElements, sidebarWidth, animationDuration]
216
+ [getFixedElements, animationDuration]
189
217
  );
190
218
 
191
219
  /**
@@ -195,11 +223,13 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
195
223
  const target = getTargetElement();
196
224
  if (!target) return;
197
225
 
226
+ const currentWidth = sidebarWidthRef.current;
227
+
198
228
  saveOriginalStyles(target);
199
229
 
200
230
  // Add smooth transition
201
231
  target.style.transition = `margin-right ${animationDuration}ms ease`;
202
- target.style.marginRight = `${sidebarWidth}px`;
232
+ target.style.marginRight = `${currentWidth}px`;
203
233
  target.style.overflowX = 'hidden';
204
234
  target.setAttribute('data-chat-sidebar', 'open');
205
235
 
@@ -207,7 +237,7 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
207
237
  adjustFixedElements(true);
208
238
 
209
239
  currentModeRef.current = 'sidebar';
210
- }, [getTargetElement, saveOriginalStyles, sidebarWidth, animationDuration, adjustFixedElements]);
240
+ }, [getTargetElement, saveOriginalStyles, animationDuration, adjustFixedElements]);
211
241
 
212
242
  /**
213
243
  * Apply floating/closed mode layout (reset sidebar push)
@@ -268,6 +298,83 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
268
298
  currentModeRef.current = 'closed';
269
299
  }, [getTargetElement, restoreOriginalStyles]);
270
300
 
301
+ /**
302
+ * Update width during resize (no animation)
303
+ */
304
+ const updateWidthImmediate = useCallback(
305
+ (newWidth: number) => {
306
+ const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, newWidth));
307
+
308
+ // Update body margin
309
+ const target = getTargetElement();
310
+ if (target && currentModeRef.current === 'sidebar') {
311
+ target.style.transition = 'none';
312
+ target.style.marginRight = `${clampedWidth}px`;
313
+ }
314
+
315
+ // Update fixed elements
316
+ fixedElementsRef.current.forEach(({ element }) => {
317
+ element.style.transition = 'none';
318
+ element.style.right = `${clampedWidth}px`;
319
+ });
320
+
321
+ return clampedWidth;
322
+ },
323
+ [getTargetElement]
324
+ );
325
+
326
+ /**
327
+ * Update sidebar width (for resize)
328
+ */
329
+ const updateWidth = useCallback(
330
+ (newWidth: number) => {
331
+ const clampedWidth = updateWidthImmediate(newWidth);
332
+ setStoredWidth(clampedWidth);
333
+ },
334
+ [updateWidthImmediate, setStoredWidth]
335
+ );
336
+
337
+ /**
338
+ * Start resize operation
339
+ */
340
+ const startResize = useCallback(
341
+ (e: React.MouseEvent) => {
342
+ e.preventDefault();
343
+ setIsResizing(true);
344
+
345
+ const startX = e.clientX;
346
+ const startWidth = sidebarWidthRef.current;
347
+
348
+ const handleMouseMove = (moveEvent: MouseEvent) => {
349
+ // Calculate new width (dragging left increases width)
350
+ const deltaX = startX - moveEvent.clientX;
351
+ const newWidth = startWidth + deltaX;
352
+ const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, newWidth));
353
+
354
+ // Update DOM immediately for smooth feel
355
+ updateWidthImmediate(clampedWidth);
356
+ sidebarWidthRef.current = clampedWidth;
357
+
358
+ // Also update React state so sidebar visually resizes
359
+ setStoredWidth(clampedWidth);
360
+ };
361
+
362
+ const handleMouseUp = () => {
363
+ setIsResizing(false);
364
+ document.removeEventListener('mousemove', handleMouseMove);
365
+ document.removeEventListener('mouseup', handleMouseUp);
366
+ document.body.style.cursor = '';
367
+ document.body.style.userSelect = '';
368
+ };
369
+
370
+ document.addEventListener('mousemove', handleMouseMove);
371
+ document.addEventListener('mouseup', handleMouseUp);
372
+ document.body.style.cursor = 'ew-resize';
373
+ document.body.style.userSelect = 'none';
374
+ },
375
+ [updateWidthImmediate, setStoredWidth]
376
+ );
377
+
271
378
  /**
272
379
  * Get CSS styles for sidebar container
273
380
  */
@@ -278,9 +385,9 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
278
385
  right: 0,
279
386
  bottom: 0,
280
387
  width: `${sidebarWidth}px`,
281
- zIndex,
388
+ zIndex: sidebarConfig.zIndex,
282
389
  };
283
- }, [sidebarWidth, zIndex]);
390
+ }, [sidebarWidth]);
284
391
 
285
392
  /**
286
393
  * Get CSS styles for floating container
@@ -289,12 +396,12 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
289
396
  (position: 'bottom-right' | 'bottom-left'): React.CSSProperties => {
290
397
  return {
291
398
  position: 'fixed',
292
- zIndex: zIndex - 50, // Slightly lower than sidebar
293
- bottom: 56, // Above banner
294
- ...(position === 'bottom-right' ? { right: 16 } : { left: 16 }),
399
+ zIndex: sidebarConfig.zIndex - 50,
400
+ bottom: fabConfig.bottom,
401
+ ...(position === 'bottom-right' ? { right: fabConfig.right } : { left: fabConfig.right }),
295
402
  };
296
403
  },
297
- [zIndex]
404
+ []
298
405
  );
299
406
 
300
407
  /**
@@ -304,12 +411,12 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
304
411
  (position: 'bottom-right' | 'bottom-left'): React.CSSProperties => {
305
412
  return {
306
413
  position: 'fixed',
307
- zIndex: zIndex - 50,
308
- bottom: 56,
309
- ...(position === 'bottom-right' ? { right: 16 } : { left: 16 }),
414
+ zIndex: sidebarConfig.zIndex - 50,
415
+ bottom: fabConfig.bottom,
416
+ ...(position === 'bottom-right' ? { right: fabConfig.right } : { left: fabConfig.right }),
310
417
  };
311
418
  },
312
- [zIndex]
419
+ []
313
420
  );
314
421
 
315
422
  // Cleanup on unmount
@@ -320,8 +427,12 @@ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
320
427
  }, [resetLayout]);
321
428
 
322
429
  return {
430
+ sidebarWidth,
323
431
  applyLayout,
324
432
  resetLayout,
433
+ updateWidth,
434
+ startResize,
435
+ isResizing,
325
436
  getSidebarStyles,
326
437
  getFloatingStyles,
327
438
  getFabStyles,
@@ -46,7 +46,6 @@ export { ChatWidget, AIChatWidget, ChatPanel, MessageBubble, AIMessageInput } fr
46
46
  export type {
47
47
  ChatWidgetProps,
48
48
  AIChatWidgetProps,
49
- ChatPanelProps,
50
49
  MessageBubbleProps,
51
50
  AIMessageInputProps,
52
51
  } from './components';
@@ -24,7 +24,6 @@ export {
24
24
  } from './McpChat';
25
25
  export type {
26
26
  AIChatWidgetProps,
27
- ChatPanelProps,
28
27
  MessageBubbleProps,
29
28
  AIMessageInputProps,
30
29
  AIChatContextState,