Skip to content
Open
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
49 changes: 49 additions & 0 deletions docs/frontend-ui-audit-2026-07-22/SharedComponentOwnershipBatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Frontend UI Audit — Shared Component Ownership Batch

**Files:** `src/components/Placeholder.tsx`, `src/components/SearchSortBar.tsx`, `src/components/FolderHeaderRow.tsx`, `src/components/ListPanel/ListPanelScrollArea.tsx`, `src/components/ListPanel/ListPanelTabPillRow.tsx`, `src/components/ListPanel/MenuPanel.tsx`
**Date:** 2026-07-22
**Auditor:** ORGII agent session

## D1 — Raw HTML vs Design System

| Line | Element | Verdict | Reason | Suggested change |
| ----------------------------- | ----------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `FolderHeaderRow.tsx:32` | Collapsible full-row `<button>` | keep with reason | The row is a compound chevron/name/branch/badge layout and uses the workstation row token contract; wrapping it in the generic Button would add incompatible button chrome and sizing. | — |
| `ListPanel/MenuPanel.tsx:93` | Full-width navigation `<button>` | keep with reason | This is a list-navigation row driven by `getListItemClasses`; generic Button does not model selected navigation-row layout. | — |
| `Placeholder.tsx:145,174` | Design-system `Button` | keep with reason | Action rendering already uses the shared Button component. | — |
| `SearchSortBar.tsx:68,96,114` | Design-system `Button`, `Input`, `Select` | keep with reason | Existing design-system controls cover all interactive behavior. | — |

## D2 — Arbitrary Tailwind Value vs Token

| Line | Value | Verdict | Reason | Suggested change |
| ---- | ------------------------------------------------------------------------------------------ | ---------------- | ----------------------------------------------- | ---------------- |
| — | No raw CSS-variable, hex, RGB, HSL, or arbitrary color classes in the relocated primitives | keep with reason | Color usage is through project Tailwind tokens. | — |

## D3 — Hardcoded Sizes / Colors

| Line | Value | Verdict | Reason | Suggested change |
| ------------------------------ | ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| `FolderHeaderRow.tsx:46` | `h-[16px] min-w-[16px]` | fix | Exact spacing-scale equivalents exist. | Replaced with `h-4 min-w-4`. |
| `FolderHeaderRow.tsx:34,36,41` | 14px/11px icon sizes | keep with reason | These are icon optical sizes aligned with existing workstation tokens and fall under micro-adjustment sizing. | — |
| `SearchSortBar.tsx:47` | Default `w-[180px]` | keep with reason | This is an overridable API default for a select column and has no exact Tailwind spacing-scale equivalent. | — |
| `SearchSortBar.tsx:131` | `text-[13px]` | keep with reason | Matches the established workstation compact typography scale used by the shared token set. | — |

## D4 — Accessibility

| Line | Element | Verdict | Reason | Suggested change |
| ---------------------------- | ----------------------- | ---------------- | ------------------------------------------------------------------------------------ | ---------------- |
| `FolderHeaderRow.tsx:32` | Toggle button | keep with reason | Semantic button has visible `name` text as its accessible name. | — |
| `ListPanel/MenuPanel.tsx:93` | Navigation button | keep with reason | Semantic button has visible item label and keyboard behavior. | — |
| `SearchSortBar.tsx:68` | Icon-only filter Button | keep with reason | `title` supplies the translated accessible label through the shared Button contract. | — |

## D5 — Visual Patterns Observed

- Collapsible folder/worktree header is centralized in `FolderHeaderRow`; no new duplicate implementation was introduced.
- List-panel tab header and scroll area are now canonical `src/components/ListPanel` primitives; old module paths only re-export them.
- Empty/loading/error presentation is centralized in `Placeholder`; old module paths only re-export it.

## Summary

- 1 fix applied
- 9 findings kept with documented reason
- 0 new abstraction candidates
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"test:watch": "vitest",
"check:unused-exports": "ts-unused-exports tsconfig.json --excludePathsFromReport='src/index.tsx;src/app/;src/i18n/;.test.ts;.test.tsx;/types.ts;/index.ts;src/scaffold/;src/types/ambient/'",
"check:unused-exports:all": "ts-unused-exports tsconfig.json",
"check:component-boundaries": "node scripts/quality/check-component-boundaries.mjs",
"check:e2e-oauth-guards": "node scripts/quality/check-e2e-oauth-guards.mjs",
"check:i18n:cloud": "node scripts/quality/check-missing-i18n-keys.mjs --namespace navigation --prefix cloud && node scripts/quality/check-missing-i18n-keys.mjs --namespace navigation --prefix collaboration.forkImported"
},
Expand Down
87 changes: 87 additions & 0 deletions scripts/quality/check-component-boundaries.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { readFileSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";

const repoRoot = join(import.meta.dirname, "..", "..");
const componentsRoot = join(repoRoot, "src", "components");
const forbiddenTopLevelLayers = new Set([
"engines",
"features",
"modules",
"scaffold",
]);

// Existing mixed-ownership components are recorded explicitly so this guard
// prevents regression without pretending the remaining migration is complete.
// Remove an entry as soon as that component is moved or inverted.
const allowedLegacyViolations = new Set([
"src/components/ComposerBar/index.tsx",
"src/components/ErrorBoundary/index.tsx",
"src/components/IssueHoverCard/index.tsx",
"src/components/MarkDown/MarkDownImpl.tsx",
"src/components/MarkDown/MermaidBlock.tsx",
"src/components/ModelTable/ModelVariantInlineCard.tsx",
"src/components/QuitConfirmationModal/index.tsx",
"src/components/ResizableSplitPanel/index.tsx",
"src/components/SessionHoverCard/SessionHoverCardContent.tsx",
"src/components/SessionHoverCard/useSessionTurnOverview.ts",
"src/components/ShellReplayOutput/index.tsx",
"src/components/System/RepoLoader.tsx",
"src/components/TerminalInteractive/terminalSetup.ts",
"src/components/TerminalReadOnly/index.tsx",
"src/components/TerminalReadOnly/outputBuffer.ts",
"src/components/WorkItemHoverCard/index.tsx",
]);

function listSourceFiles(dir) {
const files = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) files.push(...listSourceFiles(fullPath));
else if (/\.(ts|tsx)$/.test(entry.name)) files.push(fullPath);
}
return files;
}

function importedTopLevelLayers(source) {
const layers = new Set();
const importPattern = /\b(?:from\s+|import\s*\(\s*)["']@src\/([^/"']+)/g;
for (const match of source.matchAll(importPattern)) {
if (forbiddenTopLevelLayers.has(match[1])) layers.add(match[1]);
}
return layers;
}

const currentViolations = new Map();
for (const file of listSourceFiles(componentsRoot)) {
if (/\.(?:test|spec)\.(?:ts|tsx)$/.test(file)) continue;
const layers = importedTopLevelLayers(readFileSync(file, "utf8"));
if (layers.size === 0) continue;
currentViolations.set(relative(repoRoot, file), [...layers].sort());
}

const unexpected = [...currentViolations].filter(
([file]) => !allowedLegacyViolations.has(file)
);
const staleAllowlist = [...allowedLegacyViolations].filter(
(file) => !currentViolations.has(file)
);

if (unexpected.length > 0 || staleAllowlist.length > 0) {
if (unexpected.length > 0) {
console.error("New src/components layer violations found:");
for (const [file, layers] of unexpected) {
console.error(`- ${file}: imports ${layers.join(", ")}`);
}
}
if (staleAllowlist.length > 0) {
console.error(
"Resolved component violations still present in the allowlist:"
);
for (const file of staleAllowlist) console.error(`- ${file}`);
}
process.exit(1);
}

console.log(
`Component boundary check passed (${currentViolations.size} tracked legacy violations, no new violations)`
);
2 changes: 1 addition & 1 deletion src/components/FileTreeContent/FileTreeRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import React from "react";

import { FolderHeaderRow } from "@src/components/FolderHeaderRow";
import type { TreePanelNode } from "@src/components/TreePanelSidebar/types";
import { TREE_INDENT_PX, TREE_PADDING_X } from "@src/components/TreeRow";
import type {
FlattenedTreeNode,
StickyScrollNode,
} from "@src/components/VirtualizedStickyTree";
import { getStatusBgColor, getStatusColorForFile } from "@src/config/gitStatus";
import { FolderHeaderRow } from "@src/modules/WorkStation/shared/FolderHeaderRow";

import { NewItemInput } from "./NewItemInput";
import { TreeNode } from "./TreeNode";
Expand Down
4 changes: 2 additions & 2 deletions src/components/FileTreeContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import { useTranslation } from "react-i18next";
import type { VirtuosoHandle } from "react-virtuoso";

import { useActionSystem } from "@src/ActionSystem";
import { FolderHeaderRow } from "@src/components/FolderHeaderRow";
import Input from "@src/components/Input";
import { Placeholder } from "@src/components/Placeholder";
import type { TreePanelNode } from "@src/components/TreePanelSidebar/types";
import { TREE_ROW_HEIGHT } from "@src/components/TreeRow";
import type {
Expand All @@ -57,8 +59,6 @@ import {
updateFileTreeMemoryEntry,
} from "@src/hooks/perf/runtimeMemoryStats";
import { useElementDimensions } from "@src/hooks/ui/layout/useElementDimensions";
import { FolderHeaderRow } from "@src/modules/WorkStation/shared/FolderHeaderRow";
import { Placeholder } from "@src/modules/shared/layouts/blocks";
import { fileTreeSelectedPathAtom } from "@src/store/ui/fileTreeSelectionAtom";

import { FileExplorerContextMenu } from "./FileExplorerMenu";
Expand Down
56 changes: 56 additions & 0 deletions src/components/FolderHeaderRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ChevronDown, ChevronRight, GitBranch } from "lucide-react";
import React, { memo } from "react";

import { FOLDER_HEADER } from "@src/config/workstation/tokens";

export interface FolderHeaderRowProps {
name: string;
expanded: boolean;
onToggle: () => void;
branchName?: string;
badgeCount?: number;
className?: string;
onContextMenu?: (event: React.MouseEvent) => void;
actions?: React.ReactNode;
}

export const FolderHeaderRow: React.FC<FolderHeaderRowProps> = memo(
({
name,
expanded,
onToggle,
branchName,
badgeCount,
className,
onContextMenu,
actions,
}) => (
<div
className={`${FOLDER_HEADER.row}${className ? ` ${className}` : ""}`}
onContextMenu={onContextMenu}
>
<button type="button" className={FOLDER_HEADER.button} onClick={onToggle}>
{expanded ? (
<ChevronDown size={14} className="flex-shrink-0 text-text-3" />
) : (
<ChevronRight size={14} className="flex-shrink-0 text-text-3" />
)}
<span className={FOLDER_HEADER.name}>{name}</span>
{branchName && (
<>
<GitBranch size={11} className="flex-shrink-0 text-text-3" />
<span className={FOLDER_HEADER.branch}>{branchName}</span>
</>
)}
{badgeCount != null && badgeCount > 0 && (
<span className="bg-accent-7 ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium text-white">
{badgeCount}
</span>
)}
</button>
{actions && <div className={FOLDER_HEADER.actions}>{actions}</div>}
</div>
)
);

FolderHeaderRow.displayName = "FolderHeaderRow";
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
*/
import type { MutableRefObject } from "react";

import { reorderActiveRef } from "@src/engines/ChatPanel/InputArea/components/QueuedMessages";
import {
isInternalFileTreeDragActive,
isWorkstationTabDragActive,
messageQueueReorderActiveRef,
} from "@src/shared/dnd/dragSideChannel";
import { getNativeFrameScale } from "@src/util/platform/tauri/nativeFrame";

Expand All @@ -22,7 +22,7 @@ export function isInternalDrag(
const dragEvent = event as DragEvent;

// Queue reorder drag is always internal
if (reorderActiveRef.current) {
if (messageQueueReorderActiveRef.current) {
return true;
}

Expand Down Expand Up @@ -247,7 +247,7 @@ export function createPreventDefaults(
): (e: Event) => void {
return (event: Event) => {
// Queue reorder drag — never intercept
if (reorderActiveRef.current) return;
if (messageQueueReorderActiveRef.current) return;

const dragEvent = event as DragEvent;
const rawTypes = dragEvent.dataTransfer?.types;
Expand Down
23 changes: 23 additions & 0 deletions src/components/ListPanel/ListPanelScrollArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";

import { LIST_PANEL_SCROLL_AREA } from "./tokens";

export interface ListPanelScrollAreaProps {
children: React.ReactNode;
listPaddingTop?: "default" | "none";
className?: string;
}

const ListPanelScrollArea: React.FC<ListPanelScrollAreaProps> = ({
children,
listPaddingTop = "default",
className = "",
}) => (
<div
className={`min-h-0 flex-1 overflow-y-auto px-2 scrollbar-hide ${listPaddingTop === "none" ? LIST_PANEL_SCROLL_AREA.paddingTopNone : LIST_PANEL_SCROLL_AREA.paddingTopDefault} ${className}`.trim()}
>
{children}
</div>
);

export default ListPanelScrollArea;
19 changes: 19 additions & 0 deletions src/components/ListPanel/ListPanelTabPillRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";

export interface ListPanelTabPillRowProps {
children: React.ReactNode;
className?: string;
}

const ListPanelTabPillRow: React.FC<ListPanelTabPillRowProps> = ({
children,
className = "",
}) => (
<div
className={`flex h-10 flex-shrink-0 items-center gap-2 bg-bg-2 px-3 ${className}`.trim()}
>
{children}
</div>
);

export default ListPanelTabPillRow;
6 changes: 2 additions & 4 deletions src/components/ListPanel/MenuPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@
import type { LucideIcon } from "lucide-react";
import React from "react";

import ListPanelScrollArea from "@src/components/ListPanel/ListPanelScrollArea";
import ListPanelTabPillRow from "@src/components/ListPanel/ListPanelTabPillRow";
import TabPill from "@src/components/TabPill";
import {
ListPanelScrollArea,
ListPanelTabPillRow,
} from "@src/modules/shared/layouts/blocks";

import { getListIconClasses, getListItemClasses } from "./tokens";

Expand Down
Loading
Loading