From 58ab4966f347e4186b6a73a1c5283c19e30a62ef Mon Sep 17 00:00:00 2001 From: 8syncdev Date: Thu, 22 Oct 2026 08:28:09 +0700 Subject: [PATCH] =?UTF-8?q?feat(hq):=203D=20desk=20signs=20=E2=80=94=20roo?= =?UTF-8?q?m=20name=20and=20the=20card=20it=20holds=20(v0.110.46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-line canvas sprite per desk (`#12 · qa`, or what the room is doing), amber when the room waits on the owner; the hover tooltip adds the card title. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 4 +++ Cargo.lock | 4 +-- Cargo.toml | 2 +- apps/hq-ui/components/hq/office-3d.tsx | 42 +++++++++++++++++++++--- apps/hq-ui/lib/office-3d.ts | 9 ++++- apps/hq-ui/tests/office-3d.test.ts | 11 ++++++- assets/claude/.claude-plugin/plugin.json | 2 +- 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a978885..d40a2329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 8f4f2e73..58d66735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,7 +200,7 @@ dependencies = [ [[package]] name = "ccc-core" -version = "0.110.45" +version = "0.110.46" dependencies = [ "anyhow", "dirs", @@ -1503,7 +1503,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "su-code" -version = "0.110.45" +version = "0.110.46" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 43fbe9b4..af487023 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 <8sync.dev.1111@gmail.com>"] diff --git a/apps/hq-ui/components/hq/office-3d.tsx b/apps/hq-ui/components/hq/office-3d.tsx index 5d510911..9f9312e7 100644 --- a/apps/hq-ui/components/hq/office-3d.tsx +++ b/apps/hq-ui/components/hq/office-3d.tsx @@ -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"; @@ -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(null); const [hover, setHover] = useState(null); @@ -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]); @@ -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); @@ -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)); } @@ -245,6 +278,7 @@ export function Office3D({ rooms, boards, now, repo, active, onOpen }: { rooms: {hover.label} · {hover.project} · {NOW_LABEL[hover.state]} + {hover.card && {hover.card}} )} diff --git a/apps/hq-ui/lib/office-3d.ts b/apps/hq-ui/lib/office-3d.ts index 4d47e6f7..fce44aed 100644 --- a/apps/hq-ui/lib/office-3d.ts +++ b/apps/hq-ui/lib/office-3d.ts @@ -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 }; @@ -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]; +} diff --git a/apps/hq-ui/tests/office-3d.test.ts b/apps/hq-ui/tests/office-3d.test.ts index 4a7938d2..f5493182 100644 --- a/apps/hq-ui/tests/office-3d.test.ts +++ b/apps/hq-ui/tests/office-3d.test.ts @@ -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" }); @@ -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"); + }); +}); diff --git a/assets/claude/.claude-plugin/plugin.json b/assets/claude/.claude-plugin/plugin.json index c61b784a..b9696431 100644 --- a/assets/claude/.claude-plugin/plugin.json +++ b/assets/claude/.claude-plugin/plugin.json @@ -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",