ghostykit 0.9.0 → 0.9.1

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/README.md CHANGED
@@ -276,6 +276,7 @@ const split = usePilotoSplit({ widthKey: "app:agent-width", onOpenChange: setSid
276
276
  ```
277
277
 
278
278
  `useStickyPanelHeight(variant === "panel")` keeps the panel's composer in view while the page scrolls.
279
+ `<PilotoLauncher split={split} label="Talk to Ghosty" avatarSrc=… className="fixed bottom-6 right-6" />` is the open button (spring in/out, hidden while open).
279
280
  Colors: `--gk-split-handle`, `--gk-split-accent`, `--gk-split-sheet-bg`, `--gk-split-overlay`.
280
281
 
281
282
  ## Accessibility
@@ -19,6 +19,8 @@ export declare function usePilotoSplit(options: PilotoSplitOptions | string): {
19
19
  setSheetOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
20
20
  mounted: boolean;
21
21
  widthKey: string;
22
+ animating: boolean;
23
+ targetWidth: number;
22
24
  /** Abierto en la forma que toca (panel o hoja). */
23
25
  isOpen: boolean;
24
26
  open: () => void;
@@ -56,3 +58,16 @@ export declare function useStickyPanelHeight(enabled?: boolean): {
56
58
  };
57
59
  /** Panel lateral desde `minWidth` px (default 1024, el `lg` de Tailwind), hoja debajo. En el servidor, escritorio. */
58
60
  export declare function useIsDesktop(minWidth?: number): boolean;
61
+ /**
62
+ * El botón para abrir el piloto: una pastilla con el avatar y «Hablar con …» que entra y sale con
63
+ * resorte (como el tablero de Ventas de gs). Se muestra sólo con el piloto cerrado.
64
+ */
65
+ export declare function PilotoLauncher({ split, label, avatarSrc, className, style, }: {
66
+ split: PilotoSplitState;
67
+ /** Texto de la pastilla, p. ej. «Hablar con Ghosty». Sin texto, sólo el avatar. */
68
+ label?: string;
69
+ avatarSrc: string;
70
+ /** Posición y colores (`fixed bottom-6 right-6 …`). */
71
+ className?: string;
72
+ style?: CSSProperties;
73
+ }): import("react").JSX.Element;
@@ -2,8 +2,10 @@
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { Group, Panel, Separator, usePanelRef } from "react-resizable-panels";
4
4
  import { Drawer } from "vaul";
5
+ import { AnimatePresence, motion } from "motion/react";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  var SHEET_SNAPS = [0.6, 0.94];
8
+ var OPEN_MS = 320;
7
9
  function usePilotoSplit(options) {
8
10
  const { widthKey, defaultWidth = 420, desktopMinWidth = 1024, onOpenChange } = typeof options === "string" ? { widthKey: options } : options;
9
11
  const isDesktop = useIsDesktop(desktopMinWidth);
@@ -11,6 +13,13 @@ function usePilotoSplit(options) {
11
13
  const [panelOpen, setPanelOpen] = useState(false);
12
14
  const [sheetOpen, setSheetOpen] = useState(false);
13
15
  const [mounted, setMounted] = useState(false);
16
+ const [animating, setAnimating] = useState(false);
17
+ const animTimer = useRef(void 0);
18
+ const animate = useCallback(() => {
19
+ setAnimating(true);
20
+ clearTimeout(animTimer.current);
21
+ animTimer.current = setTimeout(() => setAnimating(false), OPEN_MS + 40);
22
+ }, []);
14
23
  useEffect(() => {
15
24
  const panel = panelRef.current;
16
25
  if (isDesktop || !panel || panel.isCollapsed()) return;
@@ -22,13 +31,16 @@ function usePilotoSplit(options) {
22
31
  if (!isDesktop) return setSheetOpen(true);
23
32
  const panel = panelRef.current;
24
33
  if (!panel || !panel.isCollapsed()) return;
34
+ animate();
25
35
  panel.expand();
26
36
  panel.resize(`${readWidth(widthKey, defaultWidth)}px`);
27
- }, [isDesktop, panelRef, widthKey, defaultWidth]);
37
+ }, [isDesktop, panelRef, widthKey, defaultWidth, animate]);
28
38
  const close = useCallback(() => {
29
- panelRef.current?.collapse();
39
+ const panel = panelRef.current;
40
+ if (panel && !panel.isCollapsed()) animate();
41
+ panel?.collapse();
30
42
  setSheetOpen(false);
31
- }, [panelRef]);
43
+ }, [panelRef, animate]);
32
44
  const isOpen = isDesktop ? panelOpen : sheetOpen;
33
45
  const notify = useRef(onOpenChange);
34
46
  notify.current = onOpenChange;
@@ -44,6 +56,8 @@ function usePilotoSplit(options) {
44
56
  setSheetOpen,
45
57
  mounted,
46
58
  widthKey,
59
+ animating,
60
+ targetWidth: readWidth(widthKey, defaultWidth),
47
61
  /** Abierto en la forma que toca (panel o hoja). */
48
62
  isOpen,
49
63
  open,
@@ -62,7 +76,7 @@ function PilotoSplit({
62
76
  sheetClassName = "",
63
77
  style
64
78
  }) {
65
- const { isDesktop, panelRef, panelOpen, setPanelOpen, sheetOpen, setSheetOpen, mounted, widthKey } = split;
79
+ const { isDesktop, panelRef, panelOpen, setPanelOpen, sheetOpen, setSheetOpen, mounted, widthKey, animating, targetWidth } = split;
66
80
  const [sheetSnap, setSheetSnap] = useState(SHEET_SNAPS[0]);
67
81
  const sheetChat = useRef(null);
68
82
  const [sheetChatH, setSheetChatH] = useState(null);
@@ -105,7 +119,7 @@ function PilotoSplit({
105
119
  maxSize: "60%",
106
120
  style: { overflow: "clip", minWidth: 0 },
107
121
  onResize: (size) => setPanelOpen(size.inPixels > 0),
108
- children: mounted && isDesktop && /* @__PURE__ */ jsx("div", { style: { height: "100%" }, inert: !panelOpen, children: piloto("panel") })
122
+ children: mounted && isDesktop && /* @__PURE__ */ jsx("div", { style: { height: "100%", width: animating ? `${targetWidth}px` : "100%" }, inert: !panelOpen, children: piloto("panel") })
109
123
  }
110
124
  )
111
125
  ] }) }),
@@ -188,7 +202,36 @@ function useIsDesktop(minWidth = 1024) {
188
202
  }, [minWidth]);
189
203
  return desktop;
190
204
  }
205
+ function PilotoLauncher({
206
+ split,
207
+ label,
208
+ avatarSrc,
209
+ className = "",
210
+ style
211
+ }) {
212
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: !split.isOpen && /* @__PURE__ */ jsxs(
213
+ motion.button,
214
+ {
215
+ type: "button",
216
+ onClick: split.open,
217
+ "aria-label": label ?? "Abrir el piloto",
218
+ title: label,
219
+ initial: { opacity: 0, scale: 0.6, y: 20 },
220
+ animate: { opacity: 1, scale: 1, y: 0 },
221
+ exit: { opacity: 0, scale: 0.6, y: 20 },
222
+ transition: { type: "spring", stiffness: 400, damping: 24 },
223
+ className: `gk-launcher ${label ? "" : "gk-launcher-icon"} ${className}`,
224
+ style,
225
+ children: [
226
+ /* @__PURE__ */ jsx("img", { src: avatarSrc, alt: "" }),
227
+ label && /* @__PURE__ */ jsx("span", { children: label })
228
+ ]
229
+ },
230
+ "gk-launcher"
231
+ ) });
232
+ }
191
233
  export {
234
+ PilotoLauncher,
192
235
  PilotoSplit,
193
236
  useIsDesktop,
194
237
  usePilotoSplit,
package/dist/split.css CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  /* Abrir y cerrar se animan; arrastrar, no. */
6
6
  .gk-split:not(:has([data-separator="active"])) > [data-panel] {
7
- transition: flex-grow 280ms cubic-bezier(0.22, 1, 0.36, 1);
7
+ transition: flex-grow 320ms cubic-bezier(0.22, 1, 0.36, 1);
8
8
  }
9
9
  @media (prefers-reduced-motion: reduce) {
10
10
  .gk-split > [data-panel] { transition: none; }
@@ -48,3 +48,24 @@
48
48
  }
49
49
  .gk-split-handle { margin: 8px auto !important; height: 6px !important; width: 48px !important; flex-shrink: 0; background: #d1d5db !important; }
50
50
  .gk-split-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
51
+
52
+ /* Pastilla para abrir el piloto. Colores: --gk-launcher-bg, --gk-launcher-fg. */
53
+ .gk-launcher {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 8px;
57
+ padding: 8px 16px 8px 8px;
58
+ border: 0;
59
+ border-radius: 9999px;
60
+ background: var(--gk-launcher-bg, #191a20);
61
+ color: var(--gk-launcher-fg, #fff);
62
+ font-family: inherit;
63
+ font-size: 14px;
64
+ font-weight: 600;
65
+ line-height: 1;
66
+ cursor: pointer;
67
+ box-shadow: 0 12px 30px -10px rgb(25 26 32 / 0.6);
68
+ z-index: 40;
69
+ }
70
+ .gk-launcher img { width: 32px; height: 32px; object-fit: contain; }
71
+ .gk-launcher-icon { padding: 8px; }
package/dist/styles.css CHANGED
@@ -155,12 +155,18 @@
155
155
  .piloto .right-4:not([data-piloto-isolate] *) {
156
156
  right: calc(var(--spacing) * 4);
157
157
  }
158
+ .piloto .right-6:not([data-piloto-isolate] *) {
159
+ right: calc(var(--spacing) * 6);
160
+ }
158
161
  .piloto .bottom-2:not([data-piloto-isolate] *) {
159
162
  bottom: calc(var(--spacing) * 2);
160
163
  }
161
164
  .piloto .bottom-4:not([data-piloto-isolate] *) {
162
165
  bottom: calc(var(--spacing) * 4);
163
166
  }
167
+ .piloto .bottom-6:not([data-piloto-isolate] *) {
168
+ bottom: calc(var(--spacing) * 6);
169
+ }
164
170
  .piloto .bottom-full:not([data-piloto-isolate] *) {
165
171
  bottom: 100%;
166
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Ghosty Autopilot — the autopilot chat for your React app, on AG-UI: works with LangGraph, Mastra, CrewAI or any AG-UI agent. Streaming text, agent steps, human approval cards (interrupt/resume), attachments and voice.",
5
5
  "type": "module",
6
6
  "main": "./dist/core/index.js",