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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/dogfood-qa-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ jobs:

- name: Comment PR with QA results
if: always()
# Reporting must not override the actual QA result when GitHub's
# comments API is temporarily unavailable.
continue-on-error: true
uses: actions/github-script@v9
with:
script: |
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG/components/ui-primitives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# UI primitives

Append-only lane for the shared shadcn/Radix interaction layer and migrations
away from handwritten generic control infrastructure. Newest entries first.

## 2026-07-16 — Standardize generic interactions on shadcn/Radix

Dialogs, sheets, popovers, hover cards, menus, command palettes, tabs, toggle
groups, switches, checkboxes, accordions, and toast delivery now compose shared
shadcn/Radix wrappers across the primary chat, reports, documents, research,
entity, strategy, onboarding, settings, and calendar surfaces. Retired focus-trap,
bottom-sheet, keyboard-navigation, portal-positioning, and duplicate toast
infrastructure was removed; native browser form controls, route navigation,
editor decorations, and product-specific virtualized menus remain intentionally
outside the generic primitive layer.
The production-data browser pass also repaired a missing report-menu import and
made both report rails key repeated display names by their runtime artifact ID.

**PR / canonical main commit**: PENDING #559 MAIN SHA / FINAL QA.

**Evidence state**:
- Source: pending CI-gated merge.
- Checks: local TypeScript, 31 final migration-focused tests, 297 broader regression tests, design-system tests, design lint, and the post-repair production build passed; exact PR checks pending.
- Visual proof: local desktop reports/menu and mobile chat captures at `.qa/evidence/2026-07-16-radix-migration/` show zero horizontal overflow and collision-safe controls; exact preview proof pending.
- Preview: pending exact-head Vercel preview.
- Production live: pending merge, deployment, and exact-revision verification.

**Author**: Homen Shum + Codex.
**Touches**: [`../pages/redesign-chat.md`](../pages/redesign-chat.md), [`fast-agent-panel.md`](fast-agent-panel.md).
17 changes: 17 additions & 0 deletions CHANGELOG/pages/redesign-chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
Append-only lane for the public redesign chat, reproducible answer receipts, and their
transition into an authenticated live conversation. Newest entries first.

## 2026-07-16 - Normalize structured answer typography

Structured answer copy now uses the compact product reading scale instead of an oversized
display treatment: 14px at a calm 450 weight with a 1.55 line height. The answer card and
mobile transcript use the existing spacing tokens for a quieter, more consistent rhythm.

This is presentation-only. Browser comparison confirmed identical answer text, visible
content, and accessibility snapshots; runtime provenance, controls, and touch targets are
unchanged. The focused chat guards, typecheck, design-system suite, and production build
pass, and the visual-rubric type-scale score improves from 0 to 1 without regression.

**PR / canonical main commit**: pending CI-gated candidate.

**Evidence state**: local responsive evidence remains outside git under `.qa/evidence/`.

**Author**: Homen Shum + Codex.

## 2026-07-16 - Continue an immutable answer in live chat

Reproducible answer receipts now separate two intents that were previously collapsed:
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,22 @@
"@pdfme/schemas": "^5.5.8",
"@polar-sh/checkout": "^0.2.0",
"@polar-sh/sdk": "^0.47.0",
"@radix-ui/react-accordion": "^1.2.16",
"@radix-ui/react-checkbox": "^1.3.7",
"@radix-ui/react-collapsible": "^1.1.16",
"@radix-ui/react-context-menu": "^2.3.3",
"@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-dropdown-menu": "^2.1.20",
"@radix-ui/react-hover-card": "^1.1.19",
"@radix-ui/react-popover": "^1.1.19",
"@radix-ui/react-progress": "^1.1.12",
"@radix-ui/react-scroll-area": "^1.2.14",
"@radix-ui/react-select": "^2.3.3",
"@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.3",
"@radix-ui/react-tabs": "^1.1.17",
"@radix-ui/react-toggle-group": "^1.1.15",
"@radix-ui/react-tooltip": "^1.2.12",
"@radix-ui/react-use-controllable-state": "^1.2.3",
"@remotion/cli": "4.0.486",
Expand Down
143 changes: 143 additions & 0 deletions src/components/ai-ui/PrimitiveMigration.guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, expect, it } from "vitest";
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";

const ROOT = process.cwd();
const SRC = join(ROOT, "src");

const migratedSurfaces = [
"src/shared/components/DialogOverlay.tsx",
"src/features/chat/components/ChatShareSheet.tsx",
"src/features/chat/components/ThreadActionsSheet.tsx",
"src/features/reports/components/ReportShareSheet.tsx",
"src/features/research/ProofDrawer.tsx",
"src/features/research/components/FeedReaderModal.tsx",
"src/features/research/components/newsletter/EvidenceDrawer.tsx",
"src/features/controlPlane/components/DelegationModal.tsx",
"src/features/onboarding/components/OnboardingWizard.tsx",
"src/features/redesign/components/ShortcutsOverlay.tsx",
"src/features/redesign/components/MobileShell.tsx",
"src/shared/components/MiniEditorPopover.tsx",
"src/features/calendar/components/agenda/AgendaEditorPopover.tsx",
"src/features/calendar/components/agenda/AgendaMiniRow.tsx",
"src/layouts/chrome/HashtagQuickNotePopover.tsx",
"src/features/founder/components/FeedbackWidget.tsx",
"src/features/notebook/components/NotebookBlockFinder.tsx",
"src/features/redesign/components/CommandPalette.tsx",
"src/layouts/chrome/CommandPalette.tsx",
"src/features/redesign/components/MessageActions.tsx",
"src/features/redesign/components/RightInspector.tsx",
"src/shared/ui/SurfacePrimitives.tsx",
"src/features/redesign/surfaces/ReportsSurface.tsx",
"src/layouts/settings/SettingsModal.tsx",
"src/features/research/components/DeepDiveAccordion.tsx",
"src/features/documents/components/documentsHub/planner/PlannerModeToggle.tsx",
"src/features/redesign/surfaces/ChatSurface.tsx",
"src/layouts/CockpitLayout.tsx",
"src/features/product/components/ProductFileAssetPicker.tsx",
"src/features/documents/components/DocumentsPlannerOverlays.tsx",
"src/features/entities/views/EntityPage.tsx",
].map((path) => join(ROOT, path));

const migratedOverlaySurfaces = [
"src/shared/components/DialogOverlay.tsx",
"src/features/chat/components/ChatShareSheet.tsx",
"src/features/chat/components/ThreadActionsSheet.tsx",
"src/features/reports/components/ReportShareSheet.tsx",
"src/features/research/ProofDrawer.tsx",
"src/features/research/components/FeedReaderModal.tsx",
"src/features/research/components/newsletter/EvidenceDrawer.tsx",
"src/features/controlPlane/components/DelegationModal.tsx",
"src/features/onboarding/components/OnboardingWizard.tsx",
"src/features/redesign/components/MobileShell.tsx",
"src/shared/components/MiniEditorPopover.tsx",
"src/features/calendar/components/agenda/AgendaEditorPopover.tsx",
"src/features/calendar/components/agenda/AgendaMiniRow.tsx",
"src/layouts/chrome/HashtagQuickNotePopover.tsx",
"src/features/founder/components/FeedbackWidget.tsx",
"src/features/notebook/components/NotebookBlockFinder.tsx",
"src/features/product/components/ProductFileAssetPicker.tsx",
"src/features/documents/components/DocumentsPlannerOverlays.tsx",
"src/features/entities/views/EntityPage.tsx",
].map((path) => join(ROOT, path));

function sourceFiles(dir = SRC): string[] {
return readdirSync(dir).flatMap((entry) => {
const path = join(dir, entry);
if (statSync(path).isDirectory()) return sourceFiles(path);
return /\.(?:ts|tsx)$/.test(entry) ? [path] : [];
});
}

function rel(path: string) {
return relative(ROOT, path).replaceAll("\\", "/");
}

function implementation(path: string) {
return readFileSync(path, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\/\/.*$/gm, "");
}

describe("shadcn/Radix primitive adoption", () => {
it("keeps migrated dialog semantics on shared primitives", () => {
const offenders = migratedOverlaySurfaces.flatMap((path) => {
const source = implementation(path);
if (!/aria-modal=|role=["']dialog["']/.test(source)) return [];
if (/components\/ai-ui\/(?:command|dialog|popover|sheet)/.test(source)) return [];
return [rel(path)];
});

expect(offenders).toEqual([]);
});

it("does not reintroduce handwritten tab, listbox, or menu roles on migrated surfaces", () => {
const offenders = migratedSurfaces.flatMap((path) =>
/role=["'](?:listbox|menu|menuitem|option|tab|tablist)["']/.test(implementation(path))
? [rel(path)]
: [],
);

expect(offenders).toEqual([]);
});

it("leaves portals only in editor decoration infrastructure", () => {
const portalFiles = sourceFiles()
.filter((path) => !/\.test\.tsx?$/.test(path))
.filter((path) => implementation(path).includes("createPortal("))
.map(rel)
.sort();

expect(portalFiles).toEqual([
"src/features/editor/components/UnifiedEditor/PendingEditHighlights.tsx",
"src/features/editor/components/UnifiedEditor/ProposalInlineDecorations.tsx",
]);
});

it("delegates global keyboard dismissal to primitives on migrated surfaces", () => {
const offenders = migratedOverlaySurfaces.flatMap((path) =>
/addEventListener\(["']keydown["']/.test(implementation(path)) ? [rel(path)] : [],
);

expect(offenders).toEqual([]);
});

it("removes retired handwritten focus and bottom-sheet hooks", () => {
expect(existsSync(join(SRC, "hooks/useFocusTrap.ts"))).toBe(false);
expect(existsSync(join(SRC, "hooks/useKeyboardNavigation.tsx"))).toBe(false);
expect(
existsSync(
join(SRC, "features/agents/components/FastAgentPanel/useBottomSheet.ts"),
),
).toBe(false);
});

it("keeps the report action item primitive imported at runtime", () => {
const source = readFileSync(
join(SRC, "features/redesign/surfaces/ReportsSurface.tsx"),
"utf8",
);
expect(source).toMatch(/DropdownMenuContent,\s+DropdownMenuItem,/);
expect(source).toContain("<DropdownMenuItem");
});
});
56 changes: 56 additions & 0 deletions src/components/ai-ui/ResidualControlsMigration.guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

const read = (path: string) => readFileSync(resolve(process.cwd(), path), "utf8");

describe("residual control primitive migration", () => {
it.each([
"src/features/controlPlane/views/LegalPage.tsx",
"src/features/deepSim/views/DecisionMemoView.tsx",
"src/features/research/views/ReportDetailWorkspace.tsx",
"src/features/research/views/ResearchHub.tsx",
"src/features/strategy/views/ExecutionTraceView.tsx",
"src/features/strategy/views/ProductDirectionMemoView.tsx",
])("uses the shared Radix tabs contract in %s", (path) => {
const source = read(path);
expect(source).toContain("@/components/ai-ui/tabs");
expect(source).toContain("<TabsList");
expect(source).toContain("<TabsTrigger");
expect(source).toContain("<TabsContent");
expect(source).not.toMatch(/role=["'](?:tab|tablist|tabpanel)["']/);
});

it("delegates document filter and density keyboard behavior to ToggleGroup", () => {
const toolbar = read("src/features/documents/components/FiltersToolsBar.tsx");
const dataHook = read("src/features/documents/hooks/useDocumentData.ts");

expect(toolbar).toContain("@/components/ai-ui/toggle-group");
expect(toolbar.match(/<ToggleGroup(?:\s|\n)/g)).toHaveLength(2);
expect(toolbar).not.toMatch(/role=["']tab/);
expect(dataHook).not.toContain("onFilterKeyDown");
expect(dataHook).not.toContain("filterButtonRefs");
});

it("models execution disclosure as a single-select toggle group", () => {
const source = read("src/features/strategy/views/ExecutionTraceView.tsx");
expect(source).toContain("<ToggleGroup");
expect(source).toContain('aria-label="Execution trace disclosure levels"');
expect(source).not.toContain('role="tablist"');
});

it("models composer modes, research lanes, and lenses as toggle groups", () => {
const source = read("src/features/product/components/ProductIntakeComposer.tsx");
expect(source).toContain("@/components/ai-ui/toggle-group");
expect(source.match(/<ToggleGroup(?:\s|\n)/g)).toHaveLength(3);
expect(source).not.toMatch(/role=["'](?:tab|tablist)["']/);
});

it("uses runtime report identity and ToggleGroup in the reports rail", () => {
const source = read("src/features/redesign/surfaces/HomeV2PrototypeSurface.tsx");
expect(source).toContain("@/components/ai-ui/toggle-group");
expect(source).toContain("id: report.id");
expect(source.match(/item\.id \?\? item\.name/g)).toHaveLength(2);
expect(source).not.toContain('role="tablist" aria-label="Filter reports by status"');
});
});
57 changes: 57 additions & 0 deletions src/components/ai-ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client"

import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"

import { cn } from "@/lib/utils"

const Accordion = AccordionPrimitive.Root

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
28 changes: 28 additions & 0 deletions src/components/ai-ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client"

import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded border border-edge shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<Check className="h-3.5 w-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
Loading
Loading