From 693683db373f68cf91f56bb9567f249cd2fb7c2e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 24 Aug 2026 21:16:56 +0000 Subject: [PATCH 1/2] feat(ui): use four Korean analyst GNB destinations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace BuyerNav with WorkspaceNav. Analyst chrome is 게시판, 고객 마스터, 달력, and Ask Agent. Admin stays a non-GNB destination. Unwired CalendarWeave consume fail-closes with 이 범위의 일정을 아직 받을 수 없습니다 (ADR 0183). Co-authored-by: Seongho Bae --- CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md | 7 +++ CHANGELOG.md | 16 +++++ docs/adr/0183-gnb-four-korean-chrome.md | 49 +++++++++++++++ docs/storybook-inventory.md | 1 + frontend/package.json | 2 +- frontend/src/App.css | 16 ++--- frontend/src/App.test.tsx | 39 ++++++++---- frontend/src/App.tsx | 30 +++++----- frontend/src/components/BuyerNav.test.tsx | 23 -------- frontend/src/components/BuyerNav.tsx | 39 ------------ .../src/components/WorkspaceNav.stories.tsx | 36 +++++++++++ frontend/src/components/WorkspaceNav.test.tsx | 59 +++++++++++++++++++ frontend/src/components/WorkspaceNav.tsx | 30 ++++++++++ frontend/src/gnbChrome.ts | 14 +++++ frontend/src/i18n.test.ts | 32 ++++++++-- frontend/src/i18n.ts | 8 +-- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 19 files changed, 299 insertions(+), 108 deletions(-) create mode 100644 CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md create mode 100644 docs/adr/0183-gnb-four-korean-chrome.md delete mode 100644 frontend/src/components/BuyerNav.test.tsx delete mode 100644 frontend/src/components/BuyerNav.tsx create mode 100644 frontend/src/components/WorkspaceNav.stories.tsx create mode 100644 frontend/src/components/WorkspaceNav.test.tsx create mode 100644 frontend/src/components/WorkspaceNav.tsx create mode 100644 frontend/src/gnbChrome.ts diff --git a/CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md b/CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md new file mode 100644 index 000000000..fbefdc718 --- /dev/null +++ b/CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md @@ -0,0 +1,7 @@ +# 2.15.0 — Four-destination Korean analyst GNB + +- Analyst Global Navigation is exactly 게시판, 고객 마스터, 달력, and Ask Agent. +- Buyer/Cubee copy is off the chrome; `BuyerNav` is now `WorkspaceNav`. +- Operator Admin stays a non-GNB destination. +- Unwired CalendarWeave consume fail-closes with + `이 범위의 일정을 아직 받을 수 없습니다` (ADR 0183). diff --git a/CHANGELOG.md b/CHANGELOG.md index b2da918ee..eee900975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,22 @@ All notable changes to this project are documented here. Format follows and looks up corporate-parent visibility independently of the child. OWL-Time is cited as a W3C Candidate Recommendation Draft. +## [2.15.0] - 2026-08-25 + +### Changed + +- Analyst Global Navigation is exactly four Korean destinations: 게시판, + 고객 마스터, 달력, and Ask Agent. `BuyerNav` is now `WorkspaceNav`; Buyer + and Cubee are not product names on the chrome (ADR 0183). +- Operator Admin remains available as a non-GNB destination and is not a + fifth analyst tab. + +### Fixed + +- The 달력 destination fail-closes when CalendarWeave / Naruon CalDAV consume + is unwired, with `이 범위의 일정을 아직 받을 수 없습니다`. Weekly VOC and + newspaper stay on the board. + ## [2.12.26] - 2026-08-24 ### Added diff --git a/docs/adr/0183-gnb-four-korean-chrome.md b/docs/adr/0183-gnb-four-korean-chrome.md new file mode 100644 index 000000000..8d004137a --- /dev/null +++ b/docs/adr/0183-gnb-four-korean-chrome.md @@ -0,0 +1,49 @@ +# ADR 0183: Four-destination Korean analyst GNB chrome + +- Status: Accepted +- Date: 2026-08-25 +- Supersedes (labels only): [0037](0037-buyer-gnb-and-product-surface.md) English GNB labels +- Folds: closed unmerged #474 WorkspaceNav rename + +## Context + +Main still shipped `BuyerNav` with English analyst tabs (Board / Customer +master / Calendar / Ask Agent / Admin). LineageWeave is an analyst workspace, +not a storefront, so "Buyer" and "Cubee" are not product names. Calendar is +CalendarWeave / Naruon CalDAV consume only (issue #336); this slice must not +add a calendar kernel. + +## Decision + +1. Analyst Global Navigation is exactly four destinations, in this order, with + these Korean labels regardless of locale: + + 1. 게시판 (board) + 2. 고객 마스터 (customers) + 3. 달력 (calendar) + 4. Ask Agent (ask) + +2. `BuyerNav` / `BuyerDestination` become `WorkspaceNav` / + `WorkspaceDestination`. GNB CSS uses `.workspace-gnb*`. The accessible name + is `Workspace navigation`. +3. Operator Admin remains a non-GNB destination. It is not a fifth analyst tab. +4. Weekly VOC remains a board filter. Weekly/monthly newspaper remains a + scheduled board post. Neither is a GNB item. +5. The 달력 destination fail-closes when CalendarWeave / Naruon consume is + unwired or missing, with the exact copy + `이 범위의 일정을 아직 받을 수 없습니다`. Existing advanced-review + commitment projection is not a calendar kernel and is not this GNB surface. + +## Consequences + +- Analyst chrome no longer shows Buyer, Cubee, Board, or Customer master. +- CalendarWeave wiring stays a later consume-only slice. This PR does not + import naruon mailbox, ThreadWeave, or Keyverse dumps. +- Historical ADRs keep the wording of their time. + +## References + +- ADR 0037 (original four-destination GNB) +- ADR 0038 (CalDAV consume vs internal commitments) +- Issue #336 (Naruon calendar projection) +- Closed unmerged #474 (WorkspaceNav rename) diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 86974dd97..b4c9f3e20 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -13,6 +13,7 @@ buyer-facing control you can click before changing product CSS. | `Admin/AdminPanel` | Change the tenant brand name, then verify the saved or failed state before leaving settings. | `--surface`, `--border`, `--space-panel-block`, `AdminPanel` | | `Lineage/LineageDag` | Open the current branch node; compare empty, grouped/forked, mobile-scroll, ungrouped, and long-title states before changing graph CSS. On narrow viewports, swipe the named viewport or focus it and use arrow keys to inspect the full lineage. | `--surface`, `--border`, `--color-focus-border`, `--size-control-min`, `LineageDag` | | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | +| `Navigation/WorkspaceNav` | Open 게시판, 고객 마스터, 달력, or Ask Agent. Admin is not a GNB tab. | `--gnb-height`, `--gnb-active-indicator-color`, `WorkspaceNav` | | `Evidence/OntologyExplorer` | Inspect typed people/orgs/posts, then open authorized evidence. Distinct from Event Lineage. | `--color-primary`, `--color-table-border`, `OntologyExplorer` | | `Reports/LeftoverPairList` | Read residual R, observed Y, expected E, map rank, and distance after IRT main effects, then open the named post. | `--color-chip-border`, `LeftoverPairList` | diff --git a/frontend/package.json b/frontend/package.json index 33d6c7d6e..5e9cc95d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.14.0", + "version": "2.15.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.css b/frontend/src/App.css index b8e62ffa9..01e491796 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -142,7 +142,7 @@ } /* GNB Navigation (§2.3.1) */ -.buyer-gnb { +.workspace-gnb { display: flex; align-items: center; height: var(--gnb-height); @@ -155,7 +155,7 @@ z-index: var(--z-gnb-pulldown); } -.buyer-gnb-item { +.workspace-gnb-item { display: flex; align-items: center; height: 100%; @@ -170,15 +170,15 @@ transition: color 0.15s ease; } -.buyer-gnb-item:hover { +.workspace-gnb-item:hover { color: var(--color-text-heading); } -.buyer-gnb-item[aria-current="page"] { +.workspace-gnb-item[aria-current="page"] { color: var(--color-primary); } -.buyer-gnb-item[aria-current="page"]::after { +.workspace-gnb-item[aria-current="page"]::after { content: ""; position: absolute; bottom: 0; @@ -188,7 +188,7 @@ background-color: var(--gnb-active-indicator-color); } -.buyer-gnb-tools { +.workspace-gnb-tools { margin-left: auto; display: flex; align-items: center; @@ -906,7 +906,7 @@ .app-header { padding: 0 1rem; } - .buyer-gnb { + .workspace-gnb { padding: 0 1rem; } } @@ -914,7 +914,7 @@ @media (max-width: 768px) { /* Phone Breakpoint (<768px) */ - .buyer-gnb { + .workspace-gnb { display: none; /* Replaced by drawer on mobile */ } diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index fc7e03373..0b4943abb 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1936,7 +1936,7 @@ describe("App, authenticated", () => { stubBackend(); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("Demo Corp")).toBeInTheDocument(); expect(screen.getByText("DEMO-CORP-01 · Company")).toBeInTheDocument(); @@ -1956,7 +1956,7 @@ describe("App, authenticated", () => { stubBackend({ customerEntityHierarchy: true }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("Demo Group")).toBeInTheDocument(); const subsidiaryRow = screen.getByText("Demo Corp").closest("li"); @@ -1976,7 +1976,7 @@ describe("App, authenticated", () => { stubBackend(); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("Northridge Grid")).toBeInTheDocument(); expect(screen.getByText("Voice of Customer (1), Voice of Competitor (1)")).toBeInTheDocument(); @@ -1998,7 +1998,7 @@ describe("App, authenticated", () => { stubBackend({ admin: true, manyCustomerHints: 1 }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("CUST-0")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Resolve" })); @@ -2010,7 +2010,7 @@ describe("App, authenticated", () => { stubBackend({ manyCustomerHints: 1 }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("CUST-0")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Resolve" })).not.toBeInTheDocument(); @@ -2026,7 +2026,7 @@ describe("App, authenticated", () => { stubBackend({ hintRelatedPosts: true }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); const customerSection = await screen.findByRole("region", { name: "Observed customer evidence" }); expect(within(customerSection).getByText("Related posts (1)").closest("details")).toHaveClass( @@ -2052,7 +2052,7 @@ describe("App, authenticated", () => { stubBackend({ manyCustomerHints: 45 }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("CUST-0")).toBeInTheDocument(); expect(screen.getByText(/Showing the first 30 of 45 observed customer identifiers/)).toBeInTheDocument(); @@ -3855,12 +3855,31 @@ describe("App, authenticated", () => { ); }); - it("keeps advanced review tools out of the buyer board", async () => { + it("keeps advanced review tools out of the analyst board", async () => { stubBackend(); render(); - expect(await screen.findByRole("navigation", { name: "Buyer navigation" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Board" })).toHaveAttribute("aria-current", "page"); + const nav = await screen.findByRole("navigation", { name: "Workspace navigation" }); + expect(nav).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "게시판" })).toHaveAttribute("aria-current", "page"); + expect(within(nav).getAllByRole("button").map((button) => button.textContent)).toEqual([ + "게시판", + "고객 마스터", + "달력", + "Ask Agent", + ]); + expect(nav.textContent).not.toMatch(/Buyer|Cubee|Board|Customer master/i); + expect(within(nav).queryByRole("button", { name: /Admin|관리자/i })).not.toBeInTheDocument(); expect(screen.queryByText("Advanced review tools")).not.toBeInTheDocument(); }); + + it("fails closed on the calendar destination when CalendarWeave consume is unwired", async () => { + stubBackend(); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "달력" })); + expect(screen.getByRole("heading", { name: "달력" })).toBeInTheDocument(); + expect(screen.getByText("이 범위의 일정을 아직 받을 수 없습니다")).toBeInTheDocument(); + expect(screen.queryByText(/Buyer|Cubee/i)).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 399485c35..27ada1d75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -90,7 +90,8 @@ import { OntologyExplorer } from "./components/OntologyExplorer"; import { AskEvidenceLayerPopup } from "./components/AskEvidenceLayerPopup"; import { PopupCloseButton } from "./components/PopupCloseButton"; import { chatEvidenceKindLabel } from "./evidenceKindLabels"; -import { BuyerNav, type BuyerDestination } from "./components/BuyerNav"; +import { WorkspaceNav, type WorkspaceDestination } from "./components/WorkspaceNav"; +import { CALENDAR_CONSUME_UNAVAILABLE } from "./gnbChrome"; import { LineageDag } from "./LineageDag"; import { PostBody } from "./PostBody"; import { decodeHtmlEntities } from "./postBodyDisplay"; @@ -4352,10 +4353,10 @@ function CustomerMasterPanel({ } return ( -
+

{t("Authorized customer scope")}

{t("Customer master")}

-

{t("Customer entities available to this account.")}

+

{t("Customer entities available to this account.")}

{error ?

{error}

: null} {master === null && !error ?

{t("Loading customer master...")}

: null} {master?.corporate_entities.length === 0 ? ( @@ -4380,7 +4381,7 @@ function CustomerMasterPanel({ {master && (master.relationship_network ?? []).length > 0 ? (

{t("Relationship network")}

-

+

{t("A counterparty can hold more than one role over time -- a customer in one post can be a competitor, supplier, or partner in another. Every role observed for a name is listed, not just the most frequent.")}

    @@ -4403,7 +4404,7 @@ function CustomerMasterPanel({ {master && master.source_customer_hints.length > 0 ? (

    {t("Observed customer evidence")}

    -

    +

    {t("Source identifiers are hints only; ontology and semantic evidence must resolve them before binding a customer.")}

    {master.source_customer_hints.length > HINT_RENDER_LIMIT && ( @@ -4554,10 +4555,10 @@ function AskAgentPanel({ } return ( -
    +

    {t("Evidence-grounded questions")}

    {t("Ask Agent")}

    -

    {t("Questions use authorized posts and their evidence.")}

    +

    {t("Questions use authorized posts and their evidence.")}

    {error ?

    {error}

    : null}