feat(desktop): add project cube prototype
Signed-off-by: cynfria <yescynthia@gmail.com>
40
prototypes/project-cube-orbit/.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.vinext/
|
||||
/out/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
/dist/
|
||||
/.wrangler/
|
||||
/outputs/
|
||||
/work/
|
||||
21
prototypes/project-cube-orbit/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Buzz Project Orbit
|
||||
|
||||
An isolated interaction prototype for exploring project cubes and task noodles
|
||||
in Buzz. It reuses the Berd project cube renderer, model, textures, and agent
|
||||
avatars.
|
||||
|
||||
The prototype includes:
|
||||
|
||||
- Manual 3D cube rotation with noodles that adapt to the visible cube edges.
|
||||
- Draggable task noodles and task detail interactions.
|
||||
- A pannable multi-project canvas.
|
||||
- A swipeable Projects view with glass task cards and expandable task details.
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open the local URL printed by Vite. Use `npm run build` for a production build.
|
||||
@@ -0,0 +1,3 @@
|
||||
import cubeModelUrl from "./assets/models/Cube_A_v001.gltf?url";
|
||||
|
||||
export const PROJECT_ARTIFACT_CUBE_MODEL_URL = cubeModelUrl;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type * as THREE from "three";
|
||||
|
||||
const CUBE_TEXTURE_CLONE_FLAG = "projectArtifactRendererCubeTextureClone";
|
||||
const SOURCE_TEXTURE_UUID = "sourceTextureUuid";
|
||||
|
||||
interface ProjectArtifactTextureUserData {
|
||||
[CUBE_TEXTURE_CLONE_FLAG]?: boolean;
|
||||
[SOURCE_TEXTURE_UUID]?: string;
|
||||
}
|
||||
|
||||
function textureUserData(
|
||||
texture: THREE.Texture,
|
||||
): ProjectArtifactTextureUserData {
|
||||
return texture.userData as ProjectArtifactTextureUserData;
|
||||
}
|
||||
|
||||
function isCubeTextureClone(
|
||||
texture: THREE.Texture | null | undefined,
|
||||
): texture is THREE.Texture {
|
||||
return Boolean(texture && textureUserData(texture)[CUBE_TEXTURE_CLONE_FLAG]);
|
||||
}
|
||||
|
||||
function createCubeTextureClone(texture: THREE.Texture) {
|
||||
const clone = texture.clone();
|
||||
const userData = textureUserData(clone);
|
||||
userData[CUBE_TEXTURE_CLONE_FLAG] = true;
|
||||
userData[SOURCE_TEXTURE_UUID] = texture.uuid;
|
||||
return clone;
|
||||
}
|
||||
|
||||
function disposeCubeTextureClone(material: THREE.MeshBasicMaterial) {
|
||||
if (!isCubeTextureClone(material.map)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const texture = material.map;
|
||||
material.map = null;
|
||||
material.needsUpdate = true;
|
||||
texture.dispose();
|
||||
}
|
||||
|
||||
export function applyProjectArtifactCubeTexture({
|
||||
cloneForUvTransform,
|
||||
material,
|
||||
texture,
|
||||
}: {
|
||||
cloneForUvTransform: boolean;
|
||||
material: THREE.MeshBasicMaterial;
|
||||
texture: THREE.Texture;
|
||||
}) {
|
||||
if (!cloneForUvTransform) {
|
||||
if (material.map !== texture) {
|
||||
disposeCubeTextureClone(material);
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
if (!isCubeTextureClone(material.map)) {
|
||||
material.map = createCubeTextureClone(texture);
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
|
||||
const clonedMap = material.map;
|
||||
const userData = textureUserData(clonedMap);
|
||||
if (userData[SOURCE_TEXTURE_UUID] !== texture.uuid) {
|
||||
clonedMap.image = texture.image;
|
||||
clonedMap.colorSpace = texture.colorSpace;
|
||||
clonedMap.needsUpdate = true;
|
||||
userData[SOURCE_TEXTURE_UUID] = texture.uuid;
|
||||
}
|
||||
|
||||
return clonedMap;
|
||||
}
|
||||
|
||||
export function disposeProjectArtifactCubeMaterials(
|
||||
materials: readonly THREE.MeshBasicMaterial[],
|
||||
) {
|
||||
materials.forEach((material) => {
|
||||
disposeCubeTextureClone(material);
|
||||
material.dispose();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import type { Window as TauriWindow } from "@tauri-apps/api/window";
|
||||
|
||||
export function isDocumentVisible(): boolean {
|
||||
return (
|
||||
typeof document === "undefined" || document.visibilityState !== "hidden"
|
||||
);
|
||||
}
|
||||
|
||||
async function isTauriWindowVisible(appWindow: TauriWindow): Promise<boolean> {
|
||||
const [isVisible, isMinimized] = await Promise.all([
|
||||
appWindow.isVisible().catch(() => true),
|
||||
appWindow.isMinimized().catch(() => false),
|
||||
]);
|
||||
|
||||
return isVisible && !isMinimized;
|
||||
}
|
||||
|
||||
export async function resolveRenderWindowVisible(
|
||||
appWindow: TauriWindow | null,
|
||||
): Promise<boolean> {
|
||||
if (!isDocumentVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!appWindow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isTauriWindowVisible(appWindow);
|
||||
}
|
||||
|
||||
type VisibilityListener = () => void;
|
||||
type Unlisten = () => void;
|
||||
|
||||
let currentVisibility = isDocumentVisible();
|
||||
let appWindow: TauriWindow | null = null;
|
||||
let stopTracking: Unlisten | null = null;
|
||||
let requestSequence = 0;
|
||||
const listeners = new Set<VisibilityListener>();
|
||||
|
||||
function getRenderWindowVisibleSnapshot(): boolean {
|
||||
return currentVisibility;
|
||||
}
|
||||
|
||||
function setRenderWindowVisibleSnapshot(nextVisibility: boolean): void {
|
||||
if (currentVisibility === nextVisibility) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentVisibility = nextVisibility;
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function syncRenderWindowVisibility(): void {
|
||||
const requestId = ++requestSequence;
|
||||
const requestWindow = appWindow;
|
||||
|
||||
void resolveRenderWindowVisible(requestWindow)
|
||||
.then((nextVisibility) => {
|
||||
if (requestId !== requestSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRenderWindowVisibleSnapshot(nextVisibility && isDocumentVisible());
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== requestSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRenderWindowVisibleSnapshot(isDocumentVisible());
|
||||
});
|
||||
}
|
||||
|
||||
function startTrackingRenderWindowVisibility(): void {
|
||||
if (stopTracking) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stopped = false;
|
||||
let unlistenFocus: Unlisten | undefined;
|
||||
let unlistenResize: Unlisten | undefined;
|
||||
const syncVisibility = () => syncRenderWindowVisibility();
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("visibilitychange", syncVisibility);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("focus", syncVisibility);
|
||||
window.addEventListener("blur", syncVisibility);
|
||||
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
void import("@tauri-apps/api/window")
|
||||
.then(async ({ getCurrentWindow }) => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
appWindow = getCurrentWindow();
|
||||
const [focusUnlisten, resizeUnlisten] = await Promise.all([
|
||||
appWindow.onFocusChanged(syncVisibility),
|
||||
appWindow.onResized(syncVisibility),
|
||||
]);
|
||||
|
||||
if (stopped) {
|
||||
focusUnlisten();
|
||||
resizeUnlisten();
|
||||
return;
|
||||
}
|
||||
|
||||
unlistenFocus = focusUnlisten;
|
||||
unlistenResize = resizeUnlisten;
|
||||
syncVisibility();
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stopped) {
|
||||
syncVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
syncVisibility();
|
||||
|
||||
stopTracking = () => {
|
||||
stopped = true;
|
||||
requestSequence += 1;
|
||||
appWindow = null;
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.removeEventListener("visibilitychange", syncVisibility);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("focus", syncVisibility);
|
||||
window.removeEventListener("blur", syncVisibility);
|
||||
}
|
||||
|
||||
unlistenFocus?.();
|
||||
unlistenResize?.();
|
||||
stopTracking = null;
|
||||
};
|
||||
}
|
||||
|
||||
function subscribeRenderWindowVisible(listener: VisibilityListener): Unlisten {
|
||||
listeners.add(listener);
|
||||
startTrackingRenderWindowVisibility();
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
|
||||
if (listeners.size === 0) {
|
||||
stopTracking?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useRenderWindowVisible(): boolean {
|
||||
return useSyncExternalStore(
|
||||
subscribeRenderWindowVisible,
|
||||
getRenderWindowVisibleSnapshot,
|
||||
isDocumentVisible,
|
||||
);
|
||||
}
|
||||
78
prototypes/project-cube-orbit/app/berd-project-cube/types.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export type ProjectArtifactMood =
|
||||
| "dormant"
|
||||
| "awakening"
|
||||
| "serene"
|
||||
| "active"
|
||||
| "energetic"
|
||||
| "contemplative";
|
||||
|
||||
export type ProjectArtifactContentMode =
|
||||
| "planes"
|
||||
| "sphere"
|
||||
| "cube"
|
||||
| "cubeStatic";
|
||||
|
||||
export interface ProjectArtifactInput {
|
||||
projectId?: string | null;
|
||||
name: string;
|
||||
prompt?: string | null;
|
||||
color?: string | null;
|
||||
workingDirs?: string[];
|
||||
sessionCount?: number;
|
||||
artifact?: ProjectArtifactMetadata | null;
|
||||
}
|
||||
|
||||
export interface ProjectArtifactState {
|
||||
seed: number;
|
||||
name: string;
|
||||
accentColor: string;
|
||||
accentCssColor: string;
|
||||
mood: ProjectArtifactMood;
|
||||
moodIntensity: number;
|
||||
contentMode: ProjectArtifactContentMode;
|
||||
}
|
||||
|
||||
export interface ProjectArtifactMetadata {
|
||||
seed: number;
|
||||
color: string;
|
||||
mood: ProjectArtifactMood;
|
||||
moodIntensity: number;
|
||||
contentMode: ProjectArtifactContentMode;
|
||||
}
|
||||
|
||||
export interface ProjectArtifactMotionImpulse {
|
||||
sequence: number;
|
||||
deltaX: number;
|
||||
deltaY: number;
|
||||
}
|
||||
|
||||
export interface ProjectArtifactProjection {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
/** Clockwise screen-space silhouette of the rendered cube shell. */
|
||||
contour?: Array<{ x: number; y: number }>;
|
||||
}
|
||||
|
||||
export interface ProjectArtifactRendererProps {
|
||||
state: ProjectArtifactState;
|
||||
imageUrls: string[];
|
||||
environmentUrl: string;
|
||||
className?: string;
|
||||
variant?: "preview" | "tile";
|
||||
motionImpulse?: ProjectArtifactMotionImpulse;
|
||||
/** Springs the cube toward a side-facing task-detail orientation. */
|
||||
focusSide?: boolean;
|
||||
/** Pauses continuous rendering while the home canvas drags/resizes the widget. */
|
||||
gestureFreezeActive?: boolean;
|
||||
/** Pauses continuous rendering while a mounted preview is not visible. */
|
||||
renderPaused?: boolean;
|
||||
onGlCanvasReady?: (canvas: HTMLCanvasElement) => void;
|
||||
/** Reports the rendered cube shell bounds and silhouette in canvas pixels. */
|
||||
onCubeProjection?: (projection: ProjectArtifactProjection) => void;
|
||||
/** Pulls the camera back to provide extra framing around oversized embeds. */
|
||||
cameraDistanceScale?: number;
|
||||
}
|
||||
|
||||
export type ProjectArtifactPinState = { projectId: string };
|
||||
1038
prototypes/project-cube-orbit/app/buzz-project-prototype.tsx
Normal file
305
prototypes/project-cube-orbit/app/globals.css
Normal file
@@ -0,0 +1,305 @@
|
||||
:root { --ink: #262626; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #root { margin: 0; min-height: 100%; }
|
||||
button { font: inherit; color: inherit; }
|
||||
body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; overflow: hidden; }
|
||||
|
||||
.app-shell {
|
||||
min-height: 100svh;
|
||||
background:
|
||||
radial-gradient(circle at 7% 42%, rgba(255,255,255,.92) 0 7%, transparent 30%),
|
||||
radial-gradient(circle at 91% 89%, rgba(248,247,198,.84), transparent 30%),
|
||||
linear-gradient(112deg, #f6f8f8 0%, #e4f1f6 42%, #c7e4f1 100%);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: fixed;
|
||||
inset: 0 0 auto;
|
||||
z-index: 50;
|
||||
height: 66px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.segmented { display: flex; height: 42px; align-items: center; gap: 2px; padding: 3px; border: 1px solid rgba(255,255,255,.9); border-radius: 24px; background: rgba(226,239,245,.38); box-shadow: inset 0 1px 0 rgba(255,255,255,.35); backdrop-filter: blur(18px) saturate(115%); }
|
||||
.segmented button { height: 32px; padding: 0 19px; border: 0; border-radius: 18px; background: transparent; cursor: pointer; font-size: 14px; line-height: 1; }
|
||||
.segmented button:hover { background: rgba(255,255,255,.44); }
|
||||
.segmented .selected { min-width: 78px; background: rgba(255,255,255,.78); box-shadow: 0 1px 5px rgba(46,68,79,.055); }
|
||||
.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: 48px; height: 48px; 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); }
|
||||
.search-icon { display: block; width: 18px; height: 18px; }
|
||||
.avatar { width: 46px; height: 46px; 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); }
|
||||
|
||||
.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); will-change: transform; }
|
||||
.orbit-system {
|
||||
--orbit-scale: 1;
|
||||
--focus-x: 0px;
|
||||
--focus-y: 0px;
|
||||
position: absolute;
|
||||
width: 760px;
|
||||
height: 720px;
|
||||
transform: translate(-50%, -50%) translate(var(--focus-x), var(--focus-y)) scale(var(--orbit-scale));
|
||||
transition: left 520ms cubic-bezier(.22,1,.36,1), top 520ms cubic-bezier(.22,1,.36,1), transform 520ms cubic-bezier(.22,1,.36,1), opacity 360ms ease, filter 420ms ease;
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
.orbit-primary { left: 30%; top: 55%; }
|
||||
.orbit-secondary { left: 71%; top: 39%; --orbit-scale: .58; }
|
||||
.orbit-tertiary { left: 62%; top: 96%; --orbit-scale: .6; }
|
||||
.orbit-system.is-selected-project { left: 26%; top: 54%; z-index: 18; }
|
||||
.orbit-secondary.is-selected-project,
|
||||
.orbit-tertiary.is-selected-project { --orbit-scale: .82; }
|
||||
.orbit-system.is-dimmed { opacity: .22; filter: blur(8px) saturate(.72); pointer-events: none; }
|
||||
.orbit-primary.is-dimmed { --focus-x: -90px; --focus-y: 14px; }
|
||||
.orbit-secondary.is-dimmed { --focus-x: 110px; --focus-y: -42px; }
|
||||
.orbit-tertiary.is-dimmed { --focus-x: 86px; --focus-y: 70px; }
|
||||
.canvas-wrap { position: absolute; z-index: 8; width: 650px; height: 650px; left: 55px; top: 35px; }
|
||||
.project-name-pill {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
left: 380px;
|
||||
top: 520px;
|
||||
min-width: 76px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255,255,255,.82);
|
||||
border-radius: 999px;
|
||||
color: #262626;
|
||||
background: rgba(255,255,255,.68);
|
||||
box-shadow: 0 8px 24px rgba(58,81,91,.1), inset 0 1px 0 rgba(255,255,255,.7);
|
||||
backdrop-filter: blur(16px) saturate(120%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
transform: translate(-50%, -50%) translateY(6px) scale(.97);
|
||||
transition: opacity 160ms ease-out, transform 160ms ease-out;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.project-name-pill.is-visible { opacity: 1; transform: translate(-50%, -50%) translateY(0) scale(1); }
|
||||
|
||||
.wrapped-labels {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
transform-origin: 380px 360px;
|
||||
will-change: transform;
|
||||
z-index: 9;
|
||||
}
|
||||
.label-ribbon { pointer-events: auto; cursor: grab; outline: none; touch-action: none; }
|
||||
.label-ribbon.is-dragging { cursor: grabbing; }
|
||||
.noodle-hit-area { fill: none; stroke: transparent; stroke-width: 88px; pointer-events: stroke; }
|
||||
.ribbon-stroke {
|
||||
fill: none;
|
||||
stroke: rgba(255,255,255,.96);
|
||||
stroke-width: 60px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 9px 18px rgba(73,101,112,.035));
|
||||
transition: stroke 280ms ease, filter 280ms ease;
|
||||
}
|
||||
.ribbon-text {
|
||||
fill: #262626;
|
||||
font-size: 27px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -.01em;
|
||||
dominant-baseline: middle;
|
||||
transition: fill 280ms ease;
|
||||
}
|
||||
.noodle-avatar {
|
||||
filter: drop-shadow(0 3px 5px rgba(35, 43, 46, 0.12));
|
||||
}
|
||||
.label-ribbon:hover .ribbon-stroke,
|
||||
.label-ribbon:focus-visible .ribbon-stroke { filter: drop-shadow(0 10px 22px rgba(73,101,112,.13)); }
|
||||
.label-ribbon.is-selected .ribbon-stroke { stroke: var(--task-accent); filter: drop-shadow(0 12px 25px color-mix(in srgb, var(--task-accent) 42%, transparent)); }
|
||||
.label-ribbon.is-selected .ribbon-text { fill: #fff; }
|
||||
|
||||
.projects-stage {
|
||||
position: relative;
|
||||
height: 100svh;
|
||||
min-height: 720px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scroll-padding-top: 66px;
|
||||
padding: 66px 0 88px;
|
||||
background-color: #f8e4de;
|
||||
background-image:
|
||||
radial-gradient(circle, rgba(118,98,92,.16) 1px, transparent 1.2px),
|
||||
radial-gradient(circle at 52% 24%, rgba(255,211,207,.76), transparent 42%),
|
||||
radial-gradient(circle at 88% 88%, rgba(255,235,181,.72), transparent 34%);
|
||||
background-size: 44px 44px, auto, auto;
|
||||
}
|
||||
.projects-cube-row { --carousel-drag-x: 0px; position: sticky; top: 66px; z-index: 1; height: 520px; }
|
||||
.projects-gallery-item {
|
||||
position: absolute;
|
||||
top: 54%;
|
||||
width: 560px;
|
||||
height: 560px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
opacity: 0;
|
||||
transform: translate(calc(-50% + var(--carousel-drag-x)), -50%) scale(.58);
|
||||
transition: left 460ms cubic-bezier(.645,.045,.355,1), top 460ms cubic-bezier(.645,.045,.355,1), transform 460ms cubic-bezier(.645,.045,.355,1), opacity 300ms ease;
|
||||
will-change: left, top, transform, opacity;
|
||||
}
|
||||
.projects-gallery-item.slot-active { left: 50%; top: 44%; z-index: 3; opacity: 1; transform: translate(calc(-50% + var(--carousel-drag-x)), -50%) scale(1); }
|
||||
.projects-gallery-item.slot-neighbor { z-index: 1; opacity: .84; transform: translate(calc(-50% + var(--carousel-drag-x)), -50%) scale(.7); }
|
||||
.projects-gallery-item.slot-far { z-index: 0; pointer-events: none; }
|
||||
.projects-cube-row.is-dragging .projects-gallery-item { transition: none; }
|
||||
.projects-carousel-drag-surface { position: absolute; inset: 0; z-index: 5; cursor: grab; touch-action: pan-y; outline: none; }
|
||||
.projects-carousel-drag-surface:active { cursor: grabbing; }
|
||||
.projects-carousel-drag-surface:focus-visible::after { content: ""; position: absolute; left: 50%; top: 44%; width: 360px; height: 360px; border: 2px solid rgba(38,38,38,.42); border-radius: 32%; pointer-events: none; transform: translate(-50%, -50%); }
|
||||
|
||||
.projects-info { position: relative; z-index: 8; width: min(700px, calc(100vw - 40px)); margin: -24px auto 0; }
|
||||
.projects-info-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
||||
.projects-filter,
|
||||
.projects-active-name { min-height: 36px; padding: 0 18px; border: 1px solid rgba(255,255,255,.78); border-radius: 999px; color: #262626; background: rgba(255,255,255,.76); box-shadow: inset 0 1px 0 rgba(255,255,255,.72); backdrop-filter: blur(16px) saturate(115%); font-size: 14px; }
|
||||
.projects-filter { display: inline-flex; align-items: center; gap: 14px; cursor: pointer; }
|
||||
.projects-filter svg { flex: 0 0 auto; }
|
||||
.projects-active-name { display: inline-flex; align-items: center; justify-content: center; min-width: 78px; }
|
||||
.projects-task-list { display: grid; gap: 10px; }
|
||||
.projects-task-card { position: relative; overflow: hidden; border: 1px solid rgba(255,255,255,.9); border-radius: 22px; background: linear-gradient(145deg, rgba(255,255,255,.68), rgba(255,255,255,.5)); box-shadow: 0 14px 32px rgba(91,70,67,.075), inset 0 1px 0 rgba(255,255,255,.82); -webkit-backdrop-filter: blur(28px) saturate(125%); backdrop-filter: blur(28px) saturate(125%); transition: border-radius 260ms ease, box-shadow 260ms ease, transform 260ms cubic-bezier(.645,.045,.355,1), background 260ms ease; }
|
||||
.projects-task-card.is-expanded { border-radius: 28px; background: linear-gradient(145deg, rgba(255,255,255,.75), rgba(255,255,255,.57)); box-shadow: 0 22px 44px rgba(91,70,67,.12), inset 0 1px 0 rgba(255,255,255,.9); }
|
||||
.projects-task-summary { display: grid; width: 100%; min-height: 62px; grid-template-columns: 18px 1fr auto; align-items: center; gap: 12px; padding: 0 28px; border: 0; background: transparent; text-align: left; cursor: pointer; }
|
||||
.task-status-icon { color: rgba(38,38,38,.75); }
|
||||
.projects-task-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; font-size: 14px; }
|
||||
.projects-task-title small { color: rgba(38,38,38,.48); font-size: 11px; font-weight: 400; }
|
||||
.task-expand-mark { transition: opacity 150ms ease, transform 260ms cubic-bezier(.645,.045,.355,1); }
|
||||
.projects-task-card.is-expanded .task-expand-mark { opacity: 0; transform: rotate(180deg); }
|
||||
.projects-task-expansion { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms cubic-bezier(.645,.045,.355,1); }
|
||||
.projects-task-card.is-expanded .projects-task-expansion { grid-template-rows: 1fr; }
|
||||
.projects-task-detail { min-height: 0; overflow: hidden; padding: 0 30px; opacity: 0; transition: opacity 170ms ease, padding 300ms cubic-bezier(.645,.045,.355,1); }
|
||||
.projects-task-card.is-expanded .projects-task-detail { padding: 4px 30px 28px; opacity: 1; }
|
||||
.projects-task-context-copy { max-width: 510px; padding-right: 100px; }
|
||||
.projects-task-detail span { display: block; margin-bottom: 8px; color: rgba(38,38,38,.42); font-size: 14px; }
|
||||
.projects-task-detail p { margin: 0; color: #262626; font-size: 14px; line-height: 1.45; }
|
||||
.projects-task-detail-bottom { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; margin-top: 26px; }
|
||||
.projects-task-detail-bottom > div > p + p { margin-top: 6px; }
|
||||
.projects-agent-row { display: flex; align-items: flex-end; gap: 10px; }
|
||||
.projects-agent-row img { width: 40px; height: 40px; object-fit: contain; }
|
||||
.projects-task-cta { position: absolute; top: 15px; right: 28px; min-height: 38px; padding: 0 18px; border: 0; border-radius: 999px; color: #fff; background: var(--project-accent); cursor: pointer; font-size: 14px; opacity: 0; transform: translateY(-4px); transition: opacity 170ms ease, transform 200ms ease-out; pointer-events: none; }
|
||||
.projects-task-card.is-expanded .projects-task-cta { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
||||
|
||||
.task-detail-panel {
|
||||
position: absolute;
|
||||
z-index: 35;
|
||||
left: calc(72% + 16px);
|
||||
top: 54%;
|
||||
width: min(47vw, 790px);
|
||||
min-height: 540px;
|
||||
padding: 28px 34px 32px;
|
||||
border: 1px solid rgba(255,255,255,.92);
|
||||
border-radius: 32px;
|
||||
color: #262626;
|
||||
background: rgba(248,251,252,.85);
|
||||
box-shadow: 0 22px 45px rgba(62,88,101,.16), inset 0 1px 0 rgba(255,255,255,.72);
|
||||
backdrop-filter: blur(28px) saturate(108%);
|
||||
transform: translate(-50%, -50%);
|
||||
animation: task-panel-enter 440ms cubic-bezier(.22,1,.36,1) both;
|
||||
user-select: text;
|
||||
}
|
||||
.task-detail-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
|
||||
.task-detail-header h2 { margin: 0; font-size: 20px; line-height: 1.2; font-weight: 400; letter-spacing: -.015em; }
|
||||
.task-detail-close { width: 34px; height: 34px; padding: 0; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; cursor: pointer; }
|
||||
.task-detail-close:hover { background: rgba(255,255,255,.72); }
|
||||
.task-detail-divider { height: 1px; margin: 28px 0 36px; background: rgba(30,45,52,.1); }
|
||||
.task-meta-grid { display: grid; grid-template-columns: minmax(0, 1.8fr) minmax(110px, .8fr) minmax(130px, .9fr); gap: 28px; }
|
||||
.task-meta { min-width: 0; }
|
||||
.task-meta span,
|
||||
.task-context > span,
|
||||
.task-actions > span { display: block; margin-bottom: 10px; color: rgba(31,40,44,.38); font-size: 14px; line-height: 1.35; }
|
||||
.task-meta strong { display: block; font-size: 14px; line-height: 1.4; font-weight: 400; overflow-wrap: anywhere; }
|
||||
.task-context { margin-top: 36px; }
|
||||
.task-context p { max-width: 680px; margin: 0; font-size: 14px; line-height: 1.45; }
|
||||
.task-actions { margin-top: 38px; }
|
||||
.task-action-list { display: flex; width: 100%; flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||
.task-action-list button { min-height: 40px; padding: 0 18px; border: 1px solid rgba(30,45,52,.04); border-radius: 22px; background: rgba(255,255,255,.9); box-shadow: 0 2px 6px rgba(44,62,70,.045); cursor: pointer; font-size: 14px; }
|
||||
.task-action-list button:hover { background: #fff; transform: translateY(-1px); }
|
||||
.task-action-list button:active { transform: translateY(0); }
|
||||
.response-composer {
|
||||
width: 145px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(30,45,52,.04);
|
||||
border-radius: 22px;
|
||||
background: rgba(255,255,255,.92);
|
||||
box-shadow: 0 2px 6px rgba(44,62,70,.045);
|
||||
transition: width 280ms cubic-bezier(.645,.045,.355,1), height 280ms cubic-bezier(.645,.045,.355,1), border-radius 280ms cubic-bezier(.645,.045,.355,1), box-shadow 280ms ease;
|
||||
will-change: width, height, border-radius;
|
||||
}
|
||||
.response-composer.is-open { width: 100%; height: 160px; border-radius: 24px; box-shadow: 0 4px 10px rgba(44,62,70,.065); }
|
||||
.task-action-list .response-composer-trigger { width: 100%; height: 100%; min-height: 0; border: 0; box-shadow: none; background: transparent; white-space: nowrap; }
|
||||
.task-action-list .response-composer-trigger:hover { background: #fff; }
|
||||
.response-composer textarea { display: block; width: 100%; height: 100%; padding: 24px; border: 0; outline: 0; resize: none; color: #262626; background: transparent; font: inherit; font-size: 14px; line-height: 1.45; animation: response-field-enter 180ms 90ms ease-out both; }
|
||||
.response-composer textarea::placeholder { color: rgba(31,40,44,.38); opacity: 1; }
|
||||
@keyframes response-field-enter {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes task-panel-enter {
|
||||
from { opacity: 0; transform: translate(-50%, -48%) scale(.975); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.orbit-primary { left: 34%; --orbit-scale: .76; }
|
||||
.orbit-secondary { left: 77%; top: 37%; --orbit-scale: .5; }
|
||||
.orbit-tertiary { left: 65%; top: 91%; --orbit-scale: .5; }
|
||||
.orbit-system.is-selected-project { left: 23%; top: 54%; --orbit-scale: .58; }
|
||||
.task-detail-panel { left: calc(71% + 16px); width: 52vw; min-height: 500px; padding: 26px 28px; }
|
||||
.task-meta-grid { grid-template-columns: 1fr 1fr; }
|
||||
.task-meta-path { grid-column: 1 / -1; }
|
||||
.projects-gallery-item { width: 500px; height: 500px; }
|
||||
.projects-info { margin-top: -44px; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.topbar { grid-template-columns: 1fr auto; height: 68px; padding: 0 14px; }
|
||||
.header-spacer { display: none; }
|
||||
.segmented { justify-self: start; }
|
||||
.segmented button { height: 32px; padding: 0 10px; font-size: 13px; }
|
||||
.utilities .round-button { display: none; }
|
||||
.avatar { width: 38px; height: 38px; }
|
||||
.orbit-primary { left: 50%; top: 27%; --orbit-scale: .38; }
|
||||
.orbit-secondary { left: 50%; top: 58%; --orbit-scale: .34; }
|
||||
.orbit-tertiary { left: 50%; top: 88%; --orbit-scale: .34; }
|
||||
.orbit-system.is-selected-project { left: 50%; top: 31%; --orbit-scale: .4; }
|
||||
.orbit-system.is-dimmed { opacity: .12; filter: blur(6px); }
|
||||
.task-detail-panel { left: 50%; top: auto; bottom: 12px; width: calc(100vw - 24px); min-height: 53vh; max-height: 65vh; overflow: auto; padding: 22px; border-radius: 26px; transform: translateX(-50%); animation-name: task-panel-enter-mobile; }
|
||||
.task-detail-header h2 { font-size: 20px; }
|
||||
.task-detail-divider { margin: 18px 0 24px; }
|
||||
.task-meta-grid { grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.task-meta span, .task-context > span, .task-actions > span { font-size: 14px; }
|
||||
.task-meta strong, .task-context p, .task-action-list button { font-size: 14px; }
|
||||
.task-context, .task-actions { margin-top: 24px; }
|
||||
.projects-stage { min-height: 100svh; padding-top: 70px; }
|
||||
.projects-cube-row { top: 68px; height: 390px; }
|
||||
.projects-gallery-item { width: 400px; height: 400px; transform: translate(calc(-50% + var(--carousel-drag-x)), -50%) scale(.46); }
|
||||
.projects-gallery-item.slot-active { top: 43%; transform: translate(calc(-50% + var(--carousel-drag-x)), -50%) scale(.78); }
|
||||
.projects-gallery-item.slot-neighbor { transform: translate(calc(-50% + var(--carousel-drag-x)), -50%) scale(.56); }
|
||||
.projects-info { width: calc(100vw - 24px); margin-top: -18px; }
|
||||
.projects-task-summary { padding: 0 18px; }
|
||||
.projects-task-card.is-expanded .projects-task-detail { padding: 2px 18px 22px; }
|
||||
.projects-task-context-copy { padding-right: 0; }
|
||||
.projects-task-detail-bottom { grid-template-columns: 1fr; gap: 20px; }
|
||||
.projects-task-cta { position: static; margin-top: 24px; }
|
||||
}
|
||||
|
||||
@keyframes task-panel-enter-mobile {
|
||||
from { opacity: 0; transform: translate(-50%, 18px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wrapped-labels { will-change: auto; }
|
||||
.orbit-system, .task-detail-panel { transition-duration: 1ms; animation-duration: 1ms; }
|
||||
.response-composer { transition: none; }
|
||||
.response-composer textarea { animation: none; }
|
||||
.project-name-pill { transition: none; }
|
||||
.projects-gallery-item, .projects-task-card, .projects-task-expansion, .projects-task-detail, .projects-task-cta { transition: none; }
|
||||
}
|
||||
11
prototypes/project-cube-orbit/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#e4f2f7" />
|
||||
<meta name="description" content="An experimental Buzz project cube with task activity in orbit." />
|
||||
<title>Buzz Project Orbit</title>
|
||||
</head>
|
||||
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
|
||||
</html>
|
||||
1733
prototypes/project-cube-orbit/package-lock.json
generated
Normal file
33
prototypes/project-cube-orbit/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "buzz-project-orbit-prototype",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "vite build",
|
||||
"start": "vite preview --host 127.0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "10.7.8",
|
||||
"@react-three/fiber": "9.7.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.34.0",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"three": "0.185.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.19",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "6.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "8.0.13"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 286 KiB |
87
prototypes/project-cube-orbit/public/cube.gltf
Normal file
6
prototypes/project-cube-orbit/public/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 19.2727C22 20.779 20.779 22 19.2727 22H14.7273C13.221 22 12 20.779 12 19.2727V12H19.2727C20.779 12 22 13.221 22 14.7273V19.2727Z" fill="#68C4FF"/>
|
||||
<path d="M20 2C21.1046 2 22 2.89543 22 4V7C22 8.10457 21.1046 9 20 9H17C15.8954 9 15 8.10457 15 7V4C15 2.89543 15.8954 2 17 2H20Z" fill="#0C79D8"/>
|
||||
<path d="M7 15C8.10457 15 9 15.8954 9 17V20C9 21.1046 8.10457 22 7 22H4C2.89543 22 2 21.1046 2 20V17C2 15.8954 2.89543 15 4 15H7Z" fill="#0C79D8"/>
|
||||
<path d="M12 12H4.72727C3.22104 12 2 10.779 2 9.27273V4.72727C2 3.22104 3.22104 2 4.72727 2H9.27273C10.779 2 12 3.22104 12 4.72727V12Z" fill="#2E9EFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 712 B |
8
prototypes/project-cube-orbit/src/main.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BuzzProjectPrototype } from "../app/buzz-project-prototype";
|
||||
import "../app/globals.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode><BuzzProjectPrototype /></StrictMode>,
|
||||
);
|
||||
9
prototypes/project-cube-orbit/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_INTERNALS__?: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
21
prototypes/project-cube-orbit/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["app/**/*.ts", "app/**/*.tsx", "src/**/*.ts", "src/**/*.tsx", "vite.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
7
prototypes/project-cube-orbit/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { watch: { useFsEvents: false, usePolling: true } },
|
||||
});
|
||||