Files
wursor/Originmain-Frontend-Design-SKILL.md
T
2026-04-23 04:20:14 +01:00

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 style prop for colours. Use tokens.* from @fluentui/react-components inside makeStyles.
  • Never import from @fluentui/react-components/unstable. Unstable APIs are not covered by semver.
  • Use tokens.colorBrandBackground not #0F52BA for brand colour references in Fluent 2 surfaces.
  • Heading hierarchy matters for accessibility. Fluent 2's Text component does not enforce this — you must set the correct as prop (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 theme falls 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 lineNumber is 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.
  • gitStatus prop must be boolean true, not an object or function. Git status is read from the gitStatus field on each FileTreeNode.
  • 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-label or alt text (use aria-hidden="true" for decorative icons)
  • @pierre/trees built-in ARIA (tree, treeitem, aria-level, aria-posinset, aria-setsize) is not suppressed
  • @pierre/diffs built-in keyboard navigation for the diff view is not suppressed
  • No tabIndex overrides 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)