553 lines
22 KiB
Markdown
553 lines
22 KiB
Markdown
---
|
|
name: originmain-frontend
|
|
description: "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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```typescript
|
|
// 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).
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```tsx
|
|
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
|
|
|
|
```bash
|
|
pnpm add @pierre/diffs
|
|
```
|
|
|
|
```tsx
|
|
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.
|
|
|
|
```tsx
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```tsx
|
|
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
|
|
|
|
```bash
|
|
pnpm add @pierre/trees
|
|
```
|
|
|
|
```tsx
|
|
import { FileTree } from '@pierre/trees';
|
|
import type { FileTreeNode } from '@pierre/trees';
|
|
```
|
|
|
|
### Component Usage
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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)*
|