feat: 3d ofis sahnesi ve ekip karakterlerini ekle
This commit is contained in:
883
package-lock.json
generated
883
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,14 @@
|
||||
"start": "NODE_ENV=production node server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^9.122.0",
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"node-pty": "^1.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"strip-ansi": "^7.1.0"
|
||||
"strip-ansi": "^7.1.0",
|
||||
"three": "^0.161.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
||||
BIN
web/public/apple-logo.png
Normal file
BIN
web/public/apple-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
web/public/mona.png
Normal file
BIN
web/public/mona.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
BIN
web/public/steve.png
Normal file
BIN
web/public/steve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
@@ -7,11 +7,16 @@ import TeamBoard from "./components/TeamBoard.jsx";
|
||||
import ToastStack from "./components/ToastStack.jsx";
|
||||
import { useSocket } from "./hooks/useSocket.js";
|
||||
import { useSession } from "./hooks/useSession.js";
|
||||
import { createInitialOfficeAgents } from "./office/officeAgents.js";
|
||||
import { parseOfficeCommand } from "./office/officeCommands.js";
|
||||
import { getZoneById } from "./office/officeZones.js";
|
||||
|
||||
export default function App() {
|
||||
const { socket, connected } = useSocket();
|
||||
const { session, chat, startSession, clearProject, sendPrompt, selectProject, clearError, toasts, dismissToast } = useSession(socket);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [teamView, setTeamView] = useState("board");
|
||||
const [officeAgents, setOfficeAgents] = useState(() => createInitialOfficeAgents());
|
||||
const autoStartedRef = useRef(false);
|
||||
|
||||
async function runAction(action) {
|
||||
@@ -38,6 +43,45 @@ export default function App() {
|
||||
}
|
||||
}, [connected, session.status]);
|
||||
|
||||
function handleOfficeCommand(prompt) {
|
||||
const command = parseOfficeCommand(prompt);
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const zone = getZoneById(command.zoneId);
|
||||
const officeAgent = officeAgents[command.agentId];
|
||||
if (!zone || !officeAgent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setOfficeAgents((current) => ({
|
||||
...current,
|
||||
[command.agentId]: {
|
||||
...current[command.agentId],
|
||||
zoneId: command.zoneId,
|
||||
targetPosition: zone.approachPosition
|
||||
}
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAgentArrive(agentId, position) {
|
||||
setOfficeAgents((current) => ({
|
||||
...current,
|
||||
[agentId]: {
|
||||
...current[agentId],
|
||||
currentPosition: position
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function handlePromptSubmit(prompt) {
|
||||
handleOfficeCommand(prompt);
|
||||
await runAction(() => sendPrompt(prompt));
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<div className="app-shell__header">
|
||||
@@ -60,7 +104,13 @@ export default function App() {
|
||||
<ShellFrame>
|
||||
<div className="console-grid">
|
||||
<div className="console-grid__side">
|
||||
<TeamBoard chat={chat} />
|
||||
<TeamBoard
|
||||
chat={chat}
|
||||
view={teamView}
|
||||
onViewChange={setTeamView}
|
||||
officeAgents={officeAgents}
|
||||
onAgentArrive={handleAgentArrive}
|
||||
/>
|
||||
</div>
|
||||
<div className="console-grid__main">
|
||||
<ChatStream
|
||||
@@ -77,7 +127,7 @@ export default function App() {
|
||||
/>
|
||||
<PromptComposer
|
||||
disabled={busy || session.status !== "running" || !session.teamActivated || !session.currentProjectPath}
|
||||
onSubmit={(prompt) => runAction(() => sendPrompt(prompt))}
|
||||
onSubmit={handlePromptSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function PixelButton({ tone = "green", disabled, children, ...props }) {
|
||||
export default function PixelButton({ tone = "green", disabled, className = "", children, ...props }) {
|
||||
return (
|
||||
<button className={`pixel-button pixel-button--${tone}`} disabled={disabled} {...props}>
|
||||
<button className={`pixel-button pixel-button--${tone} ${className}`.trim()} disabled={disabled} {...props}>
|
||||
<span>{children}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
import PanelFrame from "./PanelFrame.jsx";
|
||||
import PixelButton from "./PixelButton.jsx";
|
||||
import { parseTeamFeed } from "../lib/teamFeed.js";
|
||||
|
||||
const OfficeCanvas = lazy(() => import("../office/OfficeCanvas.jsx"));
|
||||
|
||||
function TeamCard({ member }) {
|
||||
return (
|
||||
<article className="team-card">
|
||||
@@ -33,16 +37,52 @@ function TeamCard({ member }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamBoard({ chat }) {
|
||||
export default function TeamBoard({ chat, view = "board", onViewChange, officeAgents, onAgentArrive }) {
|
||||
const members = parseTeamFeed(chat);
|
||||
const isOfficeView = view === "office";
|
||||
|
||||
return (
|
||||
<PanelFrame title="Team Comms Board" eyebrow="ROLE SIGNALS" className="team-board-panel">
|
||||
<div className="team-board">
|
||||
{members.map((member) => (
|
||||
<TeamCard key={member.id} member={member} />
|
||||
))}
|
||||
</div>
|
||||
<PanelFrame
|
||||
title="Team Comms Board"
|
||||
eyebrow="ROLE SIGNALS"
|
||||
className={`team-board-panel team-board-panel--${view}`}
|
||||
headerExtra={
|
||||
<div className="team-board__tabs" role="tablist" aria-label="Team Comms Board view">
|
||||
<PixelButton
|
||||
tone={view === "board" ? "green" : "cyan"}
|
||||
className={`team-board__tab ${view === "board" ? "is-active" : ""}`}
|
||||
onClick={() => onViewChange?.("board")}
|
||||
aria-pressed={view === "board"}
|
||||
>
|
||||
Board
|
||||
</PixelButton>
|
||||
<span className="team-board__divider" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
<PixelButton
|
||||
tone={view === "office" ? "red" : "cyan"}
|
||||
className={`team-board__tab ${view === "office" ? "is-active" : ""}`}
|
||||
onClick={() => onViewChange?.("office")}
|
||||
aria-pressed={view === "office"}
|
||||
>
|
||||
Office
|
||||
</PixelButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isOfficeView ? (
|
||||
<div className="team-board__office" aria-label="Office view">
|
||||
<Suspense fallback={<div className="team-board__office-fallback" />}>
|
||||
<OfficeCanvas debug={false} agents={officeAgents} onAgentArrive={onAgentArrive} />
|
||||
</Suspense>
|
||||
</div>
|
||||
) : (
|
||||
<div className="team-board">
|
||||
{members.map((member) => (
|
||||
<TeamCard key={member.id} member={member} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
|
||||
241
web/src/office/OfficeAgent.jsx
Normal file
241
web/src/office/OfficeAgent.jsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Html, RoundedBox } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
function nearlyEqual(a, b, epsilon = 0.04) {
|
||||
return Math.abs(a - b) < epsilon;
|
||||
}
|
||||
|
||||
function positionsMatch(current, target) {
|
||||
return nearlyEqual(current[0], target[0]) && nearlyEqual(current[1], target[1]) && nearlyEqual(current[2], target[2]);
|
||||
}
|
||||
|
||||
function HairStyle({ style, color }) {
|
||||
if (style === "bob") {
|
||||
return (
|
||||
<group>
|
||||
<mesh castShadow position={[0, 1.77, 0.01]}>
|
||||
<boxGeometry args={[0.46, 0.16, 0.44]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.19, 1.63, -0.01]}>
|
||||
<boxGeometry args={[0.09, 0.24, 0.36]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.19, 1.63, -0.01]}>
|
||||
<boxGeometry args={[0.09, 0.24, 0.36]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "swept") {
|
||||
return (
|
||||
<group>
|
||||
<mesh castShadow position={[0, 1.76, 0.02]}>
|
||||
<boxGeometry args={[0.4, 0.12, 0.42]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.09, 1.82, 0.06]} rotation={[0, 0, -0.38]}>
|
||||
<boxGeometry args={[0.22, 0.06, 0.32]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "sidePart") {
|
||||
return (
|
||||
<group>
|
||||
<mesh castShadow position={[0, 1.76, 0.02]}>
|
||||
<boxGeometry args={[0.42, 0.12, 0.42]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.08, 1.8, 0.06]} rotation={[0, 0, 0.22]}>
|
||||
<boxGeometry args={[0.18, 0.05, 0.28]} />
|
||||
<meshStandardMaterial color={color} roughness={0.95} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "crew") {
|
||||
return (
|
||||
<mesh castShadow position={[0, 1.75, 0.03]}>
|
||||
<boxGeometry args={[0.36, 0.08, 0.36]} />
|
||||
<meshStandardMaterial color={color} roughness={0.98} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<mesh castShadow position={[0, 1.76, 0.04]}>
|
||||
<boxGeometry args={[0.4, 0.12, 0.42]} />
|
||||
<meshStandardMaterial color={color} roughness={0.96} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfficeAgent({ agent, onArrive }) {
|
||||
const groupRef = useRef(null);
|
||||
const lastArrivalKeyRef = useRef("");
|
||||
const targetRef = useRef(new Vector3(...agent.targetPosition));
|
||||
const appearance = {
|
||||
skinColor: "#f0c6a9",
|
||||
hairColor: "#171717",
|
||||
hairStyle: "short",
|
||||
shirtColor: "#fbfbf4",
|
||||
tieColor: null,
|
||||
lowerColor: "#6d7078",
|
||||
shoeColor: "#111111",
|
||||
lowerType: "pants",
|
||||
...agent.appearance
|
||||
};
|
||||
|
||||
const isSkirt = appearance.lowerType === "skirt";
|
||||
const isShorts = appearance.lowerType === "shorts";
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
groupRef.current.position.set(...agent.currentPosition);
|
||||
}, [agent.currentPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
targetRef.current.set(...agent.targetPosition);
|
||||
}, [agent.targetPosition]);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (!groupRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const speed = Math.min(1, delta * 2.4);
|
||||
groupRef.current.position.lerp(targetRef.current, speed);
|
||||
|
||||
const current = [
|
||||
groupRef.current.position.x,
|
||||
groupRef.current.position.y,
|
||||
groupRef.current.position.z
|
||||
];
|
||||
|
||||
const arrivalKey = `${agent.id}:${agent.targetPosition.join(",")}`;
|
||||
if (positionsMatch(current, agent.targetPosition) && lastArrivalKeyRef.current !== arrivalKey) {
|
||||
lastArrivalKeyRef.current = arrivalKey;
|
||||
onArrive?.(agent.id, agent.targetPosition);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
<HairStyle style={appearance.hairStyle} color={appearance.hairColor} />
|
||||
<mesh castShadow position={[0, 1.55, 0]}>
|
||||
<boxGeometry args={[0.42, 0.42, 0.42]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.95} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.08, 1.58, 0.222]}>
|
||||
<boxGeometry args={[0.08, 0.012, 0.016]} />
|
||||
<meshStandardMaterial color="#101010" roughness={0.4} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.08, 1.58, 0.222]}>
|
||||
<boxGeometry args={[0.08, 0.012, 0.016]} />
|
||||
<meshStandardMaterial color="#101010" roughness={0.4} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.08, 1.65, 0.214]}>
|
||||
<boxGeometry args={[0.1, 0.018, 0.02]} />
|
||||
<meshStandardMaterial color="#222222" roughness={0.7} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.08, 1.65, 0.214]}>
|
||||
<boxGeometry args={[0.1, 0.018, 0.02]} />
|
||||
<meshStandardMaterial color="#222222" roughness={0.7} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0, 1.5, 0.22]} rotation={[Math.PI / 2.4, 0, 0]}>
|
||||
<coneGeometry args={[0.035, 0.13, 4]} />
|
||||
<meshStandardMaterial color="#deaf90" roughness={0.88} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0, 1.39, 0.222]}>
|
||||
<boxGeometry args={[0.12, 0.012, 0.016]} />
|
||||
<meshStandardMaterial color="#101010" roughness={0.5} />
|
||||
</mesh>
|
||||
<RoundedBox castShadow receiveShadow position={[0, 1.13, 0]} args={[0.62, 0.58, 0.32]} radius={0.04} smoothness={2}>
|
||||
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
|
||||
</RoundedBox>
|
||||
{appearance.tieColor ? (
|
||||
<mesh castShadow position={[0, 1.12, 0.17]}>
|
||||
<boxGeometry args={[0.11, 0.48, 0.03]} />
|
||||
<meshStandardMaterial color={appearance.tieColor} roughness={0.64} />
|
||||
</mesh>
|
||||
) : null}
|
||||
<mesh castShadow position={[-0.44, 1.15, 0]}>
|
||||
<boxGeometry args={[0.22, 0.16, 0.18]} />
|
||||
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.44, 1.15, 0]}>
|
||||
<boxGeometry args={[0.22, 0.16, 0.18]} />
|
||||
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.44, 0.93, 0]}>
|
||||
<boxGeometry args={[0.14, 0.28, 0.14]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.92} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.44, 0.93, 0]}>
|
||||
<boxGeometry args={[0.14, 0.28, 0.14]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.92} />
|
||||
</mesh>
|
||||
{isSkirt ? (
|
||||
<mesh castShadow position={[0, 0.76, 0]}>
|
||||
<boxGeometry args={[0.54, 0.22, 0.34]} />
|
||||
<meshStandardMaterial color={appearance.lowerColor} roughness={0.86} />
|
||||
</mesh>
|
||||
) : (
|
||||
<mesh castShadow position={[0, 0.76, 0]}>
|
||||
<boxGeometry args={[0.5, 0.22, 0.3]} />
|
||||
<meshStandardMaterial color={appearance.lowerColor} roughness={0.86} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh castShadow position={[-0.14, 0.41, 0]}>
|
||||
<boxGeometry args={[0.14, 0.48, 0.14]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.14, 0.41, 0]}>
|
||||
<boxGeometry args={[0.14, 0.48, 0.14]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.14, 0.62, 0]}>
|
||||
<boxGeometry args={[0.16, 0.08, 0.16]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.14, 0.62, 0]}>
|
||||
<boxGeometry args={[0.16, 0.08, 0.16]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
|
||||
</mesh>
|
||||
{isShorts ? (
|
||||
<>
|
||||
<mesh castShadow position={[-0.14, 0.58, 0]}>
|
||||
<boxGeometry args={[0.16, 0.08, 0.16]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.14, 0.58, 0]}>
|
||||
<boxGeometry args={[0.16, 0.08, 0.16]} />
|
||||
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
|
||||
</mesh>
|
||||
</>
|
||||
) : null}
|
||||
<mesh castShadow position={[-0.14, 0.11, 0.02]}>
|
||||
<boxGeometry args={[0.16, 0.12, 0.24]} />
|
||||
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.14, 0.11, 0.02]}>
|
||||
<boxGeometry args={[0.16, 0.12, 0.24]} />
|
||||
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
|
||||
</mesh>
|
||||
<Html position={[0, 2.08, 0]} center distanceFactor={10}>
|
||||
<div className="office-agent__label">{agent.name}</div>
|
||||
</Html>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
18
web/src/office/OfficeCamera.jsx
Normal file
18
web/src/office/OfficeCamera.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
|
||||
export default function OfficeCamera() {
|
||||
return (
|
||||
<OrbitControls
|
||||
enablePan={false}
|
||||
enableDamping
|
||||
dampingFactor={0.08}
|
||||
minDistance={12}
|
||||
maxDistance={20}
|
||||
minPolarAngle={0.7}
|
||||
maxPolarAngle={1.02}
|
||||
minAzimuthAngle={-0.85}
|
||||
maxAzimuthAngle={0.85}
|
||||
target={[0.5, 0.8, 0.2]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
web/src/office/OfficeCanvas.jsx
Normal file
12
web/src/office/OfficeCanvas.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import OfficeScene from "./OfficeScene.jsx";
|
||||
|
||||
export default function OfficeCanvas({ debug = false, agents = {}, onAgentArrive }) {
|
||||
return (
|
||||
<div className="office-canvas">
|
||||
<Canvas shadows dpr={[1, 1.5]} camera={{ position: [12, 12, 12], fov: 38 }}>
|
||||
<OfficeScene debug={debug} agents={agents} onAgentArrive={onAgentArrive} />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
web/src/office/OfficeDebugLabels.jsx
Normal file
16
web/src/office/OfficeDebugLabels.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
|
||||
export default function OfficeDebugLabels({ zones = [] }) {
|
||||
return (
|
||||
<group>
|
||||
{zones.map((zone) => (
|
||||
<Html key={zone.id} position={[zone.position[0], 1.8, zone.position[2]]} center transform sprite>
|
||||
<div className="office-debug-label">
|
||||
<span>{zone.label}</span>
|
||||
<code>{zone.id}</code>
|
||||
</div>
|
||||
</Html>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
8
web/src/office/OfficeFloor.jsx
Normal file
8
web/src/office/OfficeFloor.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function OfficeFloor() {
|
||||
return (
|
||||
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.001, 0]}>
|
||||
<planeGeometry args={[24, 18]} />
|
||||
<meshStandardMaterial color="#efe0c5" roughness={0.95} metalness={0.02} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
76
web/src/office/OfficeLayout.jsx
Normal file
76
web/src/office/OfficeLayout.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useLoader } from "@react-three/fiber";
|
||||
import { TextureLoader } from "three";
|
||||
import Desk from "./primitives/Desk.jsx";
|
||||
import MeetingTable from "./primitives/MeetingTable.jsx";
|
||||
import CoffeeMachine from "./primitives/CoffeeMachine.jsx";
|
||||
import { OFFICE_ZONES } from "./officeZones.js";
|
||||
|
||||
function AccentWallPanels() {
|
||||
const steveTexture = useLoader(TextureLoader, "/steve.png");
|
||||
const monaTexture = useLoader(TextureLoader, "/mona.png");
|
||||
const panels = [
|
||||
{ position: [-8.2, 1.8, -8.78], color: "#495b8a" },
|
||||
{ position: [0, 2.15, -8.76], color: "#6b4c78" },
|
||||
{ position: [7.4, 1.8, -8.78], color: "#495b8a" }
|
||||
];
|
||||
|
||||
return (
|
||||
<group>
|
||||
<mesh position={[-6.2, 1.47, -8.73]}>
|
||||
<planeGeometry args={[1.456, 2.184]} />
|
||||
<meshBasicMaterial
|
||||
map={monaTexture}
|
||||
transparent
|
||||
alphaTest={0.05}
|
||||
toneMapped={false}
|
||||
/>
|
||||
</mesh>
|
||||
<mesh position={[0, 1.47, -8.73]}>
|
||||
<planeGeometry args={[3.24, 2.184]} />
|
||||
<meshBasicMaterial
|
||||
map={steveTexture}
|
||||
transparent
|
||||
alphaTest={0.05}
|
||||
toneMapped={false}
|
||||
/>
|
||||
</mesh>
|
||||
{[panels[2]].map((panel, index) => (
|
||||
<mesh key={index} position={panel.position}>
|
||||
<boxGeometry args={[1, 0.55, 0.05]} />
|
||||
<meshStandardMaterial color={panel.color} roughness={0.55} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function FloorDecals() {
|
||||
return (
|
||||
<group>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[5.8, 0.002, 4]}>
|
||||
<circleGeometry args={[1.15, 24]} />
|
||||
<meshStandardMaterial color="#d9ccb6" roughness={1} />
|
||||
</mesh>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[6.4, 0.002, -4]}>
|
||||
<circleGeometry args={[2.15, 28]} />
|
||||
<meshStandardMaterial color="#ded0ba" roughness={1} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfficeLayout() {
|
||||
return (
|
||||
<group>
|
||||
<FloorDecals />
|
||||
<AccentWallPanels />
|
||||
<Desk position={OFFICE_ZONES[0].position} nameplateColor="#d6c15d" />
|
||||
<Desk position={OFFICE_ZONES[1].position} nameplateColor="#49c2f1" />
|
||||
<Desk position={OFFICE_ZONES[2].position} nameplateColor="#7bd87a" />
|
||||
<Desk position={OFFICE_ZONES[3].position} nameplateColor="#f48cc7" />
|
||||
<Desk position={OFFICE_ZONES[4].position} nameplateColor="#8aa4ff" />
|
||||
<MeetingTable position={OFFICE_ZONES[5].position} />
|
||||
<CoffeeMachine position={OFFICE_ZONES[6].position} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
21
web/src/office/OfficeLighting.jsx
Normal file
21
web/src/office/OfficeLighting.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function OfficeLighting() {
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={1.6} />
|
||||
<hemisphereLight args={["#fff4de", "#17110d", 1.15]} />
|
||||
<directionalLight
|
||||
castShadow
|
||||
intensity={1.9}
|
||||
position={[7, 12, 5]}
|
||||
shadow-mapSize-width={1024}
|
||||
shadow-mapSize-height={1024}
|
||||
shadow-camera-near={0.5}
|
||||
shadow-camera-far={40}
|
||||
shadow-camera-left={-16}
|
||||
shadow-camera-right={16}
|
||||
shadow-camera-top={16}
|
||||
shadow-camera-bottom={-16}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
web/src/office/OfficeScene.jsx
Normal file
26
web/src/office/OfficeScene.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import OfficeCamera from "./OfficeCamera.jsx";
|
||||
import OfficeLighting from "./OfficeLighting.jsx";
|
||||
import OfficeFloor from "./OfficeFloor.jsx";
|
||||
import OfficeWalls from "./OfficeWalls.jsx";
|
||||
import OfficeLayout from "./OfficeLayout.jsx";
|
||||
import OfficeDebugLabels from "./OfficeDebugLabels.jsx";
|
||||
import OfficeAgent from "./OfficeAgent.jsx";
|
||||
import { OFFICE_ZONES } from "./officeZones.js";
|
||||
|
||||
export default function OfficeScene({ debug = false, agents = {}, onAgentArrive }) {
|
||||
return (
|
||||
<>
|
||||
<color attach="background" args={["#120d0a"]} />
|
||||
<fog attach="fog" args={["#120d0a", 18, 34]} />
|
||||
<OfficeLighting />
|
||||
<OfficeFloor />
|
||||
<OfficeWalls />
|
||||
<OfficeLayout />
|
||||
{Object.values(agents).map((agent) => (
|
||||
<OfficeAgent key={agent.id} agent={agent} onArrive={onAgentArrive} />
|
||||
))}
|
||||
<OfficeCamera />
|
||||
{debug ? <OfficeDebugLabels zones={OFFICE_ZONES} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
web/src/office/OfficeWalls.jsx
Normal file
19
web/src/office/OfficeWalls.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
function Wall({ position, args }) {
|
||||
return (
|
||||
<mesh castShadow receiveShadow position={position}>
|
||||
<boxGeometry args={args} />
|
||||
<meshStandardMaterial color="#8f8b93" roughness={0.92} metalness={0.04} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfficeWalls() {
|
||||
return (
|
||||
<group>
|
||||
<Wall position={[0, 1.3, -9]} args={[24, 2.6, 0.35]} />
|
||||
<Wall position={[-12, 1.3, 0]} args={[0.35, 2.6, 18]} />
|
||||
<Wall position={[12, 1.3, 0]} args={[0.35, 2.6, 18]} />
|
||||
<Wall position={[0, 1.3, 9]} args={[24, 2.6, 0.35]} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
113
web/src/office/officeAgents.js
Normal file
113
web/src/office/officeAgents.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { getZoneById } from "./officeZones.js";
|
||||
|
||||
function createAgent({
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
zoneId,
|
||||
color,
|
||||
appearance
|
||||
}) {
|
||||
const zone = getZoneById(zoneId);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
color,
|
||||
zoneId,
|
||||
currentPosition: zone?.approachPosition ?? [0, 0, 0],
|
||||
targetPosition: zone?.approachPosition ?? [0, 0, 0],
|
||||
appearance
|
||||
};
|
||||
}
|
||||
|
||||
export function createInitialOfficeAgents() {
|
||||
return {
|
||||
teamLead: createAgent({
|
||||
id: "teamLead",
|
||||
name: "Mazlum",
|
||||
role: "Team Lead",
|
||||
zoneId: "teamLeadDesk",
|
||||
color: "#e0c15c",
|
||||
appearance: {
|
||||
skinColor: "#f0c6a9",
|
||||
hairColor: "#171717",
|
||||
hairStyle: "short",
|
||||
shirtColor: "#fbfbf4",
|
||||
tieColor: "#ba1d2f",
|
||||
lowerColor: "#75777d",
|
||||
shoeColor: "#111111",
|
||||
lowerType: "shorts"
|
||||
}
|
||||
}),
|
||||
frontend: createAgent({
|
||||
id: "frontend",
|
||||
name: "Berkecan",
|
||||
role: "Frontend Developer",
|
||||
zoneId: "frontendDesk",
|
||||
color: "#49c2f1",
|
||||
appearance: {
|
||||
skinColor: "#e8c09b",
|
||||
hairColor: "#3e2415",
|
||||
hairStyle: "swept",
|
||||
shirtColor: "#3d79d8",
|
||||
tieColor: null,
|
||||
lowerColor: "#d7c8a4",
|
||||
shoeColor: "#3b2d25",
|
||||
lowerType: "pants"
|
||||
}
|
||||
}),
|
||||
backend: createAgent({
|
||||
id: "backend",
|
||||
name: "Simsar",
|
||||
role: "Backend Developer",
|
||||
zoneId: "backendDesk",
|
||||
color: "#7bd87a",
|
||||
appearance: {
|
||||
skinColor: "#dcb28f",
|
||||
hairColor: "#2a2a2a",
|
||||
hairStyle: "crew",
|
||||
shirtColor: "#2f6b48",
|
||||
tieColor: null,
|
||||
lowerColor: "#42464f",
|
||||
shoeColor: "#141414",
|
||||
lowerType: "pants"
|
||||
}
|
||||
}),
|
||||
uiux: createAgent({
|
||||
id: "uiux",
|
||||
name: "Aybuke",
|
||||
role: "UI/UX Designer",
|
||||
zoneId: "uiuxDesk",
|
||||
color: "#f48cc7",
|
||||
appearance: {
|
||||
skinColor: "#efc4aa",
|
||||
hairColor: "#6b3f30",
|
||||
hairStyle: "bob",
|
||||
shirtColor: "#d8a0cf",
|
||||
tieColor: null,
|
||||
lowerColor: "#564760",
|
||||
shoeColor: "#2f2430",
|
||||
lowerType: "skirt"
|
||||
}
|
||||
}),
|
||||
ios: createAgent({
|
||||
id: "ios",
|
||||
name: "Ive",
|
||||
role: "iOS Developer",
|
||||
zoneId: "iosDesk",
|
||||
color: "#8aa4ff",
|
||||
appearance: {
|
||||
skinColor: "#e6bc97",
|
||||
hairColor: "#7b4d22",
|
||||
hairStyle: "sidePart",
|
||||
shirtColor: "#d4b258",
|
||||
tieColor: null,
|
||||
lowerColor: "#5a77ae",
|
||||
shoeColor: "#171717",
|
||||
lowerType: "pants"
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
50
web/src/office/officeCommands.js
Normal file
50
web/src/office/officeCommands.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const AGENT_ALIASES = {
|
||||
teamLead: ["team lead", "lead", "mazlum", "mazlum bey"],
|
||||
frontend: ["frontend", "frontend developer", "berkecan"],
|
||||
backend: ["backend", "backend developer", "simsar"],
|
||||
uiux: ["ui ux", "ui/ux", "designer", "ui ux designer", "aybuke", "aybuke hanim", "aybüke", "aybüke hanım"],
|
||||
ios: ["ios", "ios developer", "ive", "i os"]
|
||||
};
|
||||
|
||||
const ZONE_ALIASES = {
|
||||
teamLeadDesk: ["team lead desk", "lead desk", "mazlum desk", "team lead masasi", "mazlum masasi"],
|
||||
frontendDesk: ["frontend desk", "frontend masasi", "berkecan masasi"],
|
||||
backendDesk: ["backend desk", "backend masasi", "simsar masasi"],
|
||||
uiuxDesk: ["ui ux desk", "ui/ux desk", "designer desk", "aybuke masasi", "aybüke masası", "ui ux masasi"],
|
||||
iosDesk: ["ios desk", "ios masasi", "ive masasi", "i os masasi"],
|
||||
meetingTable: ["meeting table", "toplanti masasi", "toplanti masasi", "meeting room", "toplanti"],
|
||||
coffeeMachine: ["coffee machine", "kahve makinesi", "kahve alani", "kahve makinasi"]
|
||||
};
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value ?? "")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
|
||||
function findMatch(normalizedText, map) {
|
||||
return Object.entries(map).find(([, aliases]) => aliases.some((alias) => normalizedText.includes(alias)))?.[0] ?? null;
|
||||
}
|
||||
|
||||
export function parseOfficeCommand(prompt) {
|
||||
const normalized = normalizeText(prompt);
|
||||
|
||||
if (!/\b(git|gidebilir|gitsin|gidin|toplan|yuru|yurusun|gitmesi)\b/.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const agentId = findMatch(normalized, AGENT_ALIASES);
|
||||
const zoneId = findMatch(normalized, ZONE_ALIASES);
|
||||
|
||||
if (!agentId || !zoneId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "move",
|
||||
agentId,
|
||||
zoneId
|
||||
};
|
||||
}
|
||||
13
web/src/office/officeSceneData.js
Normal file
13
web/src/office/officeSceneData.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export const officeBadges = [
|
||||
{ id: "lead", label: "Mazlum Bey", tone: "amber" },
|
||||
{ id: "frontend", label: "Frontend Kanka", tone: "cyan" },
|
||||
{ id: "backend", label: "Backend Kanka", tone: "green" },
|
||||
{ id: "ios", label: "iOS Kanka", tone: "cyan" },
|
||||
{ id: "ui", label: "UI Hanim", tone: "amber" }
|
||||
];
|
||||
|
||||
export const officeNotes = [
|
||||
"Patron icin hizli briefing hazir.",
|
||||
"3D sahne: masa, ekranlar, duvar notlari, neon akis.",
|
||||
"Office modu sadece gorunum degistirir; veri akisi ayni kalir."
|
||||
];
|
||||
8
web/src/office/officeStations.js
Normal file
8
web/src/office/officeStations.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const OFFICE_STATIONS = [
|
||||
{ id: "mazlum", name: "Mazlum Bey", role: "Team Lead", debug: "BRIEFING NODE", x: 548, y: 196, tone: "amber" },
|
||||
{ id: "berkecan", name: "Berkecan", role: "Frontend", debug: "UI BENCH", x: 264, y: 392, tone: "cyan" },
|
||||
{ id: "simsar", name: "Simsar", role: "Backend", debug: "API BENCH", x: 820, y: 388, tone: "green" },
|
||||
{ id: "aybuke", name: "UI Hanim", role: "Design Wall", debug: "UX BOARD", x: 250, y: 190, tone: "amber" },
|
||||
{ id: "ive", name: "Ive", role: "iOS Bay", debug: "MOBILE RACK", x: 842, y: 178, tone: "cyan" },
|
||||
{ id: "irgatov", name: "Irgatov", role: "Coffee Station", debug: "SERVICE DESK", x: 548, y: 562, tone: "red" }
|
||||
];
|
||||
62
web/src/office/officeZones.js
Normal file
62
web/src/office/officeZones.js
Normal file
@@ -0,0 +1,62 @@
|
||||
export const OFFICE_ZONES = [
|
||||
{
|
||||
id: "teamLeadDesk",
|
||||
label: "Team Lead Desk",
|
||||
role: "Team Lead",
|
||||
type: "desk",
|
||||
position: [0, 0, -4.2],
|
||||
approachPosition: [0, 0, -2.9]
|
||||
},
|
||||
{
|
||||
id: "frontendDesk",
|
||||
label: "Frontend Desk",
|
||||
role: "Frontend Dev",
|
||||
type: "desk",
|
||||
position: [-3.8, 0, -0.9],
|
||||
approachPosition: [-2.7, 0, 0.3]
|
||||
},
|
||||
{
|
||||
id: "backendDesk",
|
||||
label: "Backend Desk",
|
||||
role: "Backend Dev",
|
||||
type: "desk",
|
||||
position: [3.8, 0, -0.9],
|
||||
approachPosition: [2.7, 0, 0.3]
|
||||
},
|
||||
{
|
||||
id: "uiuxDesk",
|
||||
label: "UI/UX Desk",
|
||||
role: "UI/UX Designer",
|
||||
type: "desk",
|
||||
position: [-3.8, 0, 2.9],
|
||||
approachPosition: [-2.7, 0, 1.8]
|
||||
},
|
||||
{
|
||||
id: "iosDesk",
|
||||
label: "iOS Desk",
|
||||
role: "iOS Dev",
|
||||
type: "desk",
|
||||
position: [3.8, 0, 2.9],
|
||||
approachPosition: [2.7, 0, 1.8]
|
||||
},
|
||||
{
|
||||
id: "meetingTable",
|
||||
label: "Meeting Table",
|
||||
role: "Meeting",
|
||||
type: "meeting",
|
||||
position: [6.4, 0, -4],
|
||||
approachPosition: [5, 0, -3.9]
|
||||
},
|
||||
{
|
||||
id: "coffeeMachine",
|
||||
label: "Coffee Machine",
|
||||
role: "Utility",
|
||||
type: "utility",
|
||||
position: [6.8, 0, 4],
|
||||
approachPosition: [5.7, 0, 4]
|
||||
}
|
||||
];
|
||||
|
||||
export function getZoneById(id) {
|
||||
return OFFICE_ZONES.find((zone) => zone.id === id) ?? null;
|
||||
}
|
||||
22
web/src/office/primitives/Chair.jsx
Normal file
22
web/src/office/primitives/Chair.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function Chair({ position = [0, 0, 0], rotation = [0, 0, 0] }) {
|
||||
return (
|
||||
<group position={position} rotation={rotation}>
|
||||
<mesh castShadow receiveShadow position={[0, 0.42, 0]}>
|
||||
<boxGeometry args={[0.7, 0.12, 0.7]} />
|
||||
<meshStandardMaterial color="#6e719c" roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 0.9, -0.24]}>
|
||||
<boxGeometry args={[0.7, 0.84, 0.12]} />
|
||||
<meshStandardMaterial color="#6e719c" roughness={0.9} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 0.2, 0]}>
|
||||
<cylinderGeometry args={[0.07, 0.07, 0.42, 10]} />
|
||||
<meshStandardMaterial color="#3d4259" roughness={0.8} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 0.04, 0]}>
|
||||
<cylinderGeometry args={[0.32, 0.22, 0.08, 12]} />
|
||||
<meshStandardMaterial color="#32364a" roughness={0.8} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
143
web/src/office/primitives/CoffeeMachine.jsx
Normal file
143
web/src/office/primitives/CoffeeMachine.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
function Cup({ position = [0, 0, 0], scale = 1, sleeve = false }) {
|
||||
return (
|
||||
<group position={position} scale={scale}>
|
||||
<mesh castShadow position={[0, 0.18, 0]}>
|
||||
<cylinderGeometry args={[0.09, 0.12, 0.28, 12]} />
|
||||
<meshStandardMaterial color="#f8f8f4" roughness={0.92} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0, 0.335, 0]}>
|
||||
<cylinderGeometry args={[0.11, 0.11, 0.05, 12]} />
|
||||
<meshStandardMaterial color="#f4f4ef" roughness={0.9} />
|
||||
</mesh>
|
||||
{sleeve ? (
|
||||
<mesh castShadow position={[0, 0.18, 0.095]} rotation={[0, 0, Math.PI / 2]}>
|
||||
<boxGeometry args={[0.15, 0.08, 0.02]} />
|
||||
<meshStandardMaterial color="#8d6239" roughness={0.95} />
|
||||
</mesh>
|
||||
) : null}
|
||||
<mesh castShadow position={[0, 0.18, 0.105]} rotation={[0, 0, Math.PI / 2]}>
|
||||
<cylinderGeometry args={[0.03, 0.03, sleeve ? 0.08 : 0.1, 18]} />
|
||||
<meshStandardMaterial color="#1f8b56" roughness={0.82} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function TopCups() {
|
||||
const topCupPositions = [
|
||||
[-0.38, 1.55, -0.14],
|
||||
[-0.16, 1.55, -0.14],
|
||||
[0.06, 1.55, -0.14],
|
||||
[0.28, 1.55, -0.14],
|
||||
[-0.27, 1.55, 0.06],
|
||||
[-0.05, 1.55, 0.06],
|
||||
[0.17, 1.55, 0.06]
|
||||
];
|
||||
|
||||
return topCupPositions.map((position, index) => (
|
||||
<Cup key={index} position={position} scale={0.75} />
|
||||
));
|
||||
}
|
||||
|
||||
function FrontCups() {
|
||||
return (
|
||||
<group>
|
||||
<Cup position={[-0.28, 0.42, 0.85]} scale={1.15} />
|
||||
<Cup position={[-0.06, 0.42, 0.82]} scale={1.05} />
|
||||
<Cup position={[0.24, 0.42, 0.83]} scale={0.82} sleeve />
|
||||
<mesh castShadow position={[0.55, 0.44, 0.82]}>
|
||||
<boxGeometry args={[0.17, 0.12, 0.17]} />
|
||||
<meshStandardMaterial color="#9a6d41" roughness={0.94} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.76, 0.44, 0.78]} rotation={[0, -0.25, 0]}>
|
||||
<boxGeometry args={[0.14, 0.04, 0.16]} />
|
||||
<meshStandardMaterial color="#b18458" roughness={0.95} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CoffeeMachine({ position = [0, 0, 0] }) {
|
||||
return (
|
||||
<group position={position} scale={0.82}>
|
||||
<mesh receiveShadow position={[0, 0.22, 0.12]}>
|
||||
<boxGeometry args={[2.25, 0.36, 1.65]} />
|
||||
<meshStandardMaterial color="#6e4024" roughness={0.98} />
|
||||
</mesh>
|
||||
<mesh receiveShadow position={[0, 0.42, 0.12]}>
|
||||
<boxGeometry args={[2.05, 0.08, 1.45]} />
|
||||
<meshStandardMaterial color="#4b2918" roughness={0.95} />
|
||||
</mesh>
|
||||
|
||||
<mesh castShadow receiveShadow position={[0, 1.02, 0]}>
|
||||
<boxGeometry args={[1.55, 0.92, 0.95]} />
|
||||
<meshStandardMaterial color="#20232a" metalness={0.08} roughness={0.62} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 1.12, 0.18]}>
|
||||
<boxGeometry args={[1.42, 0.78, 0.52]} />
|
||||
<meshStandardMaterial color="#a7adb5" metalness={0.58} roughness={0.28} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 1.58, -0.04]}>
|
||||
<boxGeometry args={[1.38, 0.08, 0.82]} />
|
||||
<meshStandardMaterial color="#42464d" metalness={0.35} roughness={0.42} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 1.61, -0.04]}>
|
||||
<boxGeometry args={[1.18, 0.025, 0.62]} />
|
||||
<meshStandardMaterial color="#181a1f" roughness={0.85} />
|
||||
</mesh>
|
||||
|
||||
<mesh castShadow position={[0, 1.22, 0.45]}>
|
||||
<boxGeometry args={[0.98, 0.18, 0.06]} />
|
||||
<meshStandardMaterial color="#111315" roughness={0.55} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.26, 1.22, 0.49]}>
|
||||
<boxGeometry args={[0.26, 0.08, 0.02]} />
|
||||
<meshStandardMaterial color="#5cf0b1" emissive="#29b787" emissiveIntensity={0.3} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.26, 1.22, 0.49]}>
|
||||
<boxGeometry args={[0.26, 0.08, 0.02]} />
|
||||
<meshStandardMaterial color="#66d5ff" emissive="#3d9fd0" emissiveIntensity={0.3} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0, 1.22, 0.49]}>
|
||||
<cylinderGeometry args={[0.09, 0.09, 0.03, 18]} />
|
||||
<meshStandardMaterial color="#d8ddd8" metalness={0.35} roughness={0.25} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.6, 1.24, 0.46]}>
|
||||
<cylinderGeometry args={[0.07, 0.07, 0.12, 14]} />
|
||||
<meshStandardMaterial color="#181a1f" roughness={0.55} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.6, 1.24, 0.46]}>
|
||||
<cylinderGeometry args={[0.07, 0.07, 0.12, 14]} />
|
||||
<meshStandardMaterial color="#181a1f" roughness={0.55} />
|
||||
</mesh>
|
||||
|
||||
<mesh castShadow receiveShadow position={[0, 0.62, 0.26]}>
|
||||
<boxGeometry args={[1.15, 0.08, 0.44]} />
|
||||
<meshStandardMaterial color="#959aa1" metalness={0.58} roughness={0.25} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 0.67, 0.26]}>
|
||||
<boxGeometry args={[0.96, 0.02, 0.3]} />
|
||||
<meshStandardMaterial color="#4e5358" roughness={0.76} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.24, 0.94, 0.22]}>
|
||||
<cylinderGeometry args={[0.04, 0.03, 0.22, 12]} />
|
||||
<meshStandardMaterial color="#b5bac1" metalness={0.7} roughness={0.2} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.24, 0.94, 0.22]}>
|
||||
<cylinderGeometry args={[0.04, 0.03, 0.22, 12]} />
|
||||
<meshStandardMaterial color="#b5bac1" metalness={0.7} roughness={0.2} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[-0.24, 0.78, 0.36]}>
|
||||
<boxGeometry args={[0.24, 0.05, 0.08]} />
|
||||
<meshStandardMaterial color="#111315" roughness={0.6} />
|
||||
</mesh>
|
||||
<mesh castShadow position={[0.24, 0.78, 0.36]}>
|
||||
<boxGeometry args={[0.24, 0.05, 0.08]} />
|
||||
<meshStandardMaterial color="#111315" roughness={0.6} />
|
||||
</mesh>
|
||||
|
||||
<TopCups />
|
||||
<FrontCups />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
57
web/src/office/primitives/Desk.jsx
Normal file
57
web/src/office/primitives/Desk.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useLoader } from "@react-three/fiber";
|
||||
import { TextureLoader } from "three";
|
||||
import { DoubleSide } from "three";
|
||||
import Chair from "./Chair.jsx";
|
||||
|
||||
export default function Desk({ position = [0, 0, 0], nameplateColor = "#52b6ff" }) {
|
||||
const appleLogoTexture = useLoader(TextureLoader, "/apple-logo.png");
|
||||
|
||||
return (
|
||||
<group position={position}>
|
||||
<mesh castShadow receiveShadow position={[0, 0.78, 0]}>
|
||||
<boxGeometry args={[1.9, 0.16, 1.15]} />
|
||||
<meshStandardMaterial color="#b78256" roughness={0.86} />
|
||||
</mesh>
|
||||
{[
|
||||
[-0.78, 0.38, -0.43],
|
||||
[0.78, 0.38, -0.43],
|
||||
[-0.78, 0.38, 0.43],
|
||||
[0.78, 0.38, 0.43]
|
||||
].map((leg, index) => (
|
||||
<mesh key={index} castShadow receiveShadow position={leg}>
|
||||
<boxGeometry args={[0.12, 0.76, 0.12]} />
|
||||
<meshStandardMaterial color="#8e603b" roughness={0.9} />
|
||||
</mesh>
|
||||
))}
|
||||
|
||||
<group position={[0, 0.92, 0]} rotation={[0, Math.PI, 0]}>
|
||||
<mesh castShadow receiveShadow position={[0, 0.02, 0.02]}>
|
||||
<boxGeometry args={[0.64, 0.04, 0.42]} />
|
||||
<meshStandardMaterial color="#c3c9d2" roughness={0.36} metalness={0.52} />
|
||||
</mesh>
|
||||
<group position={[0, 0.055, -0.19]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<mesh castShadow receiveShadow position={[0, 0, 0.2]}>
|
||||
<boxGeometry args={[0.62, 0.03, 0.4]} />
|
||||
<meshStandardMaterial color="#c8ced6" roughness={0.28} metalness={0.58} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0.016, 0.2]} rotation={[-Math.PI / 2, 0, Math.PI]}>
|
||||
<planeGeometry args={[0.114, 0.114]} />
|
||||
<meshBasicMaterial
|
||||
map={appleLogoTexture}
|
||||
transparent
|
||||
alphaTest={0.1}
|
||||
side={DoubleSide}
|
||||
toneMapped={false}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<mesh castShadow receiveShadow position={[0.65, 0.9, 0.18]}>
|
||||
<boxGeometry args={[0.26, 0.1, 0.26]} />
|
||||
<meshStandardMaterial color={nameplateColor} roughness={0.55} />
|
||||
</mesh>
|
||||
<Chair position={[0, 0, -1.05]} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
14
web/src/office/primitives/MeetingTable.jsx
Normal file
14
web/src/office/primitives/MeetingTable.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function MeetingTable({ position = [0, 0, 0] }) {
|
||||
return (
|
||||
<group position={position}>
|
||||
<mesh castShadow receiveShadow position={[0, 0.8, 0]}>
|
||||
<cylinderGeometry args={[1.55, 1.55, 0.18, 28]} />
|
||||
<meshStandardMaterial color="#bf8d61" roughness={0.88} />
|
||||
</mesh>
|
||||
<mesh castShadow receiveShadow position={[0, 0.36, 0]}>
|
||||
<cylinderGeometry args={[0.22, 0.28, 0.72, 16]} />
|
||||
<meshStandardMaterial color="#8e603b" roughness={0.86} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -367,6 +367,602 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.team-board__tabs {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.team-board__tab {
|
||||
min-width: 84px;
|
||||
}
|
||||
|
||||
.team-board__tab span {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.team-board__tab.is-active span {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.team-board__divider {
|
||||
color: var(--accent-amber);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.team-board__office {
|
||||
min-height: 1006px;
|
||||
border: 0;
|
||||
background: #000;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.team-board-panel--office .panel-frame__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.team-board-panel--office .team-board__office {
|
||||
min-height: 1006px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.team-board__office-fallback,
|
||||
.office-canvas {
|
||||
width: 100%;
|
||||
height: 1006px;
|
||||
}
|
||||
|
||||
.team-board__office-fallback {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.office-debug-label {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 140px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(99, 245, 255, 0.55);
|
||||
background: rgba(6, 11, 8, 0.9);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.5rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.office-debug-label code {
|
||||
color: var(--accent-cyan);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.64rem;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.office-agent__label {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(99, 245, 255, 0.45);
|
||||
background: rgba(6, 11, 8, 0.92);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.office-scene__svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1006px;
|
||||
min-height: 1006px;
|
||||
}
|
||||
|
||||
.office-scene__station {
|
||||
shape-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
.office-scene__station-shadow {
|
||||
fill: rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
.office-scene__station-top,
|
||||
.office-scene__station-left,
|
||||
.office-scene__station-right,
|
||||
.office-scene__station-monitor,
|
||||
.office-scene__station-stand,
|
||||
.office-scene__station-plaque {
|
||||
stroke: rgba(70, 121, 84, 0.34);
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.office-scene__station--amber .office-scene__station-top,
|
||||
.office-scene__station--amber .office-scene__station-left,
|
||||
.office-scene__station--amber .office-scene__station-right,
|
||||
.office-scene__station--amber .office-scene__station-monitor,
|
||||
.office-scene__station--amber .office-scene__station-plaque {
|
||||
fill: rgba(28, 19, 7, 0.96);
|
||||
}
|
||||
|
||||
.office-scene__station--cyan .office-scene__station-top,
|
||||
.office-scene__station--cyan .office-scene__station-left,
|
||||
.office-scene__station--cyan .office-scene__station-right,
|
||||
.office-scene__station--cyan .office-scene__station-monitor,
|
||||
.office-scene__station--cyan .office-scene__station-plaque {
|
||||
fill: rgba(8, 19, 22, 0.96);
|
||||
}
|
||||
|
||||
.office-scene__station--green .office-scene__station-top,
|
||||
.office-scene__station--green .office-scene__station-left,
|
||||
.office-scene__station--green .office-scene__station-right,
|
||||
.office-scene__station--green .office-scene__station-monitor,
|
||||
.office-scene__station--green .office-scene__station-plaque {
|
||||
fill: rgba(8, 20, 12, 0.96);
|
||||
}
|
||||
|
||||
.office-scene__station--red .office-scene__station-top,
|
||||
.office-scene__station--red .office-scene__station-left,
|
||||
.office-scene__station--red .office-scene__station-right,
|
||||
.office-scene__station--red .office-scene__station-monitor,
|
||||
.office-scene__station--red .office-scene__station-plaque {
|
||||
fill: rgba(25, 10, 10, 0.96);
|
||||
}
|
||||
|
||||
.office-scene__station-stand {
|
||||
fill: rgba(20, 28, 21, 0.98);
|
||||
}
|
||||
|
||||
.office-scene__station-debug,
|
||||
.office-scene__station-name,
|
||||
.office-scene__station-role,
|
||||
.office-scene__hud-text {
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.office-scene__station-debug {
|
||||
fill: var(--accent-amber);
|
||||
font-size: 12px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.office-scene__station-name {
|
||||
fill: var(--text-main);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.office-scene__station-role {
|
||||
fill: var(--text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.office-scene__hud-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.office-scene__hud-text--amber {
|
||||
fill: var(--accent-amber);
|
||||
}
|
||||
|
||||
.office-scene__hud-text--cyan {
|
||||
fill: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.office-scene__hud-text--green {
|
||||
fill: var(--accent-green);
|
||||
}
|
||||
|
||||
.office-scene__desk-shadow {
|
||||
fill: rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.office-scene__desk-top {
|
||||
fill: url(#desk-top);
|
||||
stroke: rgba(99, 245, 255, 0.18);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.office-scene__desk-left,
|
||||
.office-scene__desk-right {
|
||||
stroke: rgba(70, 121, 84, 0.22);
|
||||
stroke-width: 1.1;
|
||||
}
|
||||
|
||||
.office-scene__monitor {
|
||||
fill: rgba(5, 16, 12, 0.98);
|
||||
stroke: rgba(99, 245, 255, 0.28);
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.office-scene__monitor-glare {
|
||||
fill: rgba(99, 245, 255, 0.12);
|
||||
}
|
||||
|
||||
.office-scene__keyboard {
|
||||
fill: rgba(18, 24, 19, 0.98);
|
||||
stroke: rgba(99, 245, 255, 0.14);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.office-scene__mug {
|
||||
fill: rgba(132, 95, 37, 0.98);
|
||||
stroke: rgba(255, 179, 71, 0.35);
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.office-scene {
|
||||
position: relative;
|
||||
min-height: 1006px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 42%, rgba(31, 61, 39, 0.18), transparent 34%),
|
||||
radial-gradient(circle at 50% 110%, rgba(0, 0, 0, 0.92), rgba(0, 0, 0, 1) 58%);
|
||||
}
|
||||
|
||||
.office-scene__ambient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
filter: blur(12px);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.office-scene__ambient--left {
|
||||
background: radial-gradient(circle at 20% 35%, rgba(99, 245, 255, 0.18), transparent 28%);
|
||||
}
|
||||
|
||||
.office-scene__ambient--right {
|
||||
background: radial-gradient(circle at 80% 30%, rgba(255, 179, 71, 0.16), transparent 30%);
|
||||
}
|
||||
|
||||
.office-scene__viewport {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
perspective: 1600px;
|
||||
}
|
||||
|
||||
.office-scene__room {
|
||||
position: relative;
|
||||
width: min(1020px, 100%);
|
||||
height: 100%;
|
||||
min-height: 1006px;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(66deg) rotateZ(-11deg) translate3d(0, 26px, 0);
|
||||
animation: office-room-float 14s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.office-scene__wall,
|
||||
.office-scene__floor,
|
||||
.office-scene__ceiling,
|
||||
.office-scene__window,
|
||||
.office-scene__board,
|
||||
.office-scene__plant,
|
||||
.office-scene__desk,
|
||||
.office-scene__monitor,
|
||||
.office-scene__chair,
|
||||
.office-scene__mug,
|
||||
.office-scene__lamp {
|
||||
position: absolute;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.office-scene__wall {
|
||||
inset: 0;
|
||||
border: 1px solid rgba(114, 255, 132, 0.08);
|
||||
background: linear-gradient(180deg, rgba(4, 7, 5, 0.96), rgba(0, 0, 0, 0.98));
|
||||
}
|
||||
|
||||
.office-scene__wall--back {
|
||||
inset: 8% 9% 42% 9%;
|
||||
transform: translateZ(-280px);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(9, 19, 11, 0.96), rgba(4, 7, 5, 0.98)),
|
||||
radial-gradient(circle at center, rgba(114, 255, 132, 0.16), transparent 62%);
|
||||
}
|
||||
|
||||
.office-scene__wall--left {
|
||||
inset: 8% auto 14% 0;
|
||||
width: 16%;
|
||||
transform-origin: left center;
|
||||
transform: rotateY(88deg) translateZ(-220px);
|
||||
background: linear-gradient(180deg, rgba(7, 13, 9, 0.98), rgba(3, 5, 4, 0.98));
|
||||
}
|
||||
|
||||
.office-scene__wall--right {
|
||||
inset: 8% 0 14% auto;
|
||||
width: 16%;
|
||||
transform-origin: right center;
|
||||
transform: rotateY(-88deg) translateZ(-220px);
|
||||
background: linear-gradient(180deg, rgba(8, 15, 10, 0.98), rgba(2, 4, 3, 0.98));
|
||||
}
|
||||
|
||||
.office-scene__ceiling {
|
||||
inset: 8% 10% auto 10%;
|
||||
height: 8%;
|
||||
transform: translateZ(-260px);
|
||||
background: linear-gradient(180deg, rgba(28, 44, 31, 0.3), rgba(5, 8, 6, 0));
|
||||
}
|
||||
|
||||
.office-scene__floor {
|
||||
inset: 48% 8% 6% 8%;
|
||||
transform: translateZ(-120px);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(8, 10, 9, 0.78), rgba(1, 2, 1, 0.98)),
|
||||
repeating-linear-gradient(90deg, rgba(99, 245, 255, 0.05) 0 1px, transparent 1px 72px),
|
||||
repeating-linear-gradient(0deg, rgba(70, 121, 84, 0.06) 0 1px, transparent 1px 72px);
|
||||
box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.82);
|
||||
}
|
||||
|
||||
.office-scene__window {
|
||||
top: 18%;
|
||||
width: 20%;
|
||||
height: 18%;
|
||||
border: 1px solid rgba(99, 245, 255, 0.16);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(12, 34, 42, 0.45), rgba(2, 5, 8, 0.9)),
|
||||
repeating-linear-gradient(90deg, rgba(99, 245, 255, 0.14) 0 2px, transparent 2px 18px);
|
||||
box-shadow: inset 0 0 22px rgba(99, 245, 255, 0.12);
|
||||
}
|
||||
|
||||
.office-scene__window--left {
|
||||
left: 8%;
|
||||
transform: translateZ(-160px) rotateY(16deg);
|
||||
}
|
||||
|
||||
.office-scene__window--right {
|
||||
right: 8%;
|
||||
transform: translateZ(-160px) rotateY(-16deg);
|
||||
}
|
||||
|
||||
.office-scene__board {
|
||||
left: 50%;
|
||||
top: 15%;
|
||||
width: 28%;
|
||||
height: 13%;
|
||||
transform: translateX(-50%) translateZ(-150px);
|
||||
border: 2px solid rgba(255, 179, 71, 0.22);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(19, 13, 6, 0.9), rgba(7, 5, 3, 0.96)),
|
||||
repeating-linear-gradient(0deg, rgba(255, 179, 71, 0.08) 0 1px, transparent 1px 18px);
|
||||
box-shadow: inset 0 0 30px rgba(255, 179, 71, 0.08);
|
||||
}
|
||||
|
||||
.office-scene__plant {
|
||||
left: 12%;
|
||||
bottom: 18%;
|
||||
width: 8%;
|
||||
height: 16%;
|
||||
transform: translateZ(-40px);
|
||||
background:
|
||||
radial-gradient(circle at 50% 16%, rgba(70, 121, 84, 0.92), transparent 22%),
|
||||
radial-gradient(circle at 30% 38%, rgba(70, 121, 84, 0.9), transparent 18%),
|
||||
radial-gradient(circle at 70% 38%, rgba(70, 121, 84, 0.9), transparent 18%),
|
||||
linear-gradient(180deg, rgba(28, 18, 12, 0.95), rgba(10, 8, 6, 0.98));
|
||||
clip-path: polygon(34% 0, 66% 0, 76% 22%, 58% 32%, 72% 48%, 52% 58%, 62% 80%, 44% 100%, 28% 80%, 38% 58%, 18% 48%, 32% 32%, 14% 22%);
|
||||
filter: drop-shadow(0 0 12px rgba(70, 121, 84, 0.25));
|
||||
}
|
||||
|
||||
.office-scene__desk {
|
||||
left: 50%;
|
||||
bottom: 13%;
|
||||
width: 60%;
|
||||
height: 28%;
|
||||
transform: translateX(-50%) translateZ(120px);
|
||||
}
|
||||
|
||||
.office-scene__desk-top {
|
||||
position: absolute;
|
||||
inset: 0 0 38% 0;
|
||||
border: 1px solid rgba(99, 245, 255, 0.18);
|
||||
background: linear-gradient(180deg, rgba(34, 52, 39, 0.96), rgba(16, 24, 18, 0.98));
|
||||
transform: perspective(900px) rotateX(68deg);
|
||||
transform-origin: center bottom;
|
||||
box-shadow:
|
||||
inset 0 0 24px rgba(0, 0, 0, 0.4),
|
||||
0 18px 42px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.office-scene__desk-front {
|
||||
position: absolute;
|
||||
left: 6%;
|
||||
right: 6%;
|
||||
bottom: 0;
|
||||
height: 44%;
|
||||
border: 1px solid rgba(70, 121, 84, 0.3);
|
||||
background: linear-gradient(180deg, rgba(20, 31, 22, 0.98), rgba(7, 10, 8, 0.98));
|
||||
transform: perspective(900px) rotateX(18deg);
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
.office-scene__monitor {
|
||||
top: 14%;
|
||||
width: 16%;
|
||||
height: 24%;
|
||||
border: 2px solid rgba(99, 245, 255, 0.22);
|
||||
background:
|
||||
radial-gradient(circle at 50% 30%, rgba(99, 245, 255, 0.18), transparent 56%),
|
||||
linear-gradient(180deg, rgba(8, 21, 18, 0.98), rgba(4, 7, 6, 0.98));
|
||||
box-shadow:
|
||||
inset 0 0 18px rgba(99, 245, 255, 0.08),
|
||||
0 16px 24px rgba(0, 0, 0, 0.45);
|
||||
animation: office-glow 5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.office-scene__monitor span {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.48rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.office-scene__monitor--left {
|
||||
left: 10%;
|
||||
transform: perspective(900px) rotateY(24deg);
|
||||
}
|
||||
|
||||
.office-scene__monitor--center {
|
||||
left: 50%;
|
||||
width: 20%;
|
||||
height: 28%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.office-scene__monitor--right {
|
||||
right: 10%;
|
||||
transform: perspective(900px) rotateY(-24deg);
|
||||
}
|
||||
|
||||
.office-scene__chair {
|
||||
bottom: 10%;
|
||||
width: 12%;
|
||||
height: 18%;
|
||||
border: 1px solid rgba(114, 255, 132, 0.18);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 25, 17, 0.94), rgba(6, 10, 7, 0.98));
|
||||
clip-path: polygon(36% 0, 64% 0, 72% 18%, 72% 46%, 88% 70%, 80% 100%, 20% 100%, 12% 70%, 28% 46%, 28% 18%);
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.office-scene__chair--left {
|
||||
left: 18%;
|
||||
transform: perspective(900px) rotateY(12deg) rotateZ(-8deg);
|
||||
}
|
||||
|
||||
.office-scene__chair--right {
|
||||
right: 18%;
|
||||
transform: perspective(900px) rotateY(-12deg) rotateZ(8deg);
|
||||
}
|
||||
|
||||
.office-scene__mug {
|
||||
right: 18%;
|
||||
bottom: 30%;
|
||||
width: 4.5%;
|
||||
height: 7%;
|
||||
border: 1px solid rgba(255, 179, 71, 0.28);
|
||||
border-radius: 0 0 10px 10px;
|
||||
background: linear-gradient(180deg, rgba(56, 38, 14, 0.98), rgba(24, 16, 6, 0.98));
|
||||
transform: translateZ(20px);
|
||||
box-shadow: 0 0 18px rgba(255, 179, 71, 0.12);
|
||||
}
|
||||
|
||||
.office-scene__lamp {
|
||||
left: 50%;
|
||||
bottom: 43%;
|
||||
width: 11%;
|
||||
height: 12%;
|
||||
transform: translateX(-50%) translateZ(140px);
|
||||
background:
|
||||
radial-gradient(circle at 50% 0%, rgba(255, 179, 71, 0.46), transparent 42%),
|
||||
linear-gradient(180deg, rgba(255, 179, 71, 0.28), rgba(0, 0, 0, 0));
|
||||
filter: blur(0.4px);
|
||||
}
|
||||
|
||||
.office-scene__hud {
|
||||
position: absolute;
|
||||
inset: 16px 18px auto 18px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.office-scene__title {
|
||||
display: inline-flex;
|
||||
align-self: start;
|
||||
width: fit-content;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(99, 245, 255, 0.24);
|
||||
background: rgba(5, 9, 7, 0.72);
|
||||
color: var(--accent-cyan);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.office-scene__notes {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
max-width: 34ch;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(70, 121, 84, 0.24);
|
||||
background: rgba(4, 8, 5, 0.7);
|
||||
color: var(--text-dim);
|
||||
font-size: 0.64rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.office-scene__notes p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.office-scene__badges {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.office-scene__label {
|
||||
width: fit-content;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(70, 121, 84, 0.3);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.54rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.office-scene__label--amber {
|
||||
color: var(--accent-amber);
|
||||
border-color: rgba(255, 179, 71, 0.28);
|
||||
}
|
||||
|
||||
.office-scene__label--cyan {
|
||||
color: var(--accent-cyan);
|
||||
border-color: rgba(99, 245, 255, 0.28);
|
||||
}
|
||||
|
||||
.office-scene__label--green {
|
||||
color: var(--accent-green);
|
||||
border-color: rgba(114, 255, 132, 0.28);
|
||||
}
|
||||
|
||||
@keyframes office-room-float {
|
||||
0% {
|
||||
transform: rotateX(66deg) rotateZ(-11deg) translate3d(0, 22px, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotateX(66deg) rotateZ(-11deg) translate3d(0, 34px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes office-glow {
|
||||
0% {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.team-card {
|
||||
border: 2px solid var(--border-mid);
|
||||
background: linear-gradient(180deg, rgba(10, 16, 11, 0.92), rgba(8, 12, 9, 0.96));
|
||||
|
||||
Reference in New Issue
Block a user