feat(tg): Mini App V5 — brand typography, icon depth, motion, detail sheets (#583)

* feat(tg): Mini App V5 — brand typography, icon depth, motion, detail sheets

Share Tech Mono (the vendored motion-brand face) becomes the cockpit's
display voice via next/font/local scoped to #tg-shell; icon tiles, circle
actions, and avatars get gradient/ring depth; a dependency-free motion
vocabulary lands (spend count-up, tab rise-in, staggered sections,
sparkline draw-in, sheet slide-up with native BackButton dismiss); the
Board tab gains a tap-through task sheet (ACs, open findings, PR link),
Today's fleet opens a full-roster sheet, and Board/Inbox join the
/tg?demo=1 fixtures.

* feat(tg): custom RoboCo icon set + operations ring

The cockpit stops using stock lucide on its hero surfaces: a hand-drawn
duotone icon set (speedometer, seal, bell, kanban, brand-cursor bubble,
rocket, double-check, broom, robot head) covers the tab bar and the Today
ring. The ring itself stops duplicating the tab bar and becomes real
operations: Ship deep-focuses the release proposal in Approvals, Ack all
bulk-acknowledges pending notifications, Sweep runs the stale-branch
cleanup across every git-configured project behind a confirm sheet, and
Fleet opens the roster.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-19 18:59:54 +02:00
committed by GitHub
co-authored by Renn F
parent fc41dfa40e
commit 700dbcd285
19 changed files with 1467 additions and 101 deletions
@@ -0,0 +1,59 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { renderHook } from "@testing-library/react";
import { TgSheet, useCountUp } from "../motion";
describe("useCountUp", () => {
it("jumps straight to the target under reduced motion", async () => {
const original = window.matchMedia;
window.matchMedia = ((query: string) => ({
matches: true,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
})) as typeof window.matchMedia;
try {
const { result } = renderHook(() => useCountUp(42));
await waitFor(() => expect(result.current).toBe(42));
} finally {
window.matchMedia = original;
}
});
it("settles on the exact target when animating", async () => {
const { result } = renderHook(() => useCountUp(12.34, 50));
// Generous timeout: rAF frames starve under parallel test workers.
await waitFor(() => expect(result.current).toBe(12.34), { timeout: 4000 });
});
});
describe("TgSheet", () => {
it("renders nothing when closed", () => {
render(
<TgSheet open={false} onClose={vi.fn()} title="Task">
<p>body</p>
</TgSheet>,
);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("shows title and children when open, closes on backdrop tap", () => {
const onClose = vi.fn();
render(
<TgSheet open onClose={onClose} title="Fleet">
<p>sheet body</p>
</TgSheet>,
);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Fleet")).toBeInTheDocument();
expect(screen.getByText("sheet body")).toBeInTheDocument();
const [backdrop] = screen.getAllByRole("button", { name: /close/i });
fireEvent.click(backdrop);
expect(onClose).toHaveBeenCalled();
});
});
@@ -0,0 +1,135 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { TgTaskSheet } from "../tg-task-sheet";
import type { Task } from "@/types";
import type { TaskFindingsResponse } from "@/lib/api/tasks";
const { findings } = vi.hoisted(() => ({
findings: vi.fn<() => { data: TaskFindingsResponse | undefined }>(() => ({
data: undefined,
})),
}));
vi.mock("@/hooks/use-tasks", () => ({
useTaskFindings: findings,
}));
function task(overrides: Partial<Task> = {}): Task {
return {
id: "t1",
title: "Harden the retry queue",
description: "Webhook retries with backoff.",
acceptance_criteria: ["Retries back off", "DLQ after 5 attempts"],
status: "awaiting_ceo_approval",
priority: 1,
sequence: 0,
team: "backend",
created_by: "main-pm",
assigned_to: "be-dev-1",
parent_task_id: null,
dependency_ids: [],
blocker_ids: [],
created_at: "2026-07-18T10:00:00Z",
updated_at: "2026-07-19T09:00:00Z",
claimed_at: null,
started_at: null,
completed_at: null,
target_date: null,
estimated_complexity: "medium",
nature: "technical",
task_type: "code",
project_id: "p1",
docs_complete: false,
pr_created: true,
pm_approvals: {},
plan: null,
checkpoints: [],
progress_updates: [],
commits: [],
dev_notes: null,
qa_notes: null,
auditor_notes: null,
quick_context: null,
self_verified: true,
qa_verified: true,
revision_count: 2,
branch_name: "feature/backend/T1",
pr_number: 612,
pr_url: "https://example.com/pull/612",
...overrides,
} as Task;
}
describe("TgTaskSheet", () => {
it("renders nothing without a task", () => {
render(<TgTaskSheet task={null} onClose={vi.fn()} />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("shows title, ACs, bounce chip, and the PR link", () => {
render(<TgTaskSheet task={task()} onClose={vi.fn()} />);
expect(screen.getByText("Harden the retry queue")).toBeInTheDocument();
expect(screen.getByText("Retries back off")).toBeInTheDocument();
expect(screen.getByText("DLQ after 5 attempts")).toBeInTheDocument();
expect(screen.getByText(/bounced ×2/)).toBeInTheDocument();
expect(screen.getByRole("link", { name: /open pr #612/i })).toHaveAttribute(
"href",
"https://example.com/pull/612",
);
});
it("lists only the open findings", () => {
findings.mockReturnValue({
data: {
findings: [
{
id: "f1",
task_id: "t1",
origin: "qa",
round: 1,
author_slug: "be-qa",
file: "roboco/services/queue.py",
line: 42,
severity: "major",
criterion: null,
expected: "Backoff is exponential",
actual: "Fixed 1s delay",
fix: "Use exponential backoff with jitter",
evidence: null,
status: "open",
addressed_by_commit: null,
resolution_note: null,
created_at: "2026-07-19T08:00:00Z",
updated_at: null,
},
{
id: "f2",
task_id: "t1",
origin: "qa",
round: 1,
author_slug: "be-qa",
file: "roboco/services/dlq.py",
line: 7,
severity: "minor",
criterion: null,
expected: "x",
actual: "y",
fix: null,
evidence: null,
status: "verified",
addressed_by_commit: null,
resolution_note: null,
created_at: "2026-07-19T08:00:00Z",
updated_at: null,
},
],
summary: [],
total: 2,
truncated: false,
},
});
render(<TgTaskSheet task={task()} onClose={vi.fn()} />);
expect(screen.getByText(/open findings · 1/i)).toBeInTheDocument();
expect(screen.getByText("roboco/services/queue.py:42")).toBeInTheDocument();
expect(screen.queryByText(/dlq\.py/)).not.toBeInTheDocument();
});
});
@@ -4,8 +4,22 @@ import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TgTodayTab } from "../tg-today-tab";
const { get } = vi.hoisted(() => ({ get: vi.fn() }));
vi.mock("@/lib/api/client", () => ({ default: { get } }));
const { get, ackMock, notifItems } = vi.hoisted(() => ({
get: vi.fn(),
ackMock: vi.fn(),
notifItems: { current: [] as Array<Record<string, unknown>> },
}));
vi.mock("@/lib/api/client", () => ({
default: { get },
getErrorMessage: () => "error",
}));
vi.mock("@/hooks/use-notifications", () => ({
notificationKeys: { all: ["notifications"] },
useNotifications: () => ({ data: { items: notifItems.current } }),
}));
vi.mock("@/lib/api/notifications", () => ({
notificationsApi: { acknowledge: ackMock },
}));
function renderTab(onNavigate = vi.fn()) {
const client = new QueryClient({
@@ -52,14 +66,28 @@ describe("TgTodayTab", () => {
// vitest call it as an after-test teardown hook.
beforeEach(() => {
get.mockReset();
ackMock.mockReset();
notifItems.current = [];
// Reduced motion → the spend count-up lands instantly; these tests
// assert content, not animation timing.
window.matchMedia = ((query: string) => ({
matches: query.includes("prefers-reduced-motion"),
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
})) as typeof window.matchMedia;
});
it("shows skeletons while loading", () => {
get.mockReturnValue(new Promise(() => {}));
renderTab();
expect(document.querySelectorAll("[data-slot=skeleton]").length).toBeGreaterThan(
0,
);
expect(
document.querySelectorAll("[data-slot=skeleton]").length,
).toBeGreaterThan(0);
});
it("renders the all-clear state and the spend/ship numbers", async () => {
@@ -67,7 +95,8 @@ describe("TgTodayTab", () => {
renderTab();
expect(await screen.findByText(/all clear/i)).toBeInTheDocument();
expect(screen.getByText("$12.34")).toBeInTheDocument();
// The spend hero counts up to the target, so wait for the final frame.
expect(await screen.findByText("$12.34")).toBeInTheDocument();
expect(screen.getByText(/1\.2M tokens/)).toBeInTheDocument();
expect(screen.getByText("v0.25.0")).toBeInTheDocument();
expect(screen.getByText(/no release pending/i)).toBeInTheDocument();
@@ -110,6 +139,91 @@ describe("TgTodayTab", () => {
expect(onNavigate).toHaveBeenCalledWith("board");
});
it("renders the operations ring and deep-links Ship into the release", async () => {
get.mockResolvedValue({
data: brief({
ship: { version: "0.25.0", open_release_proposal: true, ci_fix_tasks: 0 },
}),
});
const onNavigate = vi.fn();
renderTab(onNavigate);
await userEvent.click(await screen.findByRole("button", { name: /ship/i }));
expect(onNavigate).toHaveBeenCalledWith("approvals", "release");
expect(screen.getByRole("button", { name: /sweep/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /fleet/i })).toBeInTheDocument();
});
it("ack-all acknowledges every pending notification", async () => {
get.mockResolvedValue({ data: brief() });
notifItems.current = [
{ id: "n1", requires_ack: true, is_acknowledged: false },
{ id: "n2", requires_ack: true, is_acknowledged: false },
{ id: "n3", requires_ack: false, is_acknowledged: false },
];
ackMock.mockResolvedValue({});
renderTab();
await userEvent.click(
await screen.findByRole("button", { name: /ack all/i }),
);
await waitFor(() => expect(ackMock).toHaveBeenCalledTimes(2));
expect(ackMock).toHaveBeenCalledWith("n1");
expect(ackMock).toHaveBeenCalledWith("n2");
});
it("opens the fleet sheet with the full working roster", async () => {
get.mockResolvedValue({
data: brief({
fleet: {
total: 26,
by_status: { active: 5, idle: 21 },
working: [
{
name: "be-dev-1",
role: "developer",
team: "backend",
task_title: "Task A",
},
{
name: "be-dev-2",
role: "developer",
team: "backend",
task_title: "Task B",
},
{
name: "fe-dev-1",
role: "developer",
team: "frontend",
task_title: "Task C",
},
{
name: "fe-qa",
role: "qa",
team: "frontend",
task_title: "Task D",
},
{
name: "ux-dev-1",
role: "developer",
team: "ux_ui",
task_title: "Task E",
},
],
},
}),
});
renderTab();
// The section previews 3 of 5; tapping it opens the full-roster sheet.
await userEvent.click(
await screen.findByText(/\+2 more · tap for the full roster/),
);
expect(await screen.findByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Task E")).toBeInTheDocument();
expect(screen.getByText(/idle · 21/)).toBeInTheDocument();
});
it("shows an error state when the brief fails to load", async () => {
get.mockRejectedValue(new Error("boom"));
renderTab();