diff --git a/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx b/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx index 8547a5d96c..9b845227a6 100644 --- a/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx +++ b/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx @@ -1009,6 +1009,102 @@ function MessageComposer({ compact = false }: { compact?: boolean }) { ); } +function BestieChat({ onClose }: { onClose: () => void }) { + const [detached, setDetached] = useState(false); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const panelRef = useRef(null); + const dragRef = useRef({ + pointerId: -1, + startX: 0, + startY: 0, + originX: 0, + originY: 0, + x: 0, + y: 0, + beganDetached: false, + }); + + const beginDrag = (event: ReactPointerEvent) => { + if ((event.target as Element).closest("button, input")) return; + const panel = panelRef.current; + if (!panel) return; + const rect = panel.getBoundingClientRect(); + event.currentTarget.setPointerCapture(event.pointerId); + dragRef.current = { + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + originX: rect.left, + originY: rect.top, + x: rect.left, + y: rect.top, + beganDetached: detached, + }; + panel.style.setProperty("--bestie-x", `${rect.left}px`); + panel.style.setProperty("--bestie-y", `${rect.top}px`); + panel.classList.add("is-dragging"); + }; + + const updateDrag = (event: ReactPointerEvent) => { + const drag = dragRef.current; + const panel = panelRef.current; + if (!panel || drag.pointerId !== event.pointerId) return; + const pullDistance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY); + drag.x = Math.max(16, Math.min(window.innerWidth - panel.offsetWidth - 16, drag.originX + event.clientX - drag.startX)); + drag.y = Math.max(74, Math.min(window.innerHeight - panel.offsetHeight - 16, drag.originY + event.clientY - drag.startY)); + panel.style.setProperty("--bestie-x", `${drag.x}px`); + panel.style.setProperty("--bestie-y", `${drag.y}px`); + panel.classList.toggle("is-tearing", !drag.beganDetached && pullDistance > 48); + }; + + const endDrag = (event: ReactPointerEvent) => { + const drag = dragRef.current; + const panel = panelRef.current; + if (!panel || drag.pointerId !== event.pointerId) return; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + const shouldDetach = drag.beganDetached || Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 84; + panel.classList.remove("is-dragging", "is-tearing"); + if (shouldDetach) { + panel.classList.add("is-detached"); + setDetached(true); + setPosition({ x: drag.x, y: drag.y }); + } else { + panel.style.removeProperty("--bestie-x"); + panel.style.removeProperty("--bestie-y"); + } + drag.pointerId = -1; + }; + + return ( + + ); +} + function MessagesView() { const [layouts, setLayouts] = useState(INITIAL_MESSAGE_LAYOUT); const [connections, setConnections] = useState(INITIAL_MESSAGE_CONNECTIONS); @@ -1304,6 +1400,7 @@ function MessagesView() { export function BuzzProjectPrototype() { const [activeTab, setActiveTab] = useState<"me" | "messages" | "projects">("me"); const [selectedTask, setSelectedTask] = useState(null); + const [bestieOpen, setBestieOpen] = useState(false); const appShellRef = useRef(null); const projectWorldRef = useRef(null); const canvasPanRef = useRef({ @@ -1389,9 +1486,11 @@ export function BuzzProjectPrototype() { -
C
+
C
+ {bestieOpen ? setBestieOpen(false)} /> : null} + {activeTab === "projects" ? ( ) : activeTab === "messages" ? ( diff --git a/prototypes/project-cube-orbit/app/globals.css b/prototypes/project-cube-orbit/app/globals.css index fdffbdbfe0..6c7c8e7a7d 100644 --- a/prototypes/project-cube-orbit/app/globals.css +++ b/prototypes/project-cube-orbit/app/globals.css @@ -47,10 +47,65 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif .segmented .add-button { width: 36px; padding: 0; display: grid; place-items: center; } .utilities { justify-self: end; display: flex; align-items: center; gap: 12px; } .round-button { width: var(--header-control-size); height: var(--header-control-size); border-radius: 50%; display: grid; place-items: center; border: 1px solid rgba(255,255,255,.9); background: rgba(244,249,251,.52); cursor: pointer; backdrop-filter: blur(16px); } +.snake-button img { display: block; width: 30px; height: 30px; object-fit: contain; } +.snake-button.is-active { background: rgba(255,255,255,.78); box-shadow: 0 8px 22px rgba(54,82,94,.12); } .search-icon { display: block; width: 18px; height: 18px; } .avatar { width: var(--header-control-size); height: var(--header-control-size); border-radius: 50%; display: grid; place-items: center; color: rgba(255,255,255,.95); background: radial-gradient(circle at 53% 29%, #d8af86 0 13%, transparent 14%), radial-gradient(ellipse at 50% 68%, #3f2923 0 25%, transparent 26%), linear-gradient(145deg, #99a65d, #49352d 65%, #c1a16e); border: 2px solid rgba(255,255,255,.95); box-shadow: 0 0 0 1px rgba(44,52,45,.08); font-size: 11px; font-weight: 650; } .avatar span { transform: translateY(1px); text-shadow: 0 1px 3px rgba(0,0,0,.38); } +.bestie-chat { + --bestie-x: 0px; + --bestie-y: 0px; + position: fixed; + z-index: 45; + top: 74px; + right: 20px; + width: min(360px, calc(100vw - 32px)); + height: 430px; + overflow: hidden; + border: 1px solid rgba(255,255,255,.92); + border-radius: 28px; + background: rgba(255,255,255,.82); + box-shadow: 0 18px 50px rgba(53,76,86,.16), inset 0 1px 0 rgba(255,255,255,.8); + backdrop-filter: blur(24px) saturate(118%); + animation: bestie-popover-enter 180ms cubic-bezier(.215,.61,.355,1); + transform-origin: calc(100% - 21px) -28px; +} +.bestie-chat.is-dragging, +.bestie-chat.is-detached { + top: 0; + right: auto; + left: 0; + transform: translate3d(var(--bestie-x), var(--bestie-y), 0) scale(1); + animation: none; + will-change: transform; +} +.bestie-chat.is-detached { z-index: 44; background: rgba(255,255,255,.9); transition: transform 240ms cubic-bezier(.455,.03,.515,.955), box-shadow 180ms ease; } +.bestie-chat.is-dragging { transition: none; } +.bestie-chat.is-tearing { transform: translate3d(var(--bestie-x), var(--bestie-y), 0) scale(1.018); box-shadow: 0 24px 64px rgba(53,76,86,.22), inset 0 1px 0 rgba(255,255,255,.9); } +.bestie-chat-header { display: flex; height: 58px; align-items: center; justify-content: space-between; padding: 0 14px 0 16px; border-bottom: 1px solid rgba(38,38,38,.08); cursor: grab; touch-action: none; user-select: none; } +.bestie-chat.is-dragging .bestie-chat-header { cursor: grabbing; } +.bestie-chat-header > div { display: flex; align-items: center; gap: 9px; } +.bestie-chat-header img { width: 28px; height: 28px; object-fit: contain; } +.bestie-chat-header strong { font-size: 14px; font-weight: 650; } +.bestie-chat-header button { display: grid; width: 34px; height: 34px; place-items: center; border: 0; border-radius: 50%; background: transparent; cursor: pointer; } +.bestie-chat-header button:hover { background: rgba(38,38,38,.055); } +.bestie-chat-content { height: calc(100% - 144px); overflow: auto; padding: 20px 16px; } +.bestie-message { display: flex; align-items: flex-end; gap: 8px; margin-bottom: 14px; } +.bestie-message img { width: 28px; height: 28px; flex: 0 0 auto; object-fit: contain; } +.bestie-message p { max-width: 82%; margin: 0; padding: 10px 13px; border-radius: 17px; background: rgba(238,238,238,.86); font-size: 14px; line-height: 1.35; } +.bestie-message.is-user { justify-content: flex-end; } +.bestie-message.is-user p { color: #fff; background: #1688cf; } +.bestie-composer { position: absolute; right: 14px; bottom: 14px; left: 14px; display: flex; height: 58px; align-items: center; gap: 10px; padding: 0 10px 0 16px; border: 1px solid rgba(38,38,38,.12); border-radius: 18px; background: rgba(255,255,255,.82); box-shadow: 0 7px 22px rgba(57,78,87,.05); } +.bestie-composer input { min-width: 0; flex: 1; border: 0; outline: 0; color: #262626; background: transparent; font-size: 14px; } +.bestie-composer input::placeholder { color: rgba(38,38,38,.4); } +.bestie-composer button { display: grid; width: 34px; height: 34px; flex: 0 0 auto; place-items: center; border: 0; border-radius: 50%; background: #f3e7ff; cursor: pointer; } + +@keyframes bestie-popover-enter { + from { opacity: 0; transform: translateY(-6px) scale(.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + .project-stage { position: relative; min-height: 100svh; overflow: hidden; touch-action: none; user-select: none; cursor: grab; } .project-stage.is-panning { cursor: grabbing; } .project-world { position: absolute; inset: 0; transform: translate3d(0,0,0); transition: transform 520ms cubic-bezier(.22,1,.36,1); will-change: transform; } @@ -464,6 +519,8 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif .orbit-system, .task-detail-panel { transition-duration: 1ms; animation-duration: 1ms; } .message-panel { transition: none; } .merge-edge-indicator { transition: none; } + .bestie-chat { animation: none; } + .bestie-chat.is-detached { transition: none; } .response-composer { transition: none; } .response-composer textarea { animation: none; } .project-name-pill { transition: none; } diff --git a/prototypes/project-cube-orbit/public/snek.png b/prototypes/project-cube-orbit/public/snek.png new file mode 100644 index 0000000000..20c71db96b Binary files /dev/null and b/prototypes/project-cube-orbit/public/snek.png differ