diff --git a/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx b/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx index 211e0e4f92..ddfffefe8c 100644 --- a/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx +++ b/prototypes/project-cube-orbit/app/buzz-project-prototype.tsx @@ -2,7 +2,23 @@ import { useEffect, useId, useRef, useState } from "react"; import type { CSSProperties, MutableRefObject, PointerEvent as ReactPointerEvent } from "react"; -import { ChevronDown, Plus, Search, SquareDashed, X } from "lucide-react"; +import { + ArrowUp, + AtSign, + ChevronDown, + Hash, + Headphones, + LockKeyhole, + MoreVertical, + Paperclip, + PanelRightOpen, + Plus, + Search, + SmilePlus, + SquareDashed, + Users, + X, +} from "lucide-react"; import { ProjectArtifactRenderer } from "./berd-project-cube/ProjectArtifactRenderer"; import type { ProjectArtifactProjection, @@ -958,8 +974,168 @@ function TaskDetailPanel({ ); } +type MessagePanelId = "sidebar" | "chat" | "thread"; +type MessagePanelLayout = { x: number; y: number; detached: boolean }; + +const INITIAL_MESSAGE_LAYOUT: Record = { + sidebar: { x: 0, y: 0, detached: false }, + chat: { x: 0, y: 0, detached: false }, + thread: { x: 0, y: 0, detached: false }, +}; + +function MessageComposer({ compact = false }: { compact?: boolean }) { + return ( +
+ Message #buzz-design +
+
+
+ ); +} + +function MessagesView() { + const [layouts, setLayouts] = useState(INITIAL_MESSAGE_LAYOUT); + const dragRef = useRef<{ + id: MessagePanelId; + pointerId: number; + startX: number; + startY: number; + originX: number; + originY: number; + cell: HTMLElement; + x: number; + y: number; + } | null>(null); + + const beginPanelDrag = ( + event: ReactPointerEvent, + id: MessagePanelId, + ) => { + if ((event.target as Element).closest("button, input")) return; + const cell = event.currentTarget.closest(".message-panel-cell") as HTMLElement | null; + if (!cell) return; + event.currentTarget.setPointerCapture(event.pointerId); + const current = layouts[id]; + dragRef.current = { + id, + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + originX: current.x, + originY: current.y, + cell, + x: current.x, + y: current.y, + }; + cell.classList.add("is-pulling", "is-active-drag"); + }; + + const updatePanelDrag = (event: ReactPointerEvent) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + if (event.buttons === 0) { + endPanelDrag(event); + return; + } + drag.x = Math.max(-340, Math.min(340, drag.originX + event.clientX - drag.startX)); + drag.y = Math.max(-190, Math.min(190, drag.originY + event.clientY - drag.startY)); + const distance = Math.hypot(drag.x, drag.y); + drag.cell.style.setProperty("--panel-x", `${drag.x}px`); + drag.cell.style.setProperty("--panel-y", `${drag.y}px`); + drag.cell.style.setProperty("--tether-distance", `${Math.min(distance, 126)}px`); + drag.cell.style.setProperty("--tether-angle", `${Math.atan2(drag.y, drag.x)}rad`); + drag.cell.style.setProperty("--tether-squeeze", String(Math.max(0.28, 1 - distance / 170))); + drag.cell.classList.toggle("is-breaking", distance > 94); + }; + + const endPanelDrag = (event: ReactPointerEvent) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + const distance = Math.hypot(drag.x, drag.y); + const shouldAttach = distance < (layouts[drag.id].detached ? 54 : 72); + const next = shouldAttach + ? { x: 0, y: 0, detached: false } + : { x: drag.x, y: drag.y, detached: true }; + drag.cell.style.setProperty("--panel-x", `${next.x}px`); + drag.cell.style.setProperty("--panel-y", `${next.y}px`); + drag.cell.classList.remove("is-pulling", "is-breaking", "is-active-drag"); + setLayouts((current) => ({ ...current, [drag.id]: next })); + dragRef.current = null; + }; + + const panelStyle = (id: MessagePanelId) => ({ + "--panel-x": `${layouts[id].x}px`, + "--panel-y": `${layouts[id].y}px`, + }) as CSSProperties; + + const dragHandlers = (id: MessagePanelId) => ({ + onPointerDown: (event: ReactPointerEvent) => beginPanelDrag(event, id), + onPointerMove: updatePanelDrag, + onPointerUp: endPanelDrag, + onPointerCancel: endPanelDrag, + }); + + return ( +
+
panel.detached) ? " has-detached-panel" : ""}`}> +
+ + +
+ + +
+ +
+
+ ); +} + export function BuzzProjectPrototype() { - const [activeTab, setActiveTab] = useState<"me" | "projects">("me"); + const [activeTab, setActiveTab] = useState<"me" | "messages" | "projects">("me"); const [selectedTask, setSelectedTask] = useState(null); const appShellRef = useRef(null); const projectWorldRef = useRef(null); @@ -973,7 +1149,7 @@ export function BuzzProjectPrototype() { y: 0, }); - const showTab = (tab: "me" | "projects") => { + const showTab = (tab: "me" | "messages" | "projects") => { setSelectedTask(null); setActiveTab(tab); }; @@ -1044,13 +1220,15 @@ export function BuzzProjectPrototype() {
{activeTab === "projects" ? ( + ) : activeTab === "messages" ? ( + ) : (
div:first-child, +.thread-header > div:first-child { display: flex; align-items: center; gap: 10px; min-width: 0; } +.chat-header strong, +.thread-header strong { overflow: hidden; font-size: 14px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } +.chat-header-actions { display: flex; align-items: center; gap: 8px; } +.chat-header-actions button, +.thread-header button { display: inline-flex; height: 36px; min-width: 36px; align-items: center; justify-content: center; gap: 7px; padding: 0 9px; border: 1px solid rgba(38,38,38,.12); border-radius: 9px; background: rgba(255,255,255,.72); cursor: pointer; } +.thread-header button { border: 0; background: transparent; } +.chat-header-actions button:hover, +.thread-header button:hover { background: rgba(38,38,38,.05); } +.chat-feed { flex: 1; overflow: auto; padding: 48px 20px 120px; } +.day-divider { display: flex; align-items: center; gap: 14px; margin-bottom: 46px; color: rgba(38,38,38,.68); font-size: 11px; } +.day-divider::before, +.day-divider::after { content: ""; height: 1px; flex: 1; background: rgba(38,38,38,.1); } +.day-divider span { padding: 8px 13px; border: 1px solid rgba(38,38,38,.12); border-radius: 999px; } +.chat-message, +.thread-message { display: grid; grid-template-columns: 42px 1fr; gap: 12px; margin-bottom: 30px; font-size: 14px; line-height: 1.42; } +.chat-message > img, +.thread-message > img { width: 42px; height: 42px; object-fit: contain; border-radius: 50%; } +.chat-message p, +.thread-message p { margin: 0 0 6px; } +.chat-message p strong, +.thread-message p strong { margin-right: 10px; font-weight: 650; } +.chat-message p span, +.thread-message p span { color: rgba(38,38,38,.65); } +.reply-link { display: flex; align-items: center; gap: 8px; margin-top: 12px; padding: 0; border: 0; color: #262626; background: transparent; cursor: pointer; font-size: 13px; } +.reply-link > span:last-child { color: rgba(38,38,38,.42); } +.mini-avatars { display: flex; padding-left: 10px; } +.mini-avatars img { width: 26px; height: 26px; margin-left: -10px; object-fit: contain; border: 2px solid #fff; border-radius: 50%; } +.message-composer { position: absolute; z-index: 4; left: 20px; right: 20px; bottom: 18px; min-height: 112px; padding: 18px; border: 1px solid rgba(38,38,38,.13); border-radius: 18px; color: rgba(38,38,38,.38); background: rgba(255,255,255,.84); box-shadow: 0 8px 28px rgba(57,78,87,.04); backdrop-filter: blur(18px); font-size: 14px; } +.message-composer-actions { position: absolute; left: 18px; right: 18px; bottom: 14px; display: flex; align-items: center; gap: 14px; color: #262626; } +.message-composer-actions button { width: 34px; height: 34px; margin-left: auto; display: grid; place-items: center; border: 0; border-radius: 50%; color: #262626; background: #f3e7ff; cursor: pointer; } + +.thread-content { flex: 1; overflow: auto; padding: 28px 20px 130px; } +.thread-message { grid-template-columns: 40px 1fr; } +.thread-message > img { width: 40px; height: 40px; } +.thread-panel .message-composer { left: 18px; right: 18px; } + +@media (max-width: 1050px) { + .messages-board { grid-template-columns: 215px minmax(370px, 1fr) 290px; } + .chat-header-actions button:nth-child(2), .chat-header-actions button:nth-child(3) { display: none; } +} + .projects-stage { position: relative; height: 100svh; @@ -317,6 +453,7 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif @media (prefers-reduced-motion: reduce) { .wrapped-labels { will-change: auto; } .orbit-system, .task-detail-panel { transition-duration: 1ms; animation-duration: 1ms; } + .message-panel, .liquid-tether { transition: none; } .response-composer { transition: none; } .response-composer textarea { animation: none; } .project-name-pill { transition: none; }