@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
  {
2
2
  "name": "@mingxy/opencode-mascot",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "OpenCode TUI mascot plugin framework - customizable ASCII mascots for your terminal",
5
5
  "author": "mingxy",
6
6
  "license": "MIT",
@@ -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
  >