22 KiB
name, description
| name | description |
|---|---|
| originmain-frontend | Use this skill for all frontend work on Originmain. It synthesises three design systems into a coherent implementation guide: Fluent 2 (@fluentui/react-components v9) for all application chrome and navigation UI, @pierre/diffs for code-level diff rendering in the export panel and Agent Bridge view, and @pierre/trees for the codebase file browser. The skill defines which library owns which surface, how to theme @pierre/* libraries using Fluent 2 tokens, critical constraints for each library, and component-level implementation patterns. Use when creating, editing, or reviewing any Originmain UI component. |
Originmain Frontend Design Skill
This skill governs all frontend work on Originmain. It is the definitive reference for which library owns which surface, how the three systems interoperate, and what constraints must never be violated.
The Three-Library Architecture
Originmain's frontend is built on three libraries that cover distinct responsibilities. Understanding their boundaries is the most important thing in this skill.
Library 1: Fluent 2 — Application Chrome & Navigation
Package: @fluentui/react-components v9
License: MIT
Governs: Everything in the application shell — toolbar, menus, panels, dialogs, inspector, inputs, feedback components, and the artboard navigator sidebar.
Fluent 2 is not a styling choice. It is the design language contract. Every Originmain-owned UI surface uses Fluent 2 components and Fluent 2 tokens. No exceptions. This ensures that the tool's own interface embodies the same systematic, accessible design language it helps teams produce.
Library 2: @pierre/diffs — Code-Level Diff Rendering
Package: @pierre/diffs
License: Apache 2.0
Governs: The export panel (where a developer previews what code changes will be applied to their codebase) and the Agent Bridge communication view (where the coding agent's status is shown alongside design intent).
@pierre/diffs does NOT replace the custom AST differ in packages/diff-engine. The AST differ computes what changed. @pierre/diffs renders it. These are separate concerns.
Library 3: @pierre/trees — Codebase File Browser
Package: @pierre/trees
License: Open source (The Pierre Computer Co.)
Governs: The codebase file browser panel — the panel that shows the connected application's repository file tree with git status indicators, search, and virtualization.
@pierre/trees does NOT replace Fluent 2's Tree component. Fluent 2 Tree governs the artboard navigator (workspace folders, artboard groups, project hierarchy). @pierre/trees governs the codebase file browser only.
Surface Ownership Map
This table is the definitive reference. When building any component, check which library owns the surface before writing a line of code.
| Surface | Library | Why |
|---|---|---|
| Root layout | Fluent 2 FluentProvider |
Required wrapper for all Fluent 2 context |
| Canvas toolbar | Fluent 2 Toolbar, ToolbarButton |
Chrome component — in-ecosystem |
| Top navigation | Fluent 2 Tab, TabList, Menu |
Chrome component |
| Artboard navigator (left panel workspace tree) | Fluent 2 Tree, TreeItem |
Navigates product metadata; small node count; must match chrome |
| Codebase file browser (repository tree) | @pierre/trees FileTree |
Navigates code; thousands of nodes; needs git status + virtualization |
| Inspector panel | Fluent 2 Card, Accordion, DataGrid |
Chrome component |
| Intent Diff inspector | Fluent 2 Card, Accordion, Badge |
Structured semantic data, not code |
| Export panel diff view | @pierre/diffs MultiFileDiff |
Code-level rendering with Shiki; split or stacked mode |
| Agent Bridge diff view | @pierre/diffs FileDiff |
Stacked mode; lines of dialogue above diff |
| Completion Zone UI | Fluent 2 Popover, Input, Button |
In-canvas control — chrome |
| Dialogs | Fluent 2 Dialog, Drawer |
Chrome |
| Status & feedback | Fluent 2 Toast, MessageBar, Spinner |
Chrome |
| Presence / multiplayer | Fluent 2 Avatar, PresenceBadge |
Chrome |
| Artboard status badges | Fluent 2 Badge, PresenceBadge |
Chrome |
| Form inputs | Fluent 2 Input, Combobox, SearchBox |
Chrome |
Fluent 2 — Rules and Patterns
Setup
// packages/app/src/app/layout.tsx
import { FluentProvider } from '@fluentui/react-components';
import { originmainTheme } from '@originmain/ui';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<FluentProvider theme={originmainTheme}>
{children}
</FluentProvider>
</body>
</html>
);
}
Brand Theme
// packages/ui/src/themes/originmain-theme.ts
import { createLightTheme, createDarkTheme, type BrandVariants } from '@fluentui/react-components';
const originmainBrand: BrandVariants = {
10: '#f0f4fc',
20: '#d8e4f7',
30: '#b3c9ef',
40: '#8daee7',
50: '#6793df',
60: '#4178d7',
70: '#2563ce',
80: '#1655c0', // primary brand colour
90: '#0f52ba', // anchor — the Originmain blue
100: '#0d4aa8',
110: '#0b4096',
120: '#083680',
130: '#062c6a',
140: '#042254',
150: '#02183e',
160: '#010d28',
};
export const originmainTheme = createLightTheme(originmainBrand);
export const originmainDarkTheme = createDarkTheme(originmainBrand);
Styling — Always Use makeStyles
Never use inline styles for Fluent 2 components except when projecting tokens into third-party libraries (see Theming Bridge below). Use makeStyles from @fluentui/react-components (Griffel CSS-in-JS).
import { makeStyles, tokens } from '@fluentui/react-components';
const useStyles = makeStyles({
panel: {
backgroundColor: tokens.colorNeutralBackground2,
borderRight: `1px solid ${tokens.colorNeutralStroke2}`,
padding: tokens.spacingHorizontalM,
},
heading: {
fontSize: tokens.fontSizeBase400,
fontWeight: tokens.fontWeightSemibold,
color: tokens.colorNeutralForeground1,
},
});
Artboard Navigator (Fluent 2 Tree)
import { Tree, TreeItem, TreeItemLayout } from '@fluentui/react-components';
interface NavigatorNode {
id: string;
label: string;
type: 'workspace' | 'folder' | 'group' | 'artboard';
originType?: 'linear' | 'git' | 'feedback' | 'manual';
children?: NavigatorNode[];
}
function renderNode(node: NavigatorNode): React.ReactNode {
if (node.children?.length) {
return (
<TreeItem key={node.id} itemType="branch">
<TreeItemLayout iconBefore={<FolderIcon />}>{node.label}</TreeItemLayout>
<Tree>{node.children.map(renderNode)}</Tree>
</TreeItem>
);
}
return (
<TreeItem key={node.id} itemType="leaf">
<TreeItemLayout iconBefore={<ArtboardIcon />}>{node.label}</TreeItemLayout>
</TreeItem>
);
}
Critical Fluent 2 Constraints
- FluentProvider is required. All Fluent 2 hooks, tokens, and components silently fail or throw outside it.
- Never use
styleprop for colours. Usetokens.*from@fluentui/react-componentsinsidemakeStyles. - Never import from
@fluentui/react-components/unstable. Unstable APIs are not covered by semver. - Use
tokens.colorBrandBackgroundnot#0F52BAfor brand colour references in Fluent 2 surfaces. - Heading hierarchy matters for accessibility. Fluent 2's
Textcomponent does not enforce this — you must set the correctasprop (h1,h2, etc.) manually.
@pierre/diffs — Rules and Patterns
Setup
pnpm add @pierre/diffs
import { MultiFileDiff, FileDiff, PatchDiff } from '@pierre/diffs/react';
Component Choices
| Situation | Component | Notes |
|---|---|---|
| Multiple files changed | MultiFileDiff |
Export panel default |
| Single file comparison | FileDiff |
Pass before and after file content strings |
| Raw git patch string | PatchDiff |
Agent Bridge git-level view |
Theming Bridge (Fluent 2 → @pierre/diffs)
@pierre/diffs is themed via CSS custom properties on its container element. Map Fluent 2 tokens to the properties @pierre/diffs exposes. Always use the style prop on the wrapper div — never hardcoded values. Note: makeStyles does not support arbitrary CSS custom properties, so this is the correct exception where inline style is used.
// packages/app/src/components/diff/CodeDiffPanel.tsx
import { MultiFileDiff } from '@pierre/diffs/react';
const diffThemeBridge = {
'--diff-background': 'var(--colorNeutralBackground1)',
'--diff-gutter-background': 'var(--colorNeutralBackground2)',
'--diff-added-line': 'var(--colorPaletteGreenBackground1)',
'--diff-removed-line': 'var(--colorPaletteRedBackground1)',
'--diff-added-gutter': 'var(--colorPaletteGreenBackground2)',
'--diff-removed-gutter': 'var(--colorPaletteRedBackground2)',
'--diff-border-color': 'var(--colorNeutralStroke2)',
'--diff-text-color': 'var(--colorNeutralForeground1)',
'--diff-annotation-color': 'var(--colorNeutralForeground3)',
'--diff-filename-color': 'var(--colorNeutralForeground2)',
'--diff-hunk-background': 'var(--colorNeutralBackground3)',
} as React.CSSProperties;
// Shiki theme selection based on Fluent 2 theme mode
function getShikiTheme(isDark: boolean): string {
return isDark ? 'github-dark-dimmed' : 'github-light';
}
function CodeDiffPanel({ diff, isDark }: { diff: IntentDiff; isDark: boolean }) {
return (
<div style={diffThemeBridge}>
<MultiFileDiff
diffs={mapIntentDiffToFileDiffs(diff)}
mode="split"
theme={getShikiTheme(isDark)}
annotations={buildDiffAnnotations(diff)}
/>
</div>
);
}
Annotation Pattern
The annotation system is @pierre/diffs' most valuable feature for Originmain. Anchor the humanSummary of each ComponentChange to the specific line it modifies. This creates the connected "design intent → code line" view that is Originmain's signature export experience.
// packages/app/src/components/diff/diff-mappers.ts
interface DiffAnnotation {
lineNumber: number;
content: string;
type: 'info' | 'warning' | 'success';
}
export function buildDiffAnnotations(diff: IntentDiff): DiffAnnotation[] {
return diff.changes
.filter(change => change.lineNumber !== undefined)
.map(change => ({
lineNumber: change.lineNumber!,
content: change.humanSummary,
type: change.changeType === 'removal' ? 'warning' : 'info',
}));
}
Mode Switching (Split vs Stacked)
The export panel uses split mode (side-by-side) at widths >= 640px and stacked (unified) mode below. The Agent Bridge communication view always uses stacked mode to maximise vertical information density alongside the chat-style dialogue.
const [mode, setMode] = React.useState<'split' | 'stacked'>('split');
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver(([entry]) => {
setMode(entry.contentRect.width >= 640 ? 'split' : 'stacked');
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
Critical @pierre/diffs Constraints
- Never use @pierre/diffs for the Intent Diff inspector sidebar. That surface shows semantic component-level changes (Fluent 2 Card/Accordion). @pierre/diffs is for code-level line diffs only.
- Always provide a Shiki theme. Omitting
themefalls back to an unstyled view that clashes with the Fluent 2 chrome. - Annotations only anchor to lines that exist in the rendered diff. Validate that
lineNumberis within range before creating an annotation — out-of-range line numbers are silently ignored. - Shadow DOM is used internally. Do not attempt to style @pierre/diffs internals with global CSS selectors. The CSS custom properties API is the only supported theming interface.
- Test both light and dark mode. The Shiki theme must switch when the user toggles Fluent 2's dark mode.
@pierre/trees — Rules and Patterns
Setup
pnpm add @pierre/trees
import { FileTree } from '@pierre/trees';
import type { FileTreeNode } from '@pierre/trees';
Component Usage
// packages/app/src/components/codebase/CodebaseFileTree.tsx
interface CodebaseFileTreeProps {
nodes: FileTreeNode[];
onFileSelect: (path: string) => void;
}
export function CodebaseFileTree({ nodes, onFileSelect }: CodebaseFileTreeProps) {
return (
<div style={{ ...treesThemeBridge, height: '100%' }}>
<FileTree
nodes={nodes}
gitStatus={true}
flattenEmptyDirectories={true}
fileTreeSearchMode="filter"
onSelect={(node) => {
if (node.type === 'file') onFileSelect(node.path);
}}
/>
</div>
);
}
FileTreeNode Shape
interface FileTreeNode {
id: string;
name: string;
path: string;
type: 'file' | 'directory';
gitStatus?: 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' | 'ignored';
children?: FileTreeNode[];
}
Build the FileTreeNode[] array from the component tree extracted by the renderer (packages/renderer/src/fiber-injector.ts). The renderer provides file paths via sourcemaps — use those to construct the tree structure.
Theming Bridge (Fluent 2 → @pierre/trees)
const treesThemeBridge = {
'--tree-background': 'var(--colorNeutralBackground2)',
'--tree-item-hover': 'var(--colorNeutralBackground3)',
'--tree-item-selected': 'var(--colorBrandBackground2)',
'--tree-item-selected-text': 'var(--colorBrandForeground2)',
'--tree-text-color': 'var(--colorNeutralForeground1)',
'--tree-secondary-text': 'var(--colorNeutralForeground3)',
'--tree-border-color': 'var(--colorNeutralStroke2)',
'--tree-git-added': 'var(--colorPaletteGreenForeground1)',
'--tree-git-modified': 'var(--colorPaletteYellowForeground1)',
'--tree-git-deleted': 'var(--colorPaletteRedForeground1)',
'--tree-git-renamed': 'var(--colorPaletteBlueForeground2)',
'--tree-git-untracked': 'var(--colorNeutralForeground3)',
'--tree-git-ignored': 'var(--colorNeutralForegroundDisabled)',
} as React.CSSProperties;
fileTreeSearchMode Options
| Mode | Behaviour | When to use |
|---|---|---|
"filter" |
Non-matching nodes are hidden | Default — cleaner search experience |
"highlight" |
Non-matching nodes are dimmed | When user needs spatial context |
"expand" |
Matching nodes' parents expand automatically | When browsing deep paths |
Critical @pierre/trees Constraints
- @pierre/trees is for the codebase file browser only. Never use it in the artboard navigator. They serve different data with different requirements.
- Virtualization is automatic. Do not set a fixed height and then manually manage virtualization. Let @pierre/trees own its scroll container.
gitStatusprop must be booleantrue, not an object or function. Git status is read from thegitStatusfield on eachFileTreeNode.- The container div must have a defined height. Set
height: '100%'and ensure the parent has a constrained height. A tree inside a flex container with no height constraint will render zero rows. - Do not render @pierre/trees outside a Fluent 2 FluentProvider. The theme bridge uses CSS variables emitted by Fluent 2's token system — those variables are undefined outside the provider.
- Search is client-side. Do not fire API calls on search input. All filtering happens in memory on the node array passed in.
Token Quick Reference
The most commonly needed Fluent 2 design tokens for Originmain surfaces:
// Backgrounds
tokens.colorNeutralBackground1 // primary surface (white in light / dark in dark)
tokens.colorNeutralBackground2 // secondary surface (panels, sidebars)
tokens.colorNeutralBackground3 // tertiary surface (hover states, tinted fills)
tokens.colorBrandBackground // brand blue fill
tokens.colorBrandBackground2 // selected item fill (lighter blue)
// Foregrounds
tokens.colorNeutralForeground1 // primary text
tokens.colorNeutralForeground2 // secondary text
tokens.colorNeutralForeground3 // tertiary / placeholder text
tokens.colorNeutralForegroundDisabled // disabled text
tokens.colorBrandForeground1 // brand blue text on neutral backgrounds
tokens.colorBrandForeground2 // brand blue text on brand backgrounds
// Strokes
tokens.colorNeutralStroke1 // default border
tokens.colorNeutralStroke2 // subtle border
tokens.colorBrandStroke1 // brand border / focus ring
// Status palette (use for git status, diff indicators, validation states)
tokens.colorPaletteGreenForeground1 // added / success
tokens.colorPaletteGreenBackground1 // added line highlight
tokens.colorPaletteYellowForeground1 // modified / warning
tokens.colorPaletteYellowBackground1 // modified line highlight
tokens.colorPaletteRedForeground1 // deleted / error
tokens.colorPaletteRedBackground1 // deleted line highlight
tokens.colorPaletteBlueForeground2 // renamed / info
// Spacing
tokens.spacingHorizontalXS // 4px
tokens.spacingHorizontalS // 8px
tokens.spacingHorizontalM // 12px
tokens.spacingHorizontalL // 16px
tokens.spacingHorizontalXL // 20px
tokens.spacingHorizontalXXL // 24px
tokens.spacingVerticalXS // 4px
tokens.spacingVerticalS // 8px
tokens.spacingVerticalM // 12px
tokens.spacingVerticalL // 16px
// Typography
tokens.fontSizeBase200 // 12px
tokens.fontSizeBase300 // 14px
tokens.fontSizeBase400 // 16px
tokens.fontSizeBase500 // 20px
tokens.fontWeightRegular // 400
tokens.fontWeightMedium // 500
tokens.fontWeightSemibold // 600
tokens.fontWeightBold // 700
// Motion
tokens.durationFast // 100ms
tokens.durationNormal // 200ms
tokens.durationSlow // 300ms
tokens.curveEasyEase // cubic-bezier(0.33, 0, 0.67, 1)
tokens.curveDecelerateMax // cubic-bezier(0, 0, 0, 1) — for panels entering
tokens.curveAccelerateMax // cubic-bezier(1, 0, 1, 1) — for panels leaving
Common Patterns
Dark Mode Toggle
import { FluentProvider } from '@fluentui/react-components';
import { originmainTheme, originmainDarkTheme } from '@originmain/ui';
function ThemedApp({ isDark, children }: { isDark: boolean; children: React.ReactNode }) {
return (
<FluentProvider theme={isDark ? originmainDarkTheme : originmainTheme}>
{children}
</FluentProvider>
);
}
Pass isDark into CodeDiffPanel and CodebaseFileTree so they switch their Shiki theme and CSS variable bridge values accordingly.
Design System Violation Badge
When a visual edit sets a value not found in the team's Design Language File:
import { Badge } from '@fluentui/react-components';
<Badge appearance="filled" color="warning" size="small">
Not in design system
</Badge>
Artboard Status Badge (Agent Bridge Sync)
Coding agent implementation status shown on artboard cards:
import { PresenceBadge } from '@fluentui/react-components';
const statusMap: Record<IntentDiff['status'], React.ReactNode> = {
implemented: <PresenceBadge status="available" />,
blocked: <PresenceBadge status="busy" />,
needs_clarification: <PresenceBadge status="away" />,
exported: <PresenceBadge status="unknown" />,
acknowledged: <PresenceBadge status="unknown" />,
draft: null,
rejected: <PresenceBadge status="offline" />,
};
Empty States
Every panel that can be empty must show a Fluent 2-styled empty state — never a blank white box:
import { Body1, Caption1, makeStyles, tokens } from '@fluentui/react-components';
const useEmptyStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: tokens.spacingVerticalM,
padding: tokens.spacingHorizontalXXL,
color: tokens.colorNeutralForeground3,
textAlign: 'center',
height: '100%',
},
});
function EmptyState({ title, description }: { title: string; description: string }) {
const styles = useEmptyStyles();
return (
<div className={styles.container}>
<Body1 weight="semibold">{title}</Body1>
<Caption1>{description}</Caption1>
</div>
);
}
Accessibility Checklist
Every component must satisfy this checklist before merge:
- Keyboard navigation works without a mouse (Tab, Arrow keys, Enter, Escape)
- ARIA roles are correct — do not override Fluent 2's built-in roles unnecessarily
- Focus is managed correctly when dialogs open and close (focus trap in
Dialog, focus restore on close) - Colour alone is never the only differentiator — use icons or labels alongside colour
- All interactive elements meet WCAG 2.1 AA contrast ratio (4.5:1 for text, 3:1 for UI components)
- All images and icons have
aria-labeloralttext (usearia-hidden="true"for decorative icons) @pierre/treesbuilt-in ARIA (tree,treeitem,aria-level,aria-posinset,aria-setsize) is not suppressed@pierre/diffsbuilt-in keyboard navigation for the diff view is not suppressed- No
tabIndexoverrides without explicit justification in the PR description
Originmain Frontend Design Skill — v1.0 — April 2026
Synthesises: Fluent 2 (@fluentui/react-components v9) · @pierre/diffs (diffs.com) · @pierre/trees (trees.software)