@amaster.ai/components-templates 1.6.0 → 1.10.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 (28) hide show
  1. package/components/ai-assistant/package.json +10 -12
  2. package/components/ai-assistant/template/ai-assistant.tsx +48 -7
  3. package/components/ai-assistant/template/components/chat-assistant-message.tsx +78 -7
  4. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +35 -11
  5. package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -1
  6. package/components/ai-assistant/template/components/chat-floating-card.tsx +49 -3
  7. package/components/ai-assistant/template/components/chat-header.tsx +1 -1
  8. package/components/ai-assistant/template/components/chat-input.tsx +57 -22
  9. package/components/ai-assistant/template/components/chat-messages.tsx +118 -25
  10. package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
  11. package/components/ai-assistant/template/components/voice-input.tsx +11 -2
  12. package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
  13. package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
  14. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +52 -5
  15. package/components/ai-assistant/template/hooks/useDraggable.ts +11 -3
  16. package/components/ai-assistant/template/hooks/usePosition.ts +19 -31
  17. package/components/ai-assistant/template/i18n.ts +8 -0
  18. package/components/ai-assistant/template/types.ts +2 -0
  19. package/components/ai-assistant-taro/package.json +16 -8
  20. package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +24 -2
  21. package/components/ai-assistant-taro/template/components/ChatInput.tsx +50 -28
  22. package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
  23. package/components/ai-assistant-taro/template/components/markdown.tsx +343 -137
  24. package/components/ai-assistant-taro/template/hooks/useConversation.ts +542 -424
  25. package/components/ai-assistant-taro/template/index.tsx +2 -2
  26. package/components/ai-assistant-taro/template/types.ts +16 -0
  27. package/package.json +1 -1
  28. package/packages/cli/package.json +1 -1
@@ -1,7 +1,11 @@
1
1
  import type React from "react";
2
- import { useEffect, useRef, useCallback } from "react";
3
- import type { Conversation } from "../types";
4
- import ChatAssistantMessage, { ChatDivider } from "./chat-assistant-message";
2
+ import { useEffect, useRef, useCallback, useMemo } from "react";
3
+ import type { Conversation, MessagesItem } from "../types";
4
+ import ChatAssistantMessage, {
5
+ ChatAssistantCollapsedGroup,
6
+ ChatDivider,
7
+ isCollapsibleAssistantMessage,
8
+ } from "./chat-assistant-message";
5
9
  import ChatUserMessage from "./chat-user-message";
6
10
  import { cn } from "@/lib/utils";
7
11
  import { LoaderCircle } from "lucide-react";
@@ -18,6 +22,77 @@ interface ChatMessagesProps {
18
22
  onLoadMore?: () => void;
19
23
  }
20
24
 
25
+ type RenderableMessageItem =
26
+ | {
27
+ type: "message";
28
+ message: MessagesItem;
29
+ key: string;
30
+ isNewest: boolean;
31
+ showAvatar: boolean;
32
+ }
33
+ | {
34
+ type: "group";
35
+ messages: MessagesItem[];
36
+ key: string;
37
+ isNewest: boolean;
38
+ showAvatar: boolean;
39
+ };
40
+
41
+ interface RenderableConversation {
42
+ taskId: string;
43
+ addDivider: boolean | undefined;
44
+ items: RenderableMessageItem[];
45
+ }
46
+
47
+ const buildRenderableMessages = (messages: MessagesItem[]): RenderableMessageItem[] => {
48
+ const items: RenderableMessageItem[] = [];
49
+
50
+ for (let index = 0; index < messages.length; index += 1) {
51
+ const message = messages[index];
52
+ const key = message.messageId || `message-${index}`;
53
+
54
+ if (!isCollapsibleAssistantMessage(message)) {
55
+ items.push({
56
+ type: "message",
57
+ message,
58
+ key,
59
+ isNewest: index === messages.length - 1,
60
+ showAvatar: index === 0 || messages[index - 1].role !== "assistant",
61
+ });
62
+ continue;
63
+ }
64
+
65
+ const startIndex = index;
66
+ const groupedMessages: MessagesItem[] = [message];
67
+
68
+ while (
69
+ index + 1 < messages.length &&
70
+ isCollapsibleAssistantMessage(messages[index + 1])
71
+ ) {
72
+ groupedMessages.push(messages[index + 1]);
73
+ index += 1;
74
+ }
75
+
76
+ items.push({
77
+ ...(groupedMessages.length > 1
78
+ ? {
79
+ type: "group" as const,
80
+ messages: groupedMessages,
81
+ }
82
+ : {
83
+ type: "message" as const,
84
+ message: groupedMessages[0],
85
+ }),
86
+ key,
87
+ isNewest: index === messages.length - 1,
88
+ showAvatar:
89
+ startIndex === 0 || messages[startIndex - 1].role !== "assistant",
90
+ });
91
+ }
92
+
93
+ return items;
94
+ };
95
+
21
96
  const ChatMessages: React.FC<ChatMessagesProps> = ({
22
97
  conversations,
23
98
  isLoading,
@@ -60,6 +135,23 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
60
135
  }, [handleScroll, scrollAreaRef]);
61
136
 
62
137
  const convLength = conversations.length;
138
+ const renderableConversations = useMemo<RenderableConversation[]>(
139
+ () =>
140
+ conversations.map((conversation, index) => {
141
+ const historyId = conversation.historyId || "";
142
+ const lastHistoryId = conversations[index - 1]?.historyId || "";
143
+ const addDivider =
144
+ (index > 0 && historyId !== lastHistoryId) ||
145
+ conversation.system?.level === "newConversation";
146
+
147
+ return {
148
+ taskId: conversation.taskId,
149
+ addDivider,
150
+ items: buildRenderableMessages(conversation.messages),
151
+ };
152
+ }),
153
+ [conversations],
154
+ );
63
155
 
64
156
  return (
65
157
  <div
@@ -80,7 +172,7 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
80
172
  }}
81
173
  data-role="chat-messages"
82
174
  >
83
- {isLoadingHistory ? (
175
+ {isLoadingHistory && convLength > 0 ? (
84
176
  <div
85
177
  key="loading-history"
86
178
  className="flex justify-center items-center gap-2 text-center"
@@ -96,37 +188,38 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
96
188
  </div>
97
189
  ) : null}
98
190
 
99
- {conversations.map((conversation, index) => {
100
- const len = conversation.messages.length;
101
- const historyId = conversation.historyId || "";
102
- const lastHistoryId = conversations[index - 1]?.historyId || "";
103
- let addDivider =
104
- (index > 0 && historyId !== lastHistoryId) ||
105
- conversation.system?.level === "newConversation";
106
-
191
+ {renderableConversations.map((conversation) => {
107
192
  return (
108
193
  <div key={conversation.taskId} className="flex flex-col gap-4">
109
- {addDivider && (
194
+ {conversation.addDivider && (
110
195
  <ChatDivider key={`${conversation.taskId}-divider`} />
111
196
  )}
112
- {conversation.messages.map((message, msgIndex) => {
113
- const key = message.messageId || `${index}-${msgIndex}`;
114
- const isNewest = msgIndex === len - 1;
115
- if (message.role === "assistant") {
197
+ {conversation.items.map((item) => {
198
+ if (item.type === "group") {
199
+ return (
200
+ <ChatAssistantCollapsedGroup
201
+ key={item.key}
202
+ messages={item.messages}
203
+ isNewest={item.isNewest}
204
+ isLoading={isLoading}
205
+ showAvatar={item.showAvatar}
206
+ />
207
+ );
208
+ }
209
+
210
+ if (item.message.role === "assistant") {
116
211
  return (
117
212
  <ChatAssistantMessage
118
- key={key}
119
- message={message}
120
- isNewest={isNewest}
213
+ key={item.key}
214
+ message={item.message}
215
+ isNewest={item.isNewest}
121
216
  isLoading={isLoading}
122
- showAvatar={
123
- msgIndex === 0 ||
124
- conversation.messages[msgIndex - 1].role !== "assistant"
125
- }
217
+ showAvatar={item.showAvatar}
126
218
  />
127
219
  );
128
220
  }
129
- return <ChatUserMessage key={key} message={message} />;
221
+
222
+ return <ChatUserMessage key={item.key} message={item.message} />;
130
223
  })}
131
224
  </div>
132
225
  );
@@ -1,3 +1,4 @@
1
+ import { useRef, useState } from "react";
1
2
  import { Button } from "@/components/ui/button";
2
3
  import { getText } from "../i18n";
3
4
 
@@ -12,23 +13,86 @@ const ChatRecommends: React.FC<{
12
13
  onSend,
13
14
  disabled,
14
15
  }) => {
16
+ const scrollRef = useRef<HTMLDivElement>(null);
17
+ const dragStateRef = useRef<{
18
+ pointerId: number;
19
+ startX: number;
20
+ startScrollLeft: number;
21
+ } | null>(null);
22
+ const [isDraggingScroll, setIsDraggingScroll] = useState(false);
23
+
15
24
  if (hidden || !data || data.length === 0) return null;
25
+
26
+ const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
27
+ if (e.pointerType === "mouse" && e.button !== 0) return;
28
+
29
+ const container = scrollRef.current;
30
+ if (!container) return;
31
+
32
+ dragStateRef.current = {
33
+ pointerId: e.pointerId,
34
+ startX: e.clientX,
35
+ startScrollLeft: container.scrollLeft,
36
+ };
37
+ setIsDraggingScroll(false);
38
+ container.setPointerCapture(e.pointerId);
39
+ };
40
+
41
+ const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
42
+ const container = scrollRef.current;
43
+ const dragState = dragStateRef.current;
44
+ if (!container || !dragState || dragState.pointerId !== e.pointerId) return;
45
+
46
+ const deltaX = e.clientX - dragState.startX;
47
+ if (Math.abs(deltaX) > 4) {
48
+ setIsDraggingScroll(true);
49
+ }
50
+ container.scrollLeft = dragState.startScrollLeft - deltaX;
51
+ };
52
+
53
+ const handlePointerEnd = (e: React.PointerEvent<HTMLDivElement>) => {
54
+ const container = scrollRef.current;
55
+ const dragState = dragStateRef.current;
56
+ if (!container || !dragState || dragState.pointerId !== e.pointerId) return;
57
+
58
+ if (container.hasPointerCapture(e.pointerId)) {
59
+ container.releasePointerCapture(e.pointerId);
60
+ }
61
+
62
+ window.requestAnimationFrame(() => {
63
+ setIsDraggingScroll(false);
64
+ });
65
+ dragStateRef.current = null;
66
+ };
67
+
16
68
  return (
17
- <div className="flex flex-wrap gap-2 pt-2 px-4">
18
- {data.map((prompt, index) => (
19
- <Button
20
- key={prompt}
21
- variant="outline"
22
- onClick={() => onSend(prompt)}
23
- disabled={disabled}
24
- className="
25
- text-xs px-2 py-1 h-auto rounded-full cursor-pointer text-nowrap
26
- transition-all duration-200 animate-in fade-in-0 slide-in-from-bottom-1"
27
- style={{ animationDelay: `${index * 50}ms` }}
28
- >
29
- {prompt}
30
- </Button>
31
- ))}
69
+ <div
70
+ ref={scrollRef}
71
+ className="overflow-x-auto overflow-y-hidden px-4 pt-2 pb-1 cursor-grab active:cursor-grabbing [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
72
+ style={{ touchAction: "pan-x" }}
73
+ onPointerDown={handlePointerDown}
74
+ onPointerMove={handlePointerMove}
75
+ onPointerUp={handlePointerEnd}
76
+ onPointerCancel={handlePointerEnd}
77
+ >
78
+ <div className="flex w-max min-w-full gap-2">
79
+ {data.map((prompt, index) => (
80
+ <Button
81
+ key={prompt}
82
+ variant="outline"
83
+ onClick={() => {
84
+ if (!isDraggingScroll) {
85
+ onSend(prompt);
86
+ }
87
+ }}
88
+ disabled={disabled}
89
+ className="h-auto shrink-0 rounded-full px-2 py-1 text-xs text-nowrap transition-all duration-200 animate-in fade-in-0 slide-in-from-bottom-1"
90
+ style={{ animationDelay: `${index * 50}ms` }}
91
+ >
92
+ {prompt}
93
+ </Button>
94
+ ))}
95
+ </div>
32
96
  </div>
33
97
  );
34
98
  };
@@ -3,12 +3,15 @@ import { Mic, StopCircle } from "lucide-react";
3
3
  import { Button } from "@/components/ui/button";
4
4
  import { cn } from "@/lib/utils";
5
5
  import { useVoiceInput } from "../hooks/useVoiceInput";
6
+ import { useEffect } from "react";
6
7
 
7
8
  const VoiceInputButton: React.FC<{
8
9
  onChange: (text: string) => void;
9
10
  disabled?: boolean;
10
11
  value?: string;
11
- }> = ({ onChange, disabled, value }) => {
12
+ onRunningChange?: (running: boolean) => void;
13
+ compact?: boolean;
14
+ }> = ({ onChange, disabled, value, onRunningChange, compact }) => {
12
15
  const {
13
16
  start,
14
17
  stop,
@@ -23,12 +26,18 @@ const VoiceInputButton: React.FC<{
23
26
  value,
24
27
  });
25
28
 
29
+ useEffect(() => {
30
+ if (onRunningChange) {
31
+ onRunningChange(running);
32
+ }
33
+ }, [running, onRunningChange]);
34
+
26
35
  return (
27
36
  <Button
28
37
  variant="ghost"
29
38
  onClick={stoppable ? stop : status === "idle" ? start : undefined}
30
39
  disabled={finalDisabled}
31
- className={cn("h-8 w-8 rounded-lg cursor-pointer text-xs ", {
40
+ className={cn("cursor-pointer text-xs", compact ? "h-7 w-7 rounded-md" : "h-8 w-8 rounded-lg", {
32
41
  "bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-primary-foreground animate-pulse":
33
42
  running,
34
43
  "w-auto": statusText,
@@ -0,0 +1,360 @@
1
+ import type React from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import type { IDisplayMode } from "../types";
4
+
5
+ interface Size {
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ interface AssistantSizeState {
11
+ floating: Size;
12
+ sideLeftWidth: number;
13
+ sideRightWidth: number;
14
+ halfTopHeight: number;
15
+ halfBottomHeight: number;
16
+ }
17
+
18
+ type ResizeType =
19
+ | "floating-corner"
20
+ | "side-left"
21
+ | "side-right"
22
+ | "half-top"
23
+ | "half-bottom";
24
+
25
+ const STORAGE_KEY = `${location.hostname}-ai-assistant-sizes`;
26
+ const BUTTON_SIZE = 56;
27
+ const EDGE_GAP = 16;
28
+
29
+ const clamp = (value: number, min: number, max: number) =>
30
+ Math.min(Math.max(value, min), max);
31
+
32
+ const getViewportBounds = () => {
33
+ const maxFloatingWidth = Math.max(360, window.innerWidth - EDGE_GAP * 2);
34
+ const maxFloatingHeight = Math.max(320, window.innerHeight - EDGE_GAP * 2);
35
+ const maxSideWidth = Math.max(320, Math.min(720, window.innerWidth - 96));
36
+ const maxHalfHeight = Math.max(280, window.innerHeight - 96);
37
+
38
+ return {
39
+ floating: {
40
+ minWidth: 360,
41
+ maxWidth: maxFloatingWidth,
42
+ minHeight: 360,
43
+ maxHeight: maxFloatingHeight,
44
+ },
45
+ side: {
46
+ minWidth: 320,
47
+ maxWidth: maxSideWidth,
48
+ },
49
+ half: {
50
+ minHeight: 280,
51
+ maxHeight: maxHalfHeight,
52
+ },
53
+ };
54
+ };
55
+
56
+ const getDefaultState = (): AssistantSizeState => {
57
+ const bounds = getViewportBounds();
58
+ const defaultFloatingWidth = clamp(420, bounds.floating.minWidth, bounds.floating.maxWidth);
59
+ const defaultFloatingHeight = clamp(
60
+ window.innerHeight < 600 ? window.innerHeight - 32 : window.innerHeight - 200,
61
+ bounds.floating.minHeight,
62
+ bounds.floating.maxHeight,
63
+ );
64
+ const defaultSideWidth = clamp(420, bounds.side.minWidth, bounds.side.maxWidth);
65
+ const defaultHalfHeight = clamp(
66
+ Math.round(window.innerHeight * 0.5),
67
+ bounds.half.minHeight,
68
+ bounds.half.maxHeight,
69
+ );
70
+
71
+ return {
72
+ floating: {
73
+ width: defaultFloatingWidth,
74
+ height: defaultFloatingHeight,
75
+ },
76
+ sideLeftWidth: defaultSideWidth,
77
+ sideRightWidth: defaultSideWidth,
78
+ halfTopHeight: defaultHalfHeight,
79
+ halfBottomHeight: defaultHalfHeight,
80
+ };
81
+ };
82
+
83
+ const normalizeState = (state: AssistantSizeState): AssistantSizeState => {
84
+ const bounds = getViewportBounds();
85
+
86
+ return {
87
+ floating: {
88
+ width: clamp(
89
+ state.floating.width,
90
+ bounds.floating.minWidth,
91
+ bounds.floating.maxWidth,
92
+ ),
93
+ height: clamp(
94
+ state.floating.height,
95
+ bounds.floating.minHeight,
96
+ bounds.floating.maxHeight,
97
+ ),
98
+ },
99
+ sideLeftWidth: clamp(
100
+ state.sideLeftWidth,
101
+ bounds.side.minWidth,
102
+ bounds.side.maxWidth,
103
+ ),
104
+ sideRightWidth: clamp(
105
+ state.sideRightWidth,
106
+ bounds.side.minWidth,
107
+ bounds.side.maxWidth,
108
+ ),
109
+ halfTopHeight: clamp(
110
+ state.halfTopHeight,
111
+ bounds.half.minHeight,
112
+ bounds.half.maxHeight,
113
+ ),
114
+ halfBottomHeight: clamp(
115
+ state.halfBottomHeight,
116
+ bounds.half.minHeight,
117
+ bounds.half.maxHeight,
118
+ ),
119
+ };
120
+ };
121
+
122
+ const getSavedState = (): AssistantSizeState => {
123
+ const fallback = getDefaultState();
124
+
125
+ try {
126
+ const saved = localStorage.getItem(STORAGE_KEY);
127
+ if (!saved) return fallback;
128
+ const parsed = JSON.parse(saved) as Partial<AssistantSizeState>;
129
+ return normalizeState({
130
+ floating: {
131
+ width: parsed.floating?.width ?? fallback.floating.width,
132
+ height: parsed.floating?.height ?? fallback.floating.height,
133
+ },
134
+ sideLeftWidth: parsed.sideLeftWidth ?? fallback.sideLeftWidth,
135
+ sideRightWidth: parsed.sideRightWidth ?? fallback.sideRightWidth,
136
+ halfTopHeight: parsed.halfTopHeight ?? fallback.halfTopHeight,
137
+ halfBottomHeight: parsed.halfBottomHeight ?? fallback.halfBottomHeight,
138
+ });
139
+ } catch {
140
+ return fallback;
141
+ }
142
+ };
143
+
144
+ export const useAssistantSize = (displayMode: IDisplayMode) => {
145
+ const [sizes, setSizes] = useState<AssistantSizeState>(getSavedState);
146
+ const [isResizing, setIsResizing] = useState(false);
147
+ const resizeRef = useRef<{
148
+ type: ResizeType;
149
+ startX: number;
150
+ startY: number;
151
+ sizes: AssistantSizeState;
152
+ } | null>(null);
153
+
154
+ useEffect(() => {
155
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(sizes));
156
+ }, [sizes]);
157
+
158
+ useEffect(() => {
159
+ const handleResize = () => {
160
+ setSizes((prev) => {
161
+ const normalized = normalizeState(prev);
162
+ return JSON.stringify(normalized) === JSON.stringify(prev)
163
+ ? prev
164
+ : normalized;
165
+ });
166
+ };
167
+
168
+ window.addEventListener("resize", handleResize);
169
+ return () => window.removeEventListener("resize", handleResize);
170
+ }, []);
171
+
172
+ useEffect(() => {
173
+ if (!isResizing) return;
174
+
175
+ const previousUserSelect = document.body.style.userSelect;
176
+ document.body.style.userSelect = "none";
177
+
178
+ const handlePointerMove = (event: PointerEvent) => {
179
+ const activeResize = resizeRef.current;
180
+ if (!activeResize) return;
181
+
182
+ const deltaX = event.clientX - activeResize.startX;
183
+ const deltaY = event.clientY - activeResize.startY;
184
+ const bounds = getViewportBounds();
185
+
186
+ setSizes((prev) => {
187
+ const next = { ...prev, floating: { ...prev.floating } };
188
+
189
+ switch (activeResize.type) {
190
+ case "floating-corner":
191
+ next.floating.width = clamp(
192
+ activeResize.sizes.floating.width - deltaX,
193
+ bounds.floating.minWidth,
194
+ bounds.floating.maxWidth,
195
+ );
196
+ next.floating.height = clamp(
197
+ activeResize.sizes.floating.height - deltaY,
198
+ bounds.floating.minHeight,
199
+ bounds.floating.maxHeight,
200
+ );
201
+ break;
202
+ case "side-left":
203
+ next.sideLeftWidth = clamp(
204
+ activeResize.sizes.sideLeftWidth + deltaX,
205
+ bounds.side.minWidth,
206
+ bounds.side.maxWidth,
207
+ );
208
+ break;
209
+ case "side-right":
210
+ next.sideRightWidth = clamp(
211
+ activeResize.sizes.sideRightWidth - deltaX,
212
+ bounds.side.minWidth,
213
+ bounds.side.maxWidth,
214
+ );
215
+ break;
216
+ case "half-top":
217
+ next.halfTopHeight = clamp(
218
+ activeResize.sizes.halfTopHeight + deltaY,
219
+ bounds.half.minHeight,
220
+ bounds.half.maxHeight,
221
+ );
222
+ break;
223
+ case "half-bottom":
224
+ next.halfBottomHeight = clamp(
225
+ activeResize.sizes.halfBottomHeight - deltaY,
226
+ bounds.half.minHeight,
227
+ bounds.half.maxHeight,
228
+ );
229
+ break;
230
+ }
231
+
232
+ return next;
233
+ });
234
+ };
235
+
236
+ const handlePointerUp = () => {
237
+ resizeRef.current = null;
238
+ setIsResizing(false);
239
+ };
240
+
241
+ window.addEventListener("pointermove", handlePointerMove);
242
+ window.addEventListener("pointerup", handlePointerUp);
243
+
244
+ return () => {
245
+ document.body.style.userSelect = previousUserSelect;
246
+ window.removeEventListener("pointermove", handlePointerMove);
247
+ window.removeEventListener("pointerup", handlePointerUp);
248
+ };
249
+ }, [isResizing]);
250
+
251
+ const getElementDimensions = useCallback(
252
+ (isOpen: boolean) => {
253
+ if (!isOpen) {
254
+ return {
255
+ width: BUTTON_SIZE,
256
+ height: BUTTON_SIZE,
257
+ };
258
+ }
259
+
260
+ switch (displayMode) {
261
+ case "floating":
262
+ return sizes.floating;
263
+ case "side-left":
264
+ return {
265
+ width: sizes.sideLeftWidth,
266
+ height: window.innerHeight,
267
+ };
268
+ case "side-right":
269
+ return {
270
+ width: sizes.sideRightWidth,
271
+ height: window.innerHeight,
272
+ };
273
+ case "half-top":
274
+ return {
275
+ width: window.innerWidth,
276
+ height: sizes.halfTopHeight,
277
+ };
278
+ case "half-bottom":
279
+ return {
280
+ width: window.innerWidth,
281
+ height: sizes.halfBottomHeight,
282
+ };
283
+ case "fullscreen":
284
+ return {
285
+ width: window.innerWidth,
286
+ height: window.innerHeight,
287
+ };
288
+ default:
289
+ return sizes.floating;
290
+ }
291
+ },
292
+ [displayMode, sizes],
293
+ );
294
+
295
+ const cardStyle = useMemo<React.CSSProperties | undefined>(() => {
296
+ switch (displayMode) {
297
+ case "floating":
298
+ return {
299
+ width: sizes.floating.width,
300
+ height: sizes.floating.height,
301
+ };
302
+ case "side-left":
303
+ return {
304
+ width: sizes.sideLeftWidth,
305
+ };
306
+ case "side-right":
307
+ return {
308
+ width: sizes.sideRightWidth,
309
+ };
310
+ case "half-top":
311
+ return {
312
+ height: sizes.halfTopHeight,
313
+ };
314
+ case "half-bottom":
315
+ return {
316
+ height: sizes.halfBottomHeight,
317
+ };
318
+ default:
319
+ return undefined;
320
+ }
321
+ }, [displayMode, sizes]);
322
+
323
+ const startResize = useCallback(
324
+ (type: ResizeType, event: React.PointerEvent) => {
325
+ event.preventDefault();
326
+ event.stopPropagation();
327
+ resizeRef.current = {
328
+ type,
329
+ startX: event.clientX,
330
+ startY: event.clientY,
331
+ sizes,
332
+ };
333
+ setIsResizing(true);
334
+ },
335
+ [sizes],
336
+ );
337
+
338
+ const resizeType: ResizeType | null =
339
+ displayMode === "floating"
340
+ ? "floating-corner"
341
+ : displayMode === "side-left"
342
+ ? "side-left"
343
+ : displayMode === "side-right"
344
+ ? "side-right"
345
+ : displayMode === "half-top"
346
+ ? "half-top"
347
+ : displayMode === "half-bottom"
348
+ ? "half-bottom"
349
+ : null;
350
+
351
+ return {
352
+ cardStyle,
353
+ getElementDimensions,
354
+ isResizing,
355
+ resizeType,
356
+ sideLeftWidth: sizes.sideLeftWidth,
357
+ sideRightWidth: sizes.sideRightWidth,
358
+ startResize,
359
+ };
360
+ };