From 54d7a542f771bc8afc5a815d5af813799209ad3a Mon Sep 17 00:00:00 2001
From: yuta
Date: Mon, 28 Sep 2026 22:38:58 +0900
Subject: [PATCH] feat: redesign daily execution and messaging interfaces
---
README.md | 6 +
docs/execution-support.md | 19 +-
e2e/integrations/home.test.ts | 96 +++-
e2e/integrations/support.test.ts | 11 +-
src/features/home/daily-brief.tsx | 6 +-
src/features/home/execution-workbench.css | 333 +++++++++++++
src/features/home/execution-workbench.tsx | 486 +++++++++++++++++++
src/features/home/home-page.tsx | 233 +--------
src/features/home/home.css | 8 +-
src/features/navigation/search.ts | 1 +
src/features/support/conversation-detail.tsx | 331 +++++++------
src/features/support/mock-conversations.ts | 5 +
src/features/support/support-page.tsx | 186 +++----
src/features/support/support.css | 402 ++++++++++++++-
src/styles.css | 1 +
tokens.css | 3 +
16 files changed, 1623 insertions(+), 504 deletions(-)
create mode 100644 src/features/home/execution-workbench.css
create mode 100644 src/features/home/execution-workbench.tsx
diff --git a/README.md b/README.md
index de9c5cb..3fd0e1a 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,12 @@ each dedicated workspace; its bottom avatar manages connected accounts.
The interface is in English. User-entered text and external posts retain their
original language.
+Home uses a Tiimo-inspired day flow with expandable activities and direct
+controls for routine steps, tasks, replies and weight entries. Messages uses
+a Beeper-inspired conversation layout with visible source messages, editable
+drafts and a contextual side panel. These execution surfaces remain mocks;
+their records share browser-session state and external sending is simulated.
+
## Workspaces and current status
| Workspace | Route | Current implementation |
diff --git a/docs/execution-support.md b/docs/execution-support.md
index 020a402..38b2804 100644
--- a/docs/execution-support.md
+++ b/docs/execution-support.md
@@ -36,13 +36,30 @@ the new communication and journal prototypes do not consume those accounts.
## Daily home
-`/` presents a greeting, a large input, and a flexible plan grouped as
+`/` presents a greeting, a large input, and a Tiimo-inspired flexible day flow grouped as
Start here, Routines, and If there’s room. There are no fixed time slots.
Tasks can be reordered, completed, deferred, restored, or followed as routine
steps. Communications can be reviewed directly in a home dialog; changes also
appear in `/support`. Journal entries and corrections appear on the home, and
reader samples open for reading. Research remains a separate work surface.
+Activity blocks expand in place into controls suited to their content: routine
+steps, a task's next action and resumption note, or a weight input that records
+an entry in Journal. Completed and deferred activities stay in their original positions with visible
+status and undo controls. Measurements retain the recorded value in place, and
+reply previews retain their outcome, so the user need not remember what was done. Expanded activity selection
+uses the `activity` URL parameter. This remains a session-only mock; a reload
+restores navigation, not recorded activity data.
+
+Messages uses a Beeper-inspired person list and visible original conversation,
+with reply drafting and review next to source-backed context. On narrow screens
+the list and conversation become separate views. Inbox, set-aside, waiting and
+archive filters describe communication state; a simulated reply does not prove
+that a separate promise was fulfilled. References include the saved
+[Beeper Copilot captures](references/beeper-copilot/README.md),
+[Beeper Desktop](https://blog.beeper.com/2025/02/24/try-out-the-new-beeper-desktop-and-ios-beta/),
+and [open-loop research](gbrain-open-loops-research.md).
+
The input currently uses a deterministic English/Japanese mock interpreter, not an LLM.
It proposes typed actions for task creation/status/reordering, routine steps,
contact reply/review or deferral, notes/weight/medication records, and navigation.
diff --git a/e2e/integrations/home.test.ts b/e2e/integrations/home.test.ts
index 51b3803..d82162f 100644
--- a/e2e/integrations/home.test.ts
+++ b/e2e/integrations/home.test.ts
@@ -1,5 +1,44 @@
import { expect, test } from "../fixtures";
+test("executes routine steps and records a measurement in the shared journal", async ({
+ page,
+ a11y,
+}, testInfo) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ await page.getByRole("button", { name: /Morning routine 0 of 3/ }).click();
+ await expect(page).toHaveURL(/activity=task/);
+ await page.getByRole("checkbox", { name: /Drink water/ }).check();
+ await expect(page.getByRole("button", { name: "Finish routine" })).toBeDisabled();
+ await page.getByRole("checkbox", { name: /Get ready/ }).check();
+ await page.getByRole("checkbox", { name: /Have breakfast/ }).check();
+ expect((await a11y().analyze()).violations).toEqual([]);
+ await page.screenshot({ path: testInfo.outputPath("home-routine.png"), fullPage: true });
+ await page.getByRole("button", { name: "Finish routine" }).click();
+ await page.getByRole("button", { name: /Record weight A quick check-in/ }).click();
+ await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
+ await page.getByRole("button", { name: "Save measurement" }).click();
+ await expect(
+ page.getByRole("button", { name: /Record weight Recorded · 65.2 kg/ }),
+ ).toBeVisible();
+ await page.getByRole("link", { name: "Journal", exact: true }).first().click();
+ await expect(page.getByRole("article", { name: "Weight entry" })).toContainText("65.2 kg");
+ await page.getByRole("link", { name: "Home", exact: true }).first().click();
+ await expect(page.getByRole("button", { name: "Restore Morning routine" })).toBeVisible();
+});
+
+test("shows the day flow and conversation context at desktop width", async ({ page }, testInfo) => {
+ await page.setViewportSize({ width: 1440, height: 1080 });
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ await expect(page.getByRole("region", { name: "Today’s plan" })).toBeVisible();
+ await page.screenshot({ path: testInfo.outputPath("home-desktop.png") });
+ await page.goto("/support?conversation=design-review");
+ await page.waitForLoadState("networkidle");
+ await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
+ await page.screenshot({ path: testInfo.outputPath("messages-desktop.png") });
+});
+
test("previews a natural-language memo and shares the executed record with journal", async ({
page,
}) => {
@@ -20,7 +59,7 @@ test("previews a natural-language memo and shares the executed record with journ
test("keeps the home usable on narrow screens", async ({ page, a11y }, testInfo) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
- for (const width of [320, 375, 768]) {
+ for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await expect(page.getByRole("textbox", { name: "Home input" })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
@@ -47,11 +86,9 @@ test("adds tasks only after confirmation and can defer and restore them", async
await expect(complete).toBeVisible();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(complete).toHaveCount(0);
- await page.locator(".home-rest summary").click();
await page.getByRole("button", { name: "Restore grocery shopping", exact: true }).click();
await complete.click();
await expect(complete).toHaveCount(0);
- await page.locator(".home-rest summary").click();
await expect(
page.getByRole("button", { name: "Restore grocery shopping", exact: true }),
).toBeVisible();
@@ -73,13 +110,13 @@ test("resolves communication within home and carries the status to the shared qu
await expect(dialog).toHaveCount(0);
await expect(
page.getByRole("button", { name: "Open conversation with Haruka Aoki" }),
- ).toHaveCount(0);
+ ).toContainText("Set aside");
await page.getByRole("link", { name: "Messages", exact: true }).first().click();
const rows = page.locator(".support-row");
await expect(rows.filter({ hasText: "Schedule next week’s design review" })).toHaveCount(0);
- await page.getByRole("button", { name: "Later", exact: true }).click();
+ await page.getByRole("button", { name: "Set aside", exact: true }).click();
await rows.filter({ hasText: "Schedule next week’s design review" }).click();
- await page.getByRole("button", { name: "Move to Now" }).click();
+ await page.getByRole("button", { name: "Move to Inbox" }).click();
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await page.getByRole("link", { name: "Home", exact: true }).first().click();
@@ -95,7 +132,7 @@ test("updates the prose brief as priorities change and opens the referenced conv
await page.waitForLoadState("networkidle");
const brief = page.getByRole("region", { name: "Daily brief" });
await expect(brief).toContainText("Review the documents");
- await expect(brief).toContainText("4 conversations");
+ await expect(brief).toContainText("3 conversations");
await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
await expect(brief).not.toContainText("Review the documents");
await brief.getByRole("button", { name: "Reply to Haruka Aoki" }).click();
@@ -104,10 +141,53 @@ test("updates the prose brief as priorities change and opens the referenced conv
exact: true,
});
await dialog.getByRole("button", { name: "Leave for later" }).click();
- await expect(brief).toContainText("3 conversations");
+ await expect(brief).toContainText("2 conversations");
await expect(brief).not.toContainText("Haruka Aoki");
await page.getByRole("textbox", { name: "Home input" }).fill("再開地点:資料の2ページ目から");
await page.getByRole("button", { name: "Preview action" }).click();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(brief).toContainText("資料の2ページ目から");
});
+
+test("keeps completed actions in their original positions with visible outcomes", async ({
+ page,
+ a11y,
+}, testInfo) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ const titles = page.locator(".execution-flow-title");
+ const before = await titles.allTextContents();
+ await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
+ await expect(
+ page.getByRole("button", { name: "Restore Review the documents", exact: true }),
+ ).toHaveAttribute("aria-pressed", "true");
+ await expect(
+ page.getByRole("button", { name: "Review the documents Completed", exact: true }),
+ ).toBeVisible();
+ await page.getByRole("button", { name: "Open conversation with Haruka Aoki" }).click();
+ const conversation = page.getByRole("dialog", {
+ name: "Conversation with Haruka Aoki",
+ exact: true,
+ });
+ await conversation.getByRole("button", { name: "Review reply", exact: true }).click();
+ await page
+ .getByRole("dialog", { name: "Review reply", exact: true })
+ .getByRole("button", { name: "Try sending" })
+ .click();
+ await expect(
+ page.getByRole("button", { name: "Open conversation with Haruka Aoki" }),
+ ).toContainText("Reply recorded");
+ await page.getByRole("button", { name: /Record weight A quick check-in/ }).click();
+ await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
+ await page.getByRole("button", { name: "Save measurement" }).click();
+ await expect(
+ page.getByRole("button", { name: /Record weight Recorded · 65.2 kg/ }),
+ ).toBeVisible();
+ expect(await titles.allTextContents()).toEqual(before);
+ expect((await a11y().analyze()).violations).toEqual([]);
+ await page.screenshot({ path: testInfo.outputPath("completion-in-place.png") });
+ await page.getByRole("button", { name: "Restore Review the documents", exact: true }).click();
+ await expect(
+ page.getByRole("button", { name: "Complete Review the documents", exact: true }),
+ ).toHaveAttribute("aria-pressed", "false");
+});
diff --git a/e2e/integrations/support.test.ts b/e2e/integrations/support.test.ts
index 399fa0a..9883db1 100644
--- a/e2e/integrations/support.test.ts
+++ b/e2e/integrations/support.test.ts
@@ -17,6 +17,7 @@ test("reviews an edited reply and keeps the simulated result in history", async
await expect(reply).toHaveValue(/Thank you for sharing/);
const edited = "I reviewed the materials. Let’s discuss the first-time experience.";
await reply.fill(edited);
+ await page.locator(".conversation-scheduling summary:visible").click();
const slot = "Mon, Oct 5, 14:00–14:30";
await page.getByRole("combobox", { name: "Suggested times" }).selectOption(slot);
await page.getByRole("button", { name: "Add to reply" }).click();
@@ -33,7 +34,7 @@ test("reviews an edited reply and keeps the simulated result in history", async
await dialog.getByRole("button", { name: "Try sending" }).click();
await expect(dialog).toHaveCount(0);
await expect(rows.filter({ hasText: reviewSubject })).toHaveCount(0);
- await page.getByRole("button", { name: "History", exact: true }).click();
+ await page.getByRole("button", { name: "Archive", exact: true }).click();
await rows.filter({ hasText: reviewSubject }).click();
await expect(page.locator(".support-outcome")).toContainText(edited);
await expect(page.locator(".support-outcome")).toContainText(slot);
@@ -49,16 +50,16 @@ test("defers a conversation and restores it to the current queue", async ({ page
.fill("土曜の予定を確認してから返事するね。");
await page.getByRole("button", { name: "Leave for later" }).click();
await expect(rows.filter({ hasText: coffeeSubject })).toHaveCount(0);
- await page.getByRole("button", { name: "Later", exact: true }).click();
+ await page.getByRole("button", { name: "Set aside", exact: true }).click();
await rows.filter({ hasText: coffeeSubject }).click();
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toHaveValue(
"土曜の予定を確認してから返事するね。",
);
- await page.getByRole("button", { name: "Move to Now" }).click();
+ await page.getByRole("button", { name: "Move to Inbox" }).click();
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await expect(rows.filter({ hasText: coffeeSubject })).toBeVisible();
- await expect(page.getByRole("button", { name: "Now", exact: true })).toHaveAttribute(
+ await expect(page.getByRole("button", { name: "Inbox", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
@@ -107,7 +108,7 @@ test("keeps corrections as new records and summarizes only selected current fact
});
test("fits communication and journal interactions in narrow screens", async ({ page, a11y }) => {
- for (const width of [320, 375, 768]) {
+ for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await page.goto("/support");
await page.waitForLoadState("networkidle");
diff --git a/src/features/home/daily-brief.tsx b/src/features/home/daily-brief.tsx
index dbf6122..371b839 100644
--- a/src/features/home/daily-brief.tsx
+++ b/src/features/home/daily-brief.tsx
@@ -5,7 +5,11 @@ export function DailyBrief({ onOpenContact }: { onOpenContact: (id: string) => v
const { tasks, statuses, entries } = useWorkspaceState();
const focus = tasks.find((task) => task.section === "focus" && task.status === "todo");
const optional = tasks.find((task) => task.section === "optional" && task.status === "todo");
- const contacts = conversations.filter((person) => (statuses[person.id] ?? "now") === "now");
+ const contacts = conversations.filter(
+ (person) =>
+ (statuses[person.id] ?? "now") === "now" &&
+ (person.replyRequired || statuses[person.id] === "now"),
+ );
const firstContact = contacts[0];
const resume = entries.find(
(entry) =>
diff --git a/src/features/home/execution-workbench.css b/src/features/home/execution-workbench.css
new file mode 100644
index 0000000..8621d66
--- /dev/null
+++ b/src/features/home/execution-workbench.css
@@ -0,0 +1,333 @@
+/* Flexible day flow: quiet activity blocks, with detail where the action happens. */
+.execution-day {
+ min-width: 0;
+}
+.execution-flow-group {
+ margin-bottom: 30px;
+}
+.execution-flow-heading {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+.execution-flow-heading h2 {
+ font-size: 14px;
+ font-weight: 600;
+}
+.execution-flow-heading > span {
+ color: var(--muted-foreground);
+ font-size: 11px;
+}
+.execution-flow-items {
+ display: grid;
+ gap: 9px;
+}
+.execution-flow-card {
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ background: var(--muted);
+ overflow: hidden;
+}
+.execution-flow-card[data-kind="focus"] {
+ background: color-mix(in oklch, var(--color-activity-lilac) 16%, var(--background));
+}
+.execution-flow-card[data-kind="routine"] {
+ background: color-mix(in oklch, var(--color-activity-sage) 16%, var(--background));
+}
+.execution-flow-card[data-kind="reply"] {
+ background: color-mix(in oklch, var(--color-activity-lilac) 9%, var(--background));
+}
+.execution-flow-card[data-kind="weight"] {
+ background: color-mix(in oklch, var(--color-activity-peach) 15%, var(--background));
+}
+.execution-flow-card[data-kind="optional"] {
+ background: color-mix(in oklch, var(--color-activity-sage) 9%, var(--background));
+}
+.execution-flow-card[data-expanded="true"] {
+ border-color: color-mix(in oklch, var(--foreground) 28%, var(--border));
+}
+.execution-flow-card[data-status="done"] {
+ background: var(--background);
+}
+.execution-flow-card[data-status="done"] .execution-flow-title {
+ color: var(--muted-foreground);
+}
+.execution-flow-card[data-status="done"] .execution-complete,
+.execution-flow-card[data-status="done"] .execution-flow-icon {
+ color: var(--color-success);
+}
+.execution-flow-row {
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ padding: 16px 18px;
+}
+.execution-complete,
+.execution-flow-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 24px;
+ height: 28px;
+ color: var(--muted-foreground);
+}
+.execution-complete {
+ cursor: pointer;
+ border-radius: 50%;
+}
+.execution-complete:disabled {
+ cursor: default;
+ opacity: 0.45;
+}
+.execution-complete:hover:not(:disabled) {
+ color: var(--foreground);
+}
+.execution-complete svg,
+.execution-flow-icon svg {
+ width: 21px;
+ height: 21px;
+ stroke-width: 1.5;
+}
+.execution-flow-open {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ flex-direction: column;
+ text-align: left;
+ gap: 5px;
+ cursor: pointer;
+ border-radius: 3px;
+}
+.execution-flow-title {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 1.5;
+}
+.execution-flow-description {
+ color: var(--muted-foreground);
+ font-size: 12px;
+ line-height: 1.5;
+}
+.execution-expand-icon {
+ width: 15px;
+ height: 15px;
+ flex-shrink: 0;
+ color: var(--muted-foreground);
+}
+.execution-flow-card[data-expanded="true"] > .execution-flow-row > .execution-expand-icon {
+ transform: rotate(180deg);
+}
+.execution-inline-detail {
+ padding: 6px 22px 24px 55px;
+}
+.execution-inline-detail h2 {
+ display: none;
+}
+.execution-detail-symbol,
+.execution-eyebrow {
+ display: none;
+}
+.execution-description {
+ font-size: 13px;
+ line-height: 1.8;
+ color: var(--muted-foreground);
+ max-width: 480px;
+}
+.execution-routine-progress {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ margin: 20px 0 8px;
+}
+.execution-step-list {
+ display: grid;
+}
+.execution-step-list label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 0;
+ cursor: pointer;
+ border-top: 1px solid var(--border);
+ font-size: 13px;
+}
+.execution-step-list label > .execution-step-text {
+ flex: 1;
+}
+.execution-step-list label[data-checked="true"] > .execution-step-text {
+ text-decoration: line-through;
+ color: var(--muted-foreground);
+}
+.execution-step-number {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ font-variant-numeric: tabular-nums;
+}
+.execution-next-step {
+ padding: 14px 0;
+ margin-top: 14px;
+ border-top: 1px solid var(--border);
+}
+.execution-next-step > span {
+ font-size: 11px;
+ color: var(--muted-foreground);
+}
+.execution-next-step p {
+ font-size: 13px;
+ line-height: 1.75;
+ margin-top: 5px;
+}
+.execution-detail-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 20px;
+}
+.execution-resume-note {
+ border-top: 1px solid var(--border);
+ margin-top: 25px;
+ padding-top: 20px;
+}
+.execution-resume-note > label {
+ display: block;
+ font-size: 12px;
+ font-weight: 500;
+ margin-bottom: 10px;
+}
+.execution-resume-note textarea {
+ min-height: 74px;
+ background: var(--background);
+ font-size: 12px;
+}
+.execution-note-footer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ justify-content: space-between;
+ margin-top: 8px;
+}
+.execution-note-footer > span {
+ color: var(--muted-foreground);
+ font-size: 11px;
+}
+.execution-order {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
+ margin-top: 20px;
+ color: var(--muted-foreground);
+}
+.execution-order > span {
+ font-size: 11px;
+ margin-right: auto;
+}
+.execution-weight-form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 12px;
+ margin-top: 18px;
+}
+.execution-weight-form > label {
+ width: 100%;
+ font-size: 12px;
+}
+.execution-weight-input {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ max-width: 180px;
+}
+.execution-weight-input input {
+ background: var(--background);
+ font-size: 20px;
+ height: 42px;
+ font-variant-numeric: tabular-nums;
+}
+.execution-weight-input > span {
+ color: var(--muted-foreground);
+ font-size: 13px;
+}
+.execution-weight-form > p {
+ width: 100%;
+ color: var(--destructive);
+ font-size: 12px;
+}
+.execution-source-note {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ margin-top: 20px;
+}
+.execution-flow-empty {
+ font-size: 12px;
+ color: var(--muted-foreground);
+ padding: 16px 0;
+}
+.execution-rest {
+ border-top: 1px solid var(--border);
+ padding-top: 20px;
+}
+.execution-rest summary {
+ cursor: pointer;
+ font-size: 12px;
+ color: var(--muted-foreground);
+}
+.execution-rest summary > span {
+ margin-left: 8px;
+ font-variant-numeric: tabular-nums;
+}
+.execution-rest-items {
+ margin-top: 12px;
+}
+.execution-rest-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 0;
+ font-size: 13px;
+}
+.execution-rest-row > svg {
+ width: 17px;
+ height: 17px;
+ color: var(--muted-foreground);
+}
+.execution-rest-row > span {
+ flex: 1;
+}
+.execution-rest-row small {
+ display: block;
+ color: var(--muted-foreground);
+ font-size: 11px;
+ margin-top: 3px;
+}
+.execution-flow-open:focus-visible,
+.execution-complete:focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: 4px;
+}
+@media (max-width: 767px) {
+ .execution-flow-row {
+ padding: 15px 14px;
+ gap: 10px;
+ }
+ .execution-flow-heading > span {
+ font-size: 10px;
+ }
+ .execution-inline-detail {
+ padding: 4px 18px 20px;
+ }
+ .execution-flow-title {
+ font-size: 13px;
+ }
+ .execution-flow-description {
+ font-size: 11px;
+ }
+ .execution-note-footer {
+ align-items: flex-start;
+ }
+}
diff --git a/src/features/home/execution-workbench.tsx b/src/features/home/execution-workbench.tsx
new file mode 100644
index 0000000..5fff2ce
--- /dev/null
+++ b/src/features/home/execution-workbench.tsx
@@ -0,0 +1,486 @@
+import {
+ ArrowDown,
+ ArrowUp,
+ ArrowUpRight,
+ Check,
+ ChevronDown,
+ Circle,
+ ListChecks,
+ MessageCircle,
+ Pause,
+ RotateCcw,
+ Scale,
+} from "lucide-react";
+import { useState } from "react";
+import { Button } from "#/components/ui/button";
+import { Checkbox } from "#/components/ui/checkbox";
+import { Input } from "#/components/ui/input";
+import { Textarea } from "#/components/ui/textarea";
+import { conversations } from "#/features/support/mock-conversations";
+import {
+ type JournalEntry,
+ type Task,
+ useWorkspaceState,
+} from "#/features/workspace/workspace-state";
+
+type ExecutionWorkbenchProps = {
+ selectedId?: string;
+ onSelect: (id: string) => void;
+ onOpenContact: (id: string) => void;
+};
+
+export function ExecutionWorkbench({
+ selectedId,
+ onSelect,
+ onOpenContact,
+}: ExecutionWorkbenchProps) {
+ const { tasks, setTasks, statuses, entries } = useWorkspaceState();
+ const currentEntries = entries.filter(
+ (entry) => !entries.some((other) => other.correctionOf === entry.id),
+ );
+ const weightEntry = currentEntries.find((entry) => entry.kind === "体重");
+ const replies = conversations.filter(
+ (conversation) => conversation.replyRequired || statuses[conversation.id] === "now",
+ );
+ function toggle(id: string) {
+ onSelect(selectedId === id ? "" : id);
+ }
+ function finish(id: string) {
+ setTasks((current) =>
+ current.map((task) =>
+ task.id === id ? { ...task, status: task.status === "todo" ? "done" : "todo" } : task,
+ ),
+ );
+ }
+ return (
+
+ {(
+ [
+ { id: "focus", label: "Start here", subtitle: "Just the next thing" },
+ { id: "routine", label: "Routines", subtitle: "Find your rhythm" },
+ { id: "optional", label: "If there’s room", subtitle: "No need to fit it all in" },
+ ] as const
+ ).map((section) => (
+
+
+
+ {tasks
+ .filter((task) => task.section === section.id)
+ .map((task) => {
+ const id = `task:${task.id}`;
+ const expanded = selectedId === id;
+ const steps = task.steps ?? [];
+ const checked = steps.filter((step) => task.completedSteps?.includes(step)).length;
+ return (
+
+
+
0 && checked < steps.length
+ }
+ onClick={() => finish(task.id)}
+ >
+ {task.status === "done" ? (
+
+ ) : task.status === "later" ? (
+
+ ) : (
+
+ )}
+
+
toggle(id)}
+ >
+ {task.title}
+
+ {task.status === "done"
+ ? "Completed"
+ : task.status === "later"
+ ? "Set aside"
+ : steps.length
+ ? `${checked} of ${steps.length} steps · ${steps.find((step) => !task.completedSteps?.includes(step)) ?? "Ready to finish"}`
+ : task.id === "review"
+ ? "Start with the first screen"
+ : "At your own pace"}
+
+
+
+
+ {expanded && (
+
+ onSelect("")} />
+
+ )}
+
+ );
+ })}
+ {section.id === "focus" &&
+ replies.slice(0, 2).map((conversation) => (
+
+
+
+ {statuses[conversation.id] === "sent" ||
+ statuses[conversation.id] === "dismissed" ? (
+
+ ) : (
+
+ )}
+
+
onOpenContact(conversation.id)}
+ >
+ Reply to {conversation.name}
+
+ {statuses[conversation.id] === "sent"
+ ? "Reply recorded · Preview only"
+ : statuses[conversation.id] === "dismissed"
+ ? "No reply needed"
+ : statuses[conversation.id] === "later"
+ ? "Set aside"
+ : statuses[conversation.id] === "waiting"
+ ? "Waiting for their reply"
+ : conversation.subject}
+
+
+
+
+
+ ))}
+ {section.id === "routine" && (
+
+
+ {weightEntry ? : }
+ toggle("weight")}
+ >
+ Record weight
+
+ {weightEntry ? `Recorded · ${weightEntry.text} kg` : "A quick check-in"}
+
+
+
+
+ {selectedId === "weight" && (
+
+ onSelect("")}
+ />
+
+ )}
+
+ )}
+ {!tasks.some((task) => task.section === section.id && task.status === "todo") &&
+ (section.id !== "focus" || !replies.length) &&
+ section.id !== "routine" && (
+
A little breathing room.
+ )}
+
+
+ ))}
+
+ );
+}
+function TaskDetail({ task, onAction }: { task: Task; onAction: () => void }) {
+ const { setTasks, entries, setEntries } = useWorkspaceState();
+ const source = `task:${task.id}`;
+ const savedNote = entries.find((entry) => entry.kind === "再開地点" && entry.source === source);
+ const [note, setNote] = useState(savedNote?.text ?? "");
+ const [notice, setNotice] = useState("");
+ const steps = task.steps ?? [];
+ const checked = steps.filter((step) => task.completedSteps?.includes(step)).length;
+ function setStatus(status: Task["status"]) {
+ setTasks((current) =>
+ current.map((item) => (item.id === task.id ? { ...item, status } : item)),
+ );
+ onAction();
+ }
+ function move(direction: -1 | 1) {
+ setTasks((current) => {
+ const position = current.findIndex((item) => item.id === task.id);
+ const siblings = current
+ .map((item, index) => ({ item, index }))
+ .filter(({ item }) => item.section === task.section && item.status === task.status);
+ const sibling = siblings.findIndex(({ item }) => item.id === task.id);
+ const target = siblings[sibling + direction];
+ const origin = current[position];
+ if (!target || !origin) return current;
+ const updated = [...current];
+ updated[position] = target.item;
+ updated[target.index] = origin;
+ return updated;
+ });
+ }
+ return (
+ <>
+
+
+
+
+ {task.section === "routine"
+ ? "Your routine"
+ : task.section === "optional"
+ ? "If there’s room"
+ : "One next step"}
+
+ {task.title}
+ {steps.length > 0 ? (
+ <>
+
+
+ {checked} of {steps.length} steps
+
+
+ {task.status === "done"
+ ? "Completed"
+ : checked === steps.length
+ ? "Ready to finish"
+ : "At your pace"}
+
+
+
+ {steps.map((step, index) => (
+
+
+ setTasks((current) =>
+ current.map((item) =>
+ item.id === task.id
+ ? {
+ ...item,
+ completedSteps: value
+ ? Array.from(new Set([...(item.completedSteps ?? []), step]))
+ : (item.completedSteps ?? []).filter(
+ (completed) => completed !== step,
+ ),
+ }
+ : item,
+ ),
+ )
+ }
+ />
+ {step}
+ {String(index + 1).padStart(2, "0")}
+
+ ))}
+
+ >
+ ) : (
+ <>
+
+ {task.id === "review"
+ ? "Look through the first-time experience. Leave a short note on anything that feels unclear."
+ : task.section === "optional"
+ ? "There’s room to decide later. Start whenever it feels right."
+ : "Choose a small first step. You can leave a note if you need to pause."}
+
+ {task.id === "review" && (
+
+
Start here
+
Open the materials and look at the first screen.
+
+ )}
+ >
+ )}
+
+ {task.status !== "done" && (
+
setStatus("done")}
+ disabled={steps.length > 0 && checked < steps.length}
+ >
+ {steps.length > 0 ? "Finish routine" : "Mark complete"}
+
+ )}
+ {task.status === "todo" && (
+
setStatus("later")}>
+ Later
+
+ )}
+ {task.status !== "todo" && (
+
setStatus("todo")}>
+ Bring back
+
+ )}
+
+
+
+
Order in your list
+
move(-1)}
+ >
+
+
+
move(1)}
+ >
+
+
+
+ >
+ );
+}
+
+function WeightDetail({ entry, onAction }: { entry?: JournalEntry; onAction: () => void }) {
+ const { setEntries } = useWorkspaceState();
+ const [weight, setWeight] = useState(entry?.text ?? "");
+ const [error, setError] = useState("");
+ return (
+ <>
+
+
+
+ A quick check-in
+ Record your weight
+ Just a measurement. No target to hit.
+
+ {entry && (
+
+
Recorded
+
{entry.text} kg
+
+ )}
+ Your measurement also appears in Journal.
+ >
+ );
+}
diff --git a/src/features/home/home-page.tsx b/src/features/home/home-page.tsx
index 6193195..6bdb617 100644
--- a/src/features/home/home-page.tsx
+++ b/src/features/home/home-page.tsx
@@ -1,21 +1,8 @@
import { Link, useNavigate, useRouteContext } from "@tanstack/react-router";
-import {
- ArrowDown,
- ArrowUp,
- ArrowUpRight,
- Check,
- ChevronRight,
- Circle,
- Coffee,
- MessagesSquare,
- Plus,
- RotateCcw,
- X,
-} from "lucide-react";
+import { ArrowUp, ArrowUpRight, Plus, X } from "lucide-react";
import { useRef, useState } from "react";
import { Dialog } from "#/components/dialog";
import { Button } from "#/components/ui/button";
-import { Checkbox } from "#/components/ui/checkbox";
import { SidebarInset, SidebarProvider } from "#/components/ui/sidebar";
import { Textarea } from "#/components/ui/textarea";
import { TooltipProvider } from "#/components/ui/tooltip";
@@ -26,11 +13,12 @@ import { ConversationDetail } from "#/features/support/conversation-detail";
import { conversations } from "#/features/support/mock-conversations";
import { entryKindLabels, useWorkspaceState } from "#/features/workspace/workspace-state";
import { DailyBrief } from "./daily-brief";
+import { ExecutionWorkbench } from "./execution-workbench";
import { type HomeAction, interpretAction } from "./interpret-action";
export function HomePage() {
const { auth } = useRouteContext({ from: "__root__" });
- const { tasks, setTasks, statuses, setStatuses, outcomes, setOutcomes, entries, setEntries } =
+ const { tasks, setTasks, setStatuses, outcomes, setOutcomes, entries, setEntries } =
useWorkspaceState();
const navigate = useNavigate();
const input = useRef(null);
@@ -49,11 +37,9 @@ export function HomePage() {
updateSearch({ article: id, conversation: undefined });
const contact = conversations.find((item) => item.id === contactId);
const article = articles.find((item) => item.id === articleId);
- const waiting = conversations.filter((item) => (statuses[item.id] ?? "now") === "now");
const currentEntries = entries.filter(
(entry) => !entries.some((other) => other.correctionOf === entry.id),
);
- const rest = tasks.filter((task) => task.status !== "todo");
function suggest(text: string) {
setPrompt(text);
@@ -230,215 +216,12 @@ export function HomePage() {
{notice}
-
- {(
- [
- { id: "focus", label: "Start here" },
- { id: "routine", label: "Routines" },
- { id: "optional", label: "If there’s room" },
- ] as const
- ).map((section) => (
-
-
- {section.label}
-
- suggest(
- section.id === "routine"
- ? "Add routine:"
- : section.id === "optional"
- ? "Optional: "
- : "Add:",
- )
- }
- >
-
-
-
- {tasks
- .filter((task) => task.section === section.id && task.status === "todo")
- .map((task) => (
-
-
- setTasks((current) =>
- current.map((item) =>
- item.id === task.id ? { ...item, status: "done" } : item,
- ),
- )
- }
- >
-
-
-
-
{task.title}
- {task.steps && (
-
- One step at a time
-
- {task.steps.map((step) => (
-
-
- setTasks((current) =>
- current.map((item) =>
- item.id === task.id
- ? {
- ...item,
- completedSteps: checked
- ? [...(item.completedSteps ?? []), step]
- : (item.completedSteps ?? []).filter(
- (value) => value !== step,
- ),
- }
- : item,
- ),
- )
- }
- />
- {step}
-
- ))}
-
-
- )}
-
-
-
moveTask(task.id, -1)}
- >
-
-
-
moveTask(task.id, 1)}
- >
-
-
-
- setTasks((current) =>
- current.map((item) =>
- item.id === task.id ? { ...item, status: "later" } : item,
- ),
- )
- }
- >
- Later
-
-
-
- ))}
- {section.id === "focus" &&
- waiting.slice(0, 2).map((item) => (
-
-
- setContactId(item.id)}
- >
- Reply to {item.name}
- {item.subject}
-
- setContactId(item.id)}
- >
-
-
-
- ))}
- {!tasks.some(
- (task) => task.section === section.id && task.status === "todo",
- ) &&
- (section.id !== "focus" || !waiting.length) && (
- It’s okay to leave some room.
- )}
-
- ))}
- {rest.length > 0 && (
-
-
- Later & done {rest.length}
-
- {rest.map((task) => (
-
- {task.status === "done" ? (
-
- ) : (
-
- )}
- {task.title}
-
- setTasks((current) =>
- current.map((item) =>
- item.id === task.id ? { ...item, status: "todo" } : item,
- ),
- )
- }
- >
-
-
-
- ))}
-
- )}
-
+
updateSearch({ activity: id || undefined })}
+ onOpenContact={setContactId}
+ />
-
-
- {conversations
- .filter((item) => !["sent", "dismissed"].includes(statuses[item.id] ?? "now"))
- .map((item) => (
- setContactId(item.id)}
- >
- {item.name}
-
- {statuses[item.id] === "waiting"
- ? "Waiting"
- : statuses[item.id] === "later"
- ? "Later"
- : item.channels.join(" / ")}
-
-
- ))}
-
Journal
diff --git a/src/features/home/home.css b/src/features/home/home.css
index 64c3ad7..0e65a83 100644
--- a/src/features/home/home.css
+++ b/src/features/home/home.css
@@ -14,9 +14,9 @@
overflow-y: auto;
}
.home-content {
- max-width: 1140px;
+ max-width: 1200px;
margin: auto;
- padding: 64px 48px 80px;
+ padding: 40px 48px 80px;
}
.home-greeting {
margin-bottom: 32px;
@@ -88,8 +88,8 @@
}
.home-columns {
display: grid;
- grid-template-columns: minmax(0, 1.65fr) minmax(240px, 1fr);
- gap: 64px;
+ grid-template-columns: minmax(0, 2.5fr) minmax(190px, 1fr);
+ gap: 40px;
margin-top: 24px;
}
.home-section {
diff --git a/src/features/navigation/search.ts b/src/features/navigation/search.ts
index 0e3734e..0382656 100644
--- a/src/features/navigation/search.ts
+++ b/src/features/navigation/search.ts
@@ -5,6 +5,7 @@ const flag = z.literal(true).optional().catch(undefined);
const workspaceSearch = z.object({
q: text,
article: text,
+ activity: text,
conversation: text,
view: z.enum(["now", "later", "waiting", "done", "all"]).optional().catch(undefined),
accounts: flag,
diff --git a/src/features/support/conversation-detail.tsx b/src/features/support/conversation-detail.tsx
index 5036450..fccf4ca 100644
--- a/src/features/support/conversation-detail.tsx
+++ b/src/features/support/conversation-detail.tsx
@@ -1,5 +1,5 @@
-import { CalendarPlus, Check, ChevronDown, Send } from "lucide-react";
-import { useState } from "react";
+import { CalendarPlus, Check, ChevronDown, CornerDownRight, Send } from "lucide-react";
+import { useId, useState } from "react";
import { Button } from "#/components/ui/button";
import {
Dialog,
@@ -20,182 +20,199 @@ export function ConversationDetail({
conversation: Conversation;
onResolve: (result: "sent" | "waiting" | "later" | "dismissed", note?: string) => void;
}) {
+ const instanceId = useId();
const [draft, setDraft] = useState(conversation.drafts[0]?.text ?? "");
const [tone, setTone] = useState(conversation.drafts[0]?.label ?? "");
const [review, setReview] = useState<"send" | "schedule" | null>(null);
const [slot, setSlot] = useState("");
const [scheduled, setScheduled] = useState("");
-
return (
-
-
-
- {conversation.summary}
-
-
{conversation.nextStep}
- {conversation.deadline && (
-
- Requested deadline · {conversation.deadline}
-
- )}
-
-
-
-
-
- Original messages{" "}
-
-
-
+
+
+ At a glance
+ {conversation.subject}
+ {conversation.summary}
+
+
{conversation.messages.map((message) => (
-
- {message.direction === "in" ? conversation.name : "You"} · {message.channel} ·{" "}
- {message.time}
+
+ {message.direction === "in" ? conversation.name.split(" ")[0] : "You"}
+
+ {message.channel} · {message.time}
+
-
{message.text}
+
{message.text}
))}
-
-
-
-
-
-
- Reply
-
-
- {conversation.replyChannel} → {conversation.name}
-
-
-
- {conversation.drafts.map((option) => (
- {
- setDraft(option.text);
- setTone(option.label);
- }}
- >
- {option.label}
-
- ))}
-
-
+
+
+
+
+ Context
+
+
+
+
+ {conversation.name
+ .split(" ")
+ .map((part) => part[0])
+ .join("")}
+
+
{conversation.name}
+
{conversation.role}
+
+
+
+
+ Next step
+
+ {conversation.nextStep}
+ {conversation.deadline && (
+
+ Requested by
+ {conversation.deadline}
+
+ )}
+
+
+ From the conversation
+
+
+
+
{
diff --git a/src/features/support/mock-conversations.ts b/src/features/support/mock-conversations.ts
index f990198..2ba7eb9 100644
--- a/src/features/support/mock-conversations.ts
+++ b/src/features/support/mock-conversations.ts
@@ -7,6 +7,7 @@ export type Conversation = {
subject: string;
channels: Channel[];
replyChannel: Channel;
+ replyRequired: boolean;
updatedAt: string;
deadline?: string;
summary: string;
@@ -32,6 +33,7 @@ export const conversations: Conversation[] = [
subject: "Schedule next week’s design review",
channels: ["Slack", "Gmail"],
replyChannel: "Slack",
+ replyRequired: true,
updatedAt: "10:42",
deadline: "Sep 29, 17:00",
summary:
@@ -86,6 +88,7 @@ export const conversations: Conversation[] = [
subject: "Catch up over coffee",
channels: ["LINE"],
replyChannel: "LINE",
+ replyRequired: true,
updatedAt: "Yesterday",
summary: "An invitation to meet for coffee while Naoto is nearby. There is no rush to reply.",
nextStep:
@@ -128,6 +131,7 @@ export const conversations: Conversation[] = [
subject: "Confirm the scope for an estimate",
channels: ["Gmail"],
replyChannel: "Gmail",
+ replyRequired: true,
updatedAt: "9:10",
deadline: "Sep 30, 12:00",
summary:
@@ -171,6 +175,7 @@ export const conversations: Conversation[] = [
subject: "The article we talked about",
channels: ["Slack"],
replyChannel: "Slack",
+ replyRequired: false,
updatedAt: "Friday",
summary:
"Yu shared an article discussed at the last study group. There are no questions or requests for a reply.",
diff --git a/src/features/support/support-page.tsx b/src/features/support/support-page.tsx
index eac316e..c118a80 100644
--- a/src/features/support/support-page.tsx
+++ b/src/features/support/support-page.tsx
@@ -1,4 +1,4 @@
-import { ArrowLeft, Search } from "lucide-react";
+import { ArrowLeft, MessageSquare, Search } from "lucide-react";
import { useState } from "react";
import { Button } from "#/components/ui/button";
import { Input } from "#/components/ui/input";
@@ -18,67 +18,75 @@ import { ConversationDetail } from "./conversation-detail";
import { conversations } from "./mock-conversations";
const views = [
- { id: "now", label: "Now" },
- { id: "later", label: "Later" },
- { id: "waiting", label: "Waiting" },
- { id: "done", label: "History" },
+ { id: "now", label: "Inbox" },
{ id: "all", label: "All" },
+ { id: "later", label: "Set aside" },
+ { id: "waiting", label: "Awaiting reply" },
+ { id: "done", label: "Archive" },
] as const;
+
export function SupportPage() {
const [search, updateSearch] = useUrlState();
const view = search.view ?? "now";
const query = search.q ?? "";
const searchState = useSearchInput("conversation");
- const searchInput = {
- value: searchState.value,
- onChange: searchState.onChange,
- onCompositionStart: searchState.onCompositionStart,
- onCompositionEnd: searchState.onCompositionEnd,
- };
const { statuses, setStatuses, outcomes, setOutcomes } = useWorkspaceState();
const selectedId = search.conversation;
- const reading = !!selectedId;
const [notice, setNotice] = useState("");
- const matches = conversations.filter((item) => {
+ const visible = conversations.filter((item) => {
const status = statuses[item.id] ?? "now";
return (
- (view === "all"
- ? true
- : view === "done"
- ? status === "sent" || status === "dismissed"
- : status === view) && `${item.name} ${item.subject}`.includes(query)
+ (view !== "now" || item.replyRequired || statuses[item.id] === "now") &&
+ (view === "all" ||
+ (view === "done" ? status === "sent" || status === "dismissed" : status === view)) &&
+ `${item.name} ${item.subject} ${item.channels.join(" ")}`
+ .toLowerCase()
+ .includes(query.toLowerCase())
);
});
- const visible = view === "now" ? matches.slice(0, 3) : matches;
const selected = visible.find((item) => item.id === selectedId) ?? visible[0];
function resolve(id: string, status: Exclude, note?: string) {
setStatuses((current) => ({ ...current, [id]: status }));
if (note) setOutcomes((current) => ({ ...current, [id]: note }));
setNotice(
status === "sent"
- ? "Reply preview recorded"
+ ? "Reply preview recorded. No message sent."
: status === "later"
- ? "Moved to Later"
+ ? "Set aside for later"
: status === "waiting"
- ? "Moved to Waiting"
- : "Marked as no action needed",
+ ? "Awaiting their reply"
+ : "Archived without replying",
);
updateSearch({ conversation: undefined });
}
return (
-
+
-
- Messages
-
+
+
+
Messages
+ {visible.length}
+
+
+
+
+
+
{views.map((tab) => (
- {
updateSearch({
@@ -89,18 +97,9 @@ export function SupportPage() {
}}
>
{tab.label}
-
+
))}
-
-
-
-
-
+
{visible.map((item) => (
@@ -109,23 +108,33 @@ export function SupportPage() {
className="support-row"
aria-current={selected?.id === item.id ? "true" : undefined}
key={item.id}
- onClick={() => {
- updateSearch({ conversation: item.id });
- }}
+ onClick={() => updateSearch({ conversation: item.id })}
>
- {item.name}
- {item.subject}
+
+ {item.name
+ .split(" ")
+ .map((part) => part[0])
+ .join("")}
+
+
+
+ {item.name}
+ {item.updatedAt}
+
+ {item.subject}
+ {item.channels.join(" · ")}
+
))}
{!visible.length && (
- {query ? "No results found" : "No messages to handle right now"}
+ {query ? "No results found" : "Nothing here. You’re all caught up."}
)}
- {view === "now" && matches.length > 3 && (
- {matches.length - 3} more
- )}
+
+ {notice || "Sample conversations"}
+
@@ -139,54 +148,55 @@ export function SupportPage() {
>
- {selected?.name}
-
- {notice}
-
+ {selected && (
+ <>
+
+ {selected.name
+ .split(" ")
+ .map((part) => part[0])
+ .join("")}
+
+
+
{selected.name}
+
{selected.channels.join(" · ")}
+
+ >
+ )}
+ {selected && view !== "now" && (
+
+ {
+ setStatuses((current) => ({ ...current, [selected.id]: "now" }));
+ updateSearch({ view: undefined, conversation: selected.id });
+ }}
+ >
+ Move to Inbox
+
+
+ )}
-
+
{conversations.map((conversation) => (
resolve(conversation.id, status, note)}
/>
-
- ))}
- {selected ? (
- <>
- {outcomes[selected.id] && (
+ {outcomes[conversation.id] && (
-
Activity log
-
{outcomes[selected.id]}
+
Reply preview
+
{outcomes[conversation.id]}
)}
- {view !== "now" && (
- {
- setStatuses((current) => ({
- ...current,
- [selected.id]: "now",
- }));
- updateSearch({
- view: undefined,
- conversation: selected.id,
- });
- }}
- >
- Move to Now
-
- )}
- >
- ) : (
- Select a conversation
+
+ ))}
+ {!selected && (
+
+
+
Select a conversation
+
)}
diff --git a/src/features/support/support.css b/src/features/support/support.css
index a626d94..52d84f5 100644
--- a/src/features/support/support.css
+++ b/src/features/support/support.css
@@ -1,11 +1,11 @@
-/* Hallmark · sidebar workbench · neutral · P5 H5 E4 S5 R5 V4 */
+/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V4 · existing neutral tokens */
.support-workspace {
height: 100dvh;
min-height: 0;
overflow: hidden;
}
.support-sidebar {
- width: 390px;
+ width: 370px;
flex-shrink: 0;
flex-direction: row;
border-right: 1px solid var(--border);
@@ -15,22 +15,126 @@
min-width: 0;
width: auto;
}
+.support-list-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.support-list-title h1 {
+ font-size: 19px;
+ font-weight: 600;
+ letter-spacing: -0.04em;
+}
+.support-list-title > span {
+ font-size: 12px;
+ color: var(--muted-foreground);
+}
+.support-filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+.support-filters button,
+.conversation-draft-options button {
+ cursor: pointer;
+ padding: 6px 9px;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ border-radius: 6px;
+ white-space: nowrap;
+}
+.support-filters button[aria-pressed="true"],
+.conversation-draft-options button[aria-pressed="true"] {
+ background: var(--sidebar-accent);
+ color: var(--foreground);
+}
+.support-filters button:hover,
+.conversation-draft-options button:hover {
+ background: var(--muted);
+ color: var(--foreground);
+}
.support-row {
display: flex;
- flex-direction: column;
- gap: 10px;
- padding: 22px 20px;
- border-bottom: 1px solid var(--border);
+ gap: 12px;
+ padding: 17px 14px;
+ margin: 2px 8px;
+ border-radius: 10px;
text-align: left;
cursor: pointer;
- line-height: 1.6;
+ line-height: 1.5;
}
.support-row:hover {
background: var(--sidebar-accent);
}
.support-row[aria-current="true"] {
+ background: var(--sidebar-accent);
+}
+.support-row:focus-visible,
+.support-filters button:focus-visible,
+.conversation-draft-options button:focus-visible,
+.conversation-scheduling summary:focus-visible,
+.conversation-context summary:focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: -2px;
+}
+.support-row:active {
background: var(--muted);
- box-shadow: inset 2px 0 var(--primary);
+}
+.support-avatar {
+ display: grid;
+ place-items: center;
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ flex: 0 0 auto;
+ background: var(--muted);
+ color: var(--muted-foreground);
+ font-size: 12px;
+ font-weight: 500;
+ border: 1px solid var(--border);
+}
+.support-avatar-small {
+ width: 32px;
+ height: 32px;
+ font-size: 10px;
+}
+.support-row-content {
+ display: flex;
+ min-width: 0;
+ flex: 1;
+ flex-direction: column;
+ gap: 3px;
+}
+.support-row-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+}
+.support-row-heading strong {
+ font-size: 12px;
+ font-weight: 550;
+}
+.support-row-heading > span {
+ font-size: 10px;
+ color: var(--muted-foreground);
+ white-space: nowrap;
+}
+.support-row-preview {
+ font-size: 12px;
+ color: var(--muted-foreground);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.support-row-channel {
+ font-size: 10px;
+ color: var(--muted-foreground);
+}
+.support-list-footer {
+ padding: 16px 20px;
+ font-size: 11px;
+ color: var(--muted-foreground);
}
.support-main {
min-width: 0;
@@ -40,33 +144,283 @@
.support-toolbar {
display: flex;
align-items: center;
- gap: 12px;
- min-height: 56px;
- padding: 12px 24px;
+ gap: 10px;
+ min-height: 70px;
+ padding: 12px 28px;
border-bottom: 1px solid var(--border);
}
+.support-toolbar strong {
+ font-size: 13px;
+ font-weight: 550;
+}
+.support-toolbar p {
+ font-size: 10px;
+ color: var(--muted-foreground);
+ margin-top: 2px;
+}
.support-detail {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-wrap: anywhere;
-}
-.support-detail > * {
- max-width: 800px;
- margin-inline: auto;
+ container-type: inline-size;
}
.support-outcome {
padding: 24px;
border-top: 1px solid var(--border);
+ font-size: 12px;
+}
+.support-outcome p {
+ white-space: pre-wrap;
+ margin-top: 8px;
+ color: var(--muted-foreground);
}
.support-empty {
padding: 64px 24px;
color: var(--muted-foreground);
text-align: center;
+ font-size: 13px;
+ display: grid;
+ justify-items: center;
+ gap: 12px;
}
.support-back {
display: none;
}
+.conversation-workbench {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ min-width: 0;
+}
+.conversation-thread {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+.conversation-summary {
+ padding: 26px 30px 22px;
+ border-bottom: 1px solid var(--border);
+}
+.conversation-eyebrow {
+ color: var(--muted-foreground);
+ font-size: 10px;
+}
+.conversation-summary h2 {
+ margin-top: 7px;
+ font-size: 15px;
+ line-height: 1.5;
+ font-weight: 550;
+ letter-spacing: -0.015em;
+}
+.conversation-summary > p {
+ margin-top: 8px;
+ font-size: 12px;
+ line-height: 1.8;
+ color: var(--muted-foreground);
+ max-width: 64ch;
+}
+.conversation-messages {
+ padding: 28px 30px 34px;
+ display: flex;
+ flex-direction: column;
+ gap: 22px;
+ flex: 1;
+}
+.conversation-message {
+ max-width: 90%;
+ border-radius: 12px;
+ scroll-margin: 24px;
+}
+.conversation-message:focus {
+ outline: 2px solid var(--ring);
+ outline-offset: 5px;
+}
+.conversation-message[data-direction="out"] {
+ align-self: flex-end;
+}
+.conversation-message-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 10px;
+ font-size: 10px;
+ margin-bottom: 7px;
+ color: var(--muted-foreground);
+}
+.conversation-message-meta span {
+ opacity: 0.8;
+}
+.conversation-bubble {
+ padding: 14px 17px;
+ border-radius: 3px 14px 14px;
+ background: var(--muted);
+ font-size: 13px;
+ line-height: 1.8;
+ white-space: pre-wrap;
+}
+.conversation-message[data-direction="out"] .conversation-bubble {
+ background: var(--accent);
+ border-radius: 14px 3px 14px 14px;
+}
+.conversation-compose {
+ padding: 20px 30px 24px;
+ border-top: 1px solid var(--border);
+ display: grid;
+ gap: 12px;
+}
+.conversation-compose-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 12px;
+}
+.conversation-compose-heading label {
+ font-weight: 550;
+}
+.conversation-compose-heading span {
+ font-size: 11px;
+ color: var(--muted-foreground);
+}
+.conversation-draft-options {
+ display: flex;
+ gap: 4px;
+}
+.conversation-compose-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.conversation-compose-footer > span {
+ color: var(--muted-foreground);
+ font-size: 10px;
+}
+.conversation-triage {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 2px;
+}
+.conversation-scheduling summary {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ list-style: none;
+ font-size: 11px;
+ cursor: pointer;
+ color: var(--muted-foreground);
+}
+.conversation-scheduling summary svg {
+ width: 13px;
+ height: 13px;
+}
+.conversation-scheduling > div {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 10px;
+}
+.conversation-context {
+ border-top: 1px solid var(--border);
+ min-width: 0;
+ padding: 22px;
+ background: var(--sidebar);
+}
+.conversation-context-details > summary {
+ list-style: none;
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 500;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.conversation-context-details > summary svg {
+ width: 14px;
+ height: 14px;
+}
+.conversation-person {
+ display: grid;
+ justify-items: start;
+ padding: 25px 0;
+}
+.conversation-person h3 {
+ margin-top: 12px;
+ font-size: 13px;
+ font-weight: 550;
+}
+.conversation-person p {
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--muted-foreground);
+}
+.conversation-next {
+ padding: 20px 0;
+ border-block: 1px solid var(--border);
+}
+.conversation-next h3,
+.conversation-facts h3 {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ font-weight: 550;
+}
+.conversation-next h3 svg {
+ width: 13px;
+ height: 13px;
+}
+.conversation-next > p {
+ font-size: 12px;
+ line-height: 1.8;
+ margin-top: 12px;
+}
+.conversation-deadline {
+ display: grid;
+ gap: 5px;
+ margin-top: 18px;
+ font-size: 11px;
+}
+.conversation-deadline span {
+ color: var(--muted-foreground);
+}
+.conversation-deadline strong {
+ font-weight: 500;
+}
+.conversation-next > .conversation-next-note {
+ font-size: 10px;
+ color: var(--muted-foreground);
+ margin-top: 16px;
+}
+.conversation-facts {
+ padding-top: 22px;
+}
+.conversation-facts ul {
+ display: grid;
+ gap: 18px;
+ padding-top: 16px;
+}
+.conversation-facts li p {
+ font-size: 11px;
+ line-height: 1.8;
+}
+.conversation-facts a {
+ display: inline-block;
+ margin-top: 5px;
+ color: var(--muted-foreground);
+ font-size: 10px;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+@container (min-width: 780px) {
+ .conversation-workbench {
+ grid-template-columns: minmax(0, 1fr) 250px;
+ min-height: calc(100dvh - 70px);
+ }
+ .conversation-context {
+ border-top: 0;
+ border-left: 1px solid var(--border);
+ }
+}
@media (max-width: 767px) {
.support-sidebar {
width: 100%;
@@ -86,4 +440,22 @@
.support-toolbar {
padding: 12px;
}
+ .conversation-summary,
+ .conversation-compose {
+ padding: 20px;
+ }
+ .conversation-messages {
+ padding: 24px 20px;
+ }
+ .conversation-message {
+ max-width: 100%;
+ }
+ .conversation-compose-footer > span {
+ max-width: 100px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .conversation-message {
+ scroll-behavior: auto;
+ }
}
diff --git a/src/styles.css b/src/styles.css
index c81fc01..292f3f9 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -11,4 +11,5 @@
@import "./features/journal/journal.css";
@import "./features/support/support.css";
@import "./features/home/home.css";
+@import "./features/home/execution-workbench.css";
@import "./features/vitals/vitals.css";
diff --git a/tokens.css b/tokens.css
index 9b4d2df..c7d6023 100644
--- a/tokens.css
+++ b/tokens.css
@@ -15,6 +15,9 @@
--color-control-rule: var(--input);
--color-focus: var(--ring);
--color-error: var(--destructive);
+ --color-activity-lilac: oklch(78% 0.09 295);
+ --color-activity-sage: oklch(80% 0.07 155);
+ --color-activity-peach: oklch(82% 0.08 60);
--color-success: oklch(78% 0.13 155);
--color-shadow: oklch(10% 0.015 250 / 45%);
--color-overlay: oklch(10% 0.015 250 / 70%);