Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nh

## [Unreleased]

## [0.110.46] — 2026-10-22
### Added
- **Tab 3D: biển tên trên mỗi bàn** (tiếp .43; owner: "ui ux strong realistic working day"): sprite 2 dòng trên bàn ghi tên phòng và card đang giữ (`#12 · qa`), không có card thì "đang làm / chờ anh / kẹt / rảnh" (dòng hai màu cam khi chờ anh); tooltip rê chuột thêm tiêu đề card. Canvas texture 256×96, không tải font, không thêm dependency. Test office-3d +1.

## [0.110.45] — 2026-10-22
### Changed
- **Sidebar HQ: nhóm "Công ty"** (tiếp .40/.44): khi `auto company on`, tiêu đề danh sách dự án ở sidebar đổi từ "Dự án" thành "Công ty · N dự án" (tooltip giải thích một team làm mọi dự án); `/api/office` thêm `company` từ lần đọc `auto status` gần nhất. Test sidebar +1.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/cli", "crates/ccc-core"]

[workspace.package]
version = "0.110.45"
version = "0.110.46"
edition = "2021"
license = "PolyForm-Noncommercial-1.0.0"
authors = ["8sync-dev <[email protected]>"]
Expand Down
42 changes: 38 additions & 4 deletions apps/hq-ui/components/hq/office-3d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { BoxIcon } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { NOW_LABEL, roster, type NowState } from "@/lib/now";
import { cameraDistance, layoutOffice, STATE_COLOR, type Desk, type GroupIn } from "@/lib/office-3d";
import { cameraDistance, deskSign, layoutOffice, STATE_COLOR, type Desk, type GroupIn } from "@/lib/office-3d";
import { groupByRepo } from "@/lib/sidebar";
import { cn } from "@/lib/utils";
import { EmptyState } from "./bits";
Expand All @@ -16,9 +16,34 @@ import type { Room } from "./use-office";

type Three = typeof import("three");

type Hover = { key: string; label: string; state: NowState; project: string; x: number; y: number } | null;
type Hover = { key: string; label: string; state: NowState; project: string; card: string | null; x: number; y: number } | null;
type Stats = { calls: number; ms: number; rooms: number };

/** A 2-line text sprite (canvas texture): 256×96 px, always faces the camera, no font loading. */
function makeSign(T: Three, lines: [string, string], dark: boolean, loud: boolean) {
const canvas = document.createElement("canvas");
canvas.width = 256;
canvas.height = 96;
const g = canvas.getContext("2d")!;
g.fillStyle = dark ? "rgba(20,24,28,0.85)" : "rgba(255,255,255,0.9)";
g.beginPath();
g.roundRect(4, 4, 248, 88, 12);
g.fill();
g.textAlign = "center";
g.fillStyle = dark ? "#e8eaed" : "#1b1f24";
g.font = "600 30px system-ui, sans-serif";
g.fillText(lines[0].slice(0, 16), 128, 42);
g.fillStyle = loud ? "#d9822b" : dark ? "#aab0b8" : "#5c6470";
g.font = "500 24px system-ui, sans-serif";
g.fillText(lines[1].slice(0, 20), 128, 76);
const tex = new T.CanvasTexture(canvas);
tex.colorSpace = T.SRGBColorSpace;
const mat = new T.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
const sprite = new T.Sprite(mat);
sprite.scale.set(1.6, 0.6, 1);
return sprite;
}

export function Office3D({ rooms, boards, now, repo, active, onOpen }: { rooms: Room[]; boards: BoardsState; now: number; repo: string | null; active: boolean; onOpen: (room: Room) => void }) {
const host = useRef<HTMLDivElement>(null);
const [hover, setHover] = useState<Hover>(null);
Expand All @@ -31,7 +56,11 @@ export function Office3D({ rooms, boards, now, repo, active, onOpen }: { rooms:
const rows = new Map(roster(live, boards.boards ?? [], now).map((r) => [r.key, r]));
return groupByRepo(live).map((g) => ({
project: { root: g.project.root, name: g.project.name },
rooms: g.rooms.map((r) => ({ key: r.key, label: r.label, state: rows.get(r.key)?.state ?? "idle", role: rows.get(r.key)?.role ?? null })),
rooms: g.rooms.map((r) => {
const row = rows.get(r.key);
const c = row?.card;
return { key: r.key, label: r.label, state: row?.state ?? "idle", role: row?.role ?? null, card: c ? { id: c.id, lane: c.lane, text: c.text } : null };
}),
}));
}, [rooms, boards.boards, now, repo]);
const layout = useMemo(() => layoutOffice(groups), [groups]);
Expand Down Expand Up @@ -118,6 +147,10 @@ export function Office3D({ rooms, boards, now, repo, active, onOpen }: { rooms:
a.userData.key = d.key;
office.add(a);
avatars.push({ mesh: a, desk: d });
// The desk sign: room name and its card (owner 2026-10-22: a realistic working day, not just colours).
const sign = makeSign(T, deskSign(d), dark, d.state === "needs");
sign.position.set(d.x, 1.35, d.z);
office.add(sign);
}
const dist = cameraDistance(l);
camera.position.set(dist * 0.55, dist * 0.75, dist * 0.8);
Expand All @@ -138,7 +171,7 @@ export function Office3D({ rooms, boards, now, repo, active, onOpen }: { rooms:
renderer.domElement.style.cursor = key ? "pointer" : "";
const a = avatars.find((x) => x.desk.key === key);
const l = layoutRef.current;
setHover(a ? { key: a.desk.key, label: a.desk.label, state: a.desk.state, project: l.floors.find((f) => f.root === a.desk.root)?.name ?? "", x: e.clientX - r.left, y: e.clientY - r.top } : null);
setHover(a ? { key: a.desk.key, label: a.desk.label, state: a.desk.state, project: l.floors.find((f) => f.root === a.desk.root)?.name ?? "", card: a.desk.card ? `#${a.desk.card.id} ${a.desk.card.text}` : null, x: e.clientX - r.left, y: e.clientY - r.top } : null);
} else if (key) {
setHover((h) => (h ? { ...h, x: e.clientX - r.left, y: e.clientY - r.top } : h));
}
Expand Down Expand Up @@ -245,6 +278,7 @@ export function Office3D({ rooms, boards, now, repo, active, onOpen }: { rooms:
<span className="font-medium">{hover.label}</span>
<span className="text-muted-foreground"> · {hover.project} · </span>
<span className={cn(hover.state === "needs" && "text-st-needs", hover.state === "working" && "text-st-running")}>{NOW_LABEL[hover.state]}</span>
{hover.card && <span className="block max-w-[40ch] truncate text-muted-foreground">{hover.card}</span>}
</div>
)}
</div>
Expand Down
9 changes: 8 additions & 1 deletion apps/hq-ui/lib/office-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// Tested in tests/office-3d.test.ts.
import type { NowState } from "./now";

export type DeskIn = { key: string; label: string; state: NowState; role: string | null };
export type DeskCard = { id: number; lane: string; text: string };
export type DeskIn = { key: string; label: string; state: NowState; role: string | null; /** The card the room holds (its board claim), shown on the desk. */ card?: DeskCard | null };
export type GroupIn = { project: { root: string; name: string }; rooms: DeskIn[] };

export type Floor = { root: string; name: string; x: number; z: number; w: number; d: number };
Expand Down Expand Up @@ -62,3 +63,9 @@ export function cameraDistance(l: Layout): number {
const span = Math.max(l.width, l.depth, 6);
return span * 1.35 + 4;
}

/** The two lines on a desk's sign: the room, then its card (`#12 · qa`) or what it is doing when idle. */
export function deskSign(d: DeskIn): [string, string] {
const second = d.card ? `#${d.card.id} · ${d.card.lane}` : d.state === "needs" ? "chờ anh" : d.state === "working" ? "đang làm" : d.state === "blocked" ? "kẹt" : "rảnh";
return [d.label, second];
}
11 changes: 10 additions & 1 deletion apps/hq-ui/tests/office-3d.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { DESK_GAP, FLOOR_PAD, MAX_COLS, STATE_COLOR, cameraDistance, layoutOffice } from "@/lib/office-3d";
import { DESK_GAP, FLOOR_PAD, MAX_COLS, STATE_COLOR, cameraDistance, deskSign, layoutOffice } from "@/lib/office-3d";
import { NOW_STATES } from "@/lib/now";

const room = (i: number, state: "working" | "needs" | "idle" = "working") => ({ key: `r${i}`, label: `dev${i}`, state, role: "dev" });
Expand Down Expand Up @@ -37,3 +37,12 @@ describe("3D office layout (one floor per project, desks in a grid)", () => {
expect(layoutOffice([])).toEqual({ floors: [], desks: [], width: 0, depth: 0 });
});
});

describe("deskSign", () => {
it("names the room and its card, or what it is doing without one", () => {
expect(deskSign({ key: "k", label: "dev2", state: "working", role: "dev", card: { id: 12, lane: "qa", text: "t" } })).toEqual(["dev2", "#12 · qa"]);
expect(deskSign({ key: "k", label: "lead", state: "needs", role: "lead", card: null })).toEqual(["lead", "chờ anh"]);
expect(deskSign({ key: "k", label: "qa", state: "idle", role: "qa" })).toEqual(["qa", "rảnh"]);
expect(deskSign({ key: "k", label: "ui", state: "blocked", role: "ui" })[1]).toBe("kẹt");
});
});
2 changes: 1 addition & 1 deletion assets/claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "8sync-kit",
"displayName": "8sync kit",
"version": "0.110.45",
"version": "0.110.46",
"description": "8 Sync kit for Claude Code: role-routed subagents (explore, task, quick-task, reviewer, designer, researcher, verifier, pm, architect, qa, critic) and the /ship /rules /sec /harden skills. Hooks, rules and the full skill set come from `8sync ccc kit`.",
"author": { "name": "8 Sync", "url": "https://8syncdev.com" },
"homepage": "https://get.8syncdev.com",
Expand Down
Loading