@mingxy/opencode-mascot 0.2.7 → 0.2.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
|
|
3
|
-
import { createSignal } from "solid-js";
|
|
3
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
4
4
|
import type { JSX } from "@opentui/solid";
|
|
5
5
|
import type { MascotPack, MascotState } from "../core/types";
|
|
6
6
|
import { createAnimatedRenderer } from "../core/ascii-renderer";
|
|
@@ -27,6 +27,11 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
|
|
|
27
27
|
sleeping: "baozi",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const MASCOT_WIDTH = 10;
|
|
31
|
+
const PEEK = 2;
|
|
32
|
+
const PEEK_INTERVAL = 1200;
|
|
33
|
+
const EDGE_THRESHOLD = 3;
|
|
34
|
+
|
|
30
35
|
export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
31
36
|
const names = Object.keys(props.mascots);
|
|
32
37
|
const initialName =
|
|
@@ -37,24 +42,96 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
37
42
|
const [currentName, setCurrentName] = createSignal(initialName);
|
|
38
43
|
const [posX, setPosX] = createSignal(20);
|
|
39
44
|
const [posY, setPosY] = createSignal(2);
|
|
45
|
+
const [containerWidth, setContainerWidth] = createSignal(0);
|
|
40
46
|
let dragStartX = 0;
|
|
41
47
|
let dragStartY = 0;
|
|
42
48
|
let dragAnchorX = 0;
|
|
43
49
|
let dragAnchorY = 0;
|
|
44
50
|
let lastClickTime = 0;
|
|
45
51
|
let isDragging = false;
|
|
52
|
+
let hideSide: "left" | "right" | null = null;
|
|
53
|
+
let peekTimer: ReturnType<typeof setInterval> | null = null;
|
|
54
|
+
let returnTimer: ReturnType<typeof setInterval> | null = null;
|
|
46
55
|
|
|
47
56
|
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
48
57
|
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
49
58
|
renderers[name] = createAnimatedRenderer(pack);
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
const stopPeek = () => {
|
|
62
|
+
if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
|
|
63
|
+
};
|
|
64
|
+
const stopReturn = () => {
|
|
65
|
+
if (returnTimer) { clearInterval(returnTimer); returnTimer = null; }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
onCleanup(() => { stopPeek(); stopReturn(); });
|
|
69
|
+
|
|
52
70
|
const switchToNext = () => {
|
|
53
71
|
const cur = currentName();
|
|
54
72
|
const idx = names.indexOf(cur);
|
|
55
73
|
setCurrentName(names[(idx + 1) % names.length]);
|
|
56
74
|
};
|
|
57
75
|
|
|
76
|
+
const getCw = () => containerWidth() || 30;
|
|
77
|
+
|
|
78
|
+
const clampX = (rawX: number): number => {
|
|
79
|
+
const cw = getCw();
|
|
80
|
+
return Math.max(-(MASCOT_WIDTH - PEEK), Math.min(rawX, cw - PEEK));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const clampY = (rawY: number): number => {
|
|
84
|
+
return Math.max(0, rawY);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const checkEdge = () => {
|
|
88
|
+
const cw = getCw();
|
|
89
|
+
const x = posX();
|
|
90
|
+
if (x <= -(MASCOT_WIDTH - PEEK) + EDGE_THRESHOLD) {
|
|
91
|
+
hideSide = "left";
|
|
92
|
+
startPeek();
|
|
93
|
+
} else if (x >= cw - PEEK - EDGE_THRESHOLD) {
|
|
94
|
+
hideSide = "right";
|
|
95
|
+
startPeek();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const startPeek = () => {
|
|
100
|
+
stopPeek();
|
|
101
|
+
let phase = false;
|
|
102
|
+
peekTimer = setInterval(() => {
|
|
103
|
+
phase = !phase;
|
|
104
|
+
const stretch = phase ? PEEK : 0;
|
|
105
|
+
const cw = getCw();
|
|
106
|
+
if (hideSide === "left") {
|
|
107
|
+
setPosX(-(MASCOT_WIDTH - PEEK) + stretch);
|
|
108
|
+
} else if (hideSide === "right") {
|
|
109
|
+
setPosX(cw - PEEK - stretch);
|
|
110
|
+
}
|
|
111
|
+
}, PEEK_INTERVAL);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const returnToView = () => {
|
|
115
|
+
if (!hideSide) return;
|
|
116
|
+
stopPeek();
|
|
117
|
+
const cw = getCw();
|
|
118
|
+
const cur = posX();
|
|
119
|
+
const targetX = hideSide === "left" ? 0 : Math.max(0, cw - MASCOT_WIDTH);
|
|
120
|
+
const step = targetX > cur ? 1 : -1;
|
|
121
|
+
|
|
122
|
+
returnTimer = setInterval(() => {
|
|
123
|
+
const now = posX();
|
|
124
|
+
if (Math.abs(now - targetX) <= 1) {
|
|
125
|
+
setPosX(targetX);
|
|
126
|
+
stopReturn();
|
|
127
|
+
hideSide = null;
|
|
128
|
+
renderers[currentName()].bounce();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
setPosX(now + step);
|
|
132
|
+
}, 16);
|
|
133
|
+
};
|
|
134
|
+
|
|
58
135
|
const setStateWithSwitch = (s: MascotState) => {
|
|
59
136
|
const cur = currentName();
|
|
60
137
|
renderers[cur].setState(s);
|
|
@@ -69,6 +146,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
69
146
|
const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
|
|
70
147
|
const statusType = payload?.properties?.status?.type;
|
|
71
148
|
if (statusType === "busy" || statusType === "retry") {
|
|
149
|
+
if (hideSide) returnToView();
|
|
72
150
|
renderers[currentName()].setState("busy");
|
|
73
151
|
} else {
|
|
74
152
|
setStateWithSwitch("idle");
|
|
@@ -110,7 +188,19 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
110
188
|
alignItems="center"
|
|
111
189
|
zIndex={100}
|
|
112
190
|
flexDirection="column"
|
|
191
|
+
ref={(node: any) => {
|
|
192
|
+
if (node) {
|
|
193
|
+
setContainerWidth(node.width || 0);
|
|
194
|
+
if (node.onSizeChange !== undefined) {
|
|
195
|
+
node.onSizeChange = () => {
|
|
196
|
+
setContainerWidth(node.width || 0);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
113
201
|
onMouseDown={(e: any) => {
|
|
202
|
+
if (hideSide) { returnToView(); return; }
|
|
203
|
+
|
|
114
204
|
const now = Date.now();
|
|
115
205
|
if (now - lastClickTime < 300) {
|
|
116
206
|
switchToNext();
|
|
@@ -133,8 +223,8 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
133
223
|
}}
|
|
134
224
|
onMouseDrag={(e: any) => {
|
|
135
225
|
if (e.modifiers?.alt && isDragging) {
|
|
136
|
-
setPosX(dragAnchorX + (e.x - dragStartX));
|
|
137
|
-
setPosY(dragAnchorY + (e.y - dragStartY));
|
|
226
|
+
setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
|
|
227
|
+
setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
|
|
138
228
|
e.preventDefault();
|
|
139
229
|
e.stopPropagation();
|
|
140
230
|
props.api.renderer.clearSelection();
|
|
@@ -144,12 +234,14 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
144
234
|
if (isDragging) {
|
|
145
235
|
isDragging = false;
|
|
146
236
|
renderers[currentName()].setDragging(false);
|
|
237
|
+
checkEdge();
|
|
147
238
|
}
|
|
148
239
|
}}
|
|
149
240
|
onMouseDragEnd={() => {
|
|
150
241
|
if (isDragging) {
|
|
151
242
|
isDragging = false;
|
|
152
243
|
renderers[currentName()].setDragging(false);
|
|
244
|
+
checkEdge();
|
|
153
245
|
}
|
|
154
246
|
}}
|
|
155
247
|
>
|