---
name: field-notebook-ui
description: >
  Use when the user asks to build a UI, interactive explainer, dashboard, visualizer,
  diagram, reference tool, or any artifact that needs a consistent visual design system
  in the "engineer's field-notebook" aesthetic. Trigger on: "build a UI for", "create an
  interactive", "make a visualizer", "generate an artifact", "design a component",
  "create a dashboard", "make an explainer", or any request to produce a React/HTML
  artifact with content to display. Also trigger when the user says "make it look like
  the field-notebook style" or references the warm stone-paper aesthetic.
when_to_use: >
  User says: "build a UI for X", "create an interactive explainer", "make a visualizer",
  "generate a React artifact", "design a dashboard", "create a reference tool",
  "make it look like the field-notebook style", "use the warm stone-paper aesthetic",
  or asks for any interactive component that should share a coherent design language
  without re-specifying the token system each time.
argument-hint: '[what to build — e.g. "k8s glossary", "helm explainer", "auth flow diagram"]'
arguments: []
disable-model-invocation: false
user-invocable: true
allowed-tools:
  - Read
  - Write
  - Bash
disallowed-tools: []
model: claude-sonnet-4-6
effort: medium
context: ''
agent: ''
hooks: {}
paths: []
shell: bash
metadata:
  capability: creative-ui
  tags:
    - ui
    - react
    - design-system
    - artifact
    - creative
    - interactive
  updated-date: "2026-06-24"
---

# Field-Notebook UI Skill

Generates React artifacts in a warm "engineer's field-notebook" visual system.
Every artifact you produce using this skill must feel like a page from a physical technical
notebook — structured, readable, purposeful — translated to an interactive screen.

---

## Design token system

Copy these tokens verbatim into every artifact. Never invent replacements.

```js
const T = {
  // Backgrounds
  bg:        "#ECE9DF",   // stone-paper — the page itself
  surface:   "#E5E1D5",   // slightly darker for cards / panels
  surfaceB:  "#DDD9CC",   // code block background

  // Text
  ink:       "#172423",   // petrol-black — primary text
  inkMid:    "#3D4F4E",   // secondary text
  inkDim:    "#6E7D7C",   // captions, placeholders
  rule:      "#C8C3B4",   // dividers, borders
  ruleHair:  "#B8B2A2",   // hairline card borders

  // Primary
  green:     "#0F6E56",   // petrol-green — primary actions, success, structural
  greenFill: "#0F6E5612", // low-alpha fill
  greenBg:   "#0F6E5620",

  // Semantic accents (use 2–3 per view, never all at once)
  rust:      "#B94A2C",   // errors, rollback, danger
  rustFill:  "#B94A2C14",
  amber:     "#C17D11",   // warnings, overrides, caution
  amberFill: "#C17D1114",
  purple:    "#5B3F8C",   // model, template, AI concepts
  purpleFill:"#5B3F8C12",
  blue:      "#1B5FA8",   // agent, commands, informational
  blueFill:  "#1B5FA812",
};
```

---

## Typography

Three fonts, three distinct roles. Never swap them.

| Role | Font | Usage |
|---|---|---|
| Display | Space Grotesk | Headings, labels on cards, tab names |
| Body | Inter | Prose, descriptions, detail text |
| Mono | JetBrains Mono | Eyebrows, code, technical labels, metadata |

Always load via Google Fonts in a `useEffect`:

```js
useEffect(() => {
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap";
  document.head.appendChild(link);
}, []);
```

### Eyebrow pattern

Every section opens with a spaced uppercase mono eyebrow, then a sentence-case Space Grotesk heading.

```jsx
// Eyebrow component
function Eyebrow({ children, color = T.green }) {
  return (
    <div style={{
      fontFamily: "'JetBrains Mono', monospace",
      fontSize: "9px",
      fontWeight: 700,
      letterSpacing: "2.5px",
      textTransform: "uppercase",
      color,
      marginBottom: "5px",
    }}>
      {children}
    </div>
  );
}

// Usage: always eyebrow THEN heading
<Eyebrow>Chart structure</Eyebrow>
<h2 style={{ fontFamily: "'Space Grotesk', sans-serif", fontWeight: 600, fontSize: "16px" }}>
  File anatomy
</h2>
```

---

## Core components

### Card with colored left border

The standard content container. Hairline border, soft radius (0 on left to meet the accent stripe).

```jsx
function Card({ accent = T.green, fill, children, style = {} }) {
  return (
    <div style={{
      background: fill || T.surface,
      border: `1px solid ${T.ruleHair}`,
      borderLeft: `3px solid ${accent}`,
      borderRadius: "0 6px 6px 0",
      padding: "12px 14px",
      ...style,
    }}>
      {children}
    </div>
  );
}
```

### AirGap — the signature element

A dashed enclosure with a small corner label. Use this for Paragon-specific context,
important notes, security warnings, or any annotation that must stand apart.
This is the single most distinctive element of the design — use it deliberately, not on every card.

```jsx
function AirGap({ label = "note", children }) {
  return (
    <div style={{
      position: "relative",
      border: `1.5px dashed ${T.green}`,
      borderRadius: "6px",
      padding: "18px 16px 14px",
      marginTop: "20px",
    }}>
      <div style={{
        position: "absolute",
        top: "-9px",
        left: "12px",
        background: T.bg,
        padding: "0 6px",
        fontFamily: "'JetBrains Mono', monospace",
        fontSize: "8px",
        fontWeight: 700,
        letterSpacing: "2px",
        textTransform: "uppercase",
        color: T.green,
      }}>
        ╴{label}╶
      </div>
      <div style={{ fontFamily: "'Inter', sans-serif", fontSize: "12px", color: T.inkMid, lineHeight: "1.8" }}>
        {children}
      </div>
    </div>
  );
}
```

### Code block

Left accent border in the relevant semantic color. Mono font, surfaceB background.

```jsx
function CodeBlock({ children, accent = T.blue }) {
  return (
    <div style={{
      background: T.surfaceB,
      border: `1px solid ${T.rule}`,
      borderLeft: `3px solid ${accent}`,
      borderRadius: "0 5px 5px 0",
      padding: "12px 14px",
      overflowX: "auto",
    }}>
      <pre style={{
        fontFamily: "'JetBrains Mono', monospace",
        fontSize: "11px",
        lineHeight: "1.8",
        color: T.inkMid,
        whiteSpace: "pre",
        margin: 0,
      }}>
        {children}
      </pre>
    </div>
  );
}
```

### Tab button

Active state: green background, white text. Inactive: transparent, dimmed mono label.

```jsx
function TabBtn({ active, onClick, children }) {
  return (
    <button onClick={onClick} style={{
      fontFamily: "'JetBrains Mono', monospace",
      fontSize: "10px",
      fontWeight: active ? 700 : 500,
      letterSpacing: "1px",
      textTransform: "uppercase",
      padding: "7px 16px",
      background: active ? T.green : "transparent",
      color: active ? "#fff" : T.inkDim,
      border: `1px solid ${active ? T.green : T.rule}`,
      borderRadius: "4px",
      cursor: "pointer",
      transition: "background 0.15s, color 0.15s, border-color 0.15s",
    }}>
      {children}
    </button>
  );
}
```

---

## Interaction patterns

### Click-to-expand

Use for timeline steps, glossary terms, layered diagrams. Always show a ▼/▲ indicator.
Manage state with `useState({})` keyed by item id.

```jsx
const [expanded, setExpanded] = useState({});
const toggle = (id) => setExpanded(p => ({ ...p, [id]: !p[id] }));

// In render:
<button onClick={() => toggle(item.id)} style={{ /* card styles */ }}>
  <span>{item.title}</span>
  {item.hasDetail && <span>{expanded[item.id] ? "▲" : "▼"}</span>}
</button>
{expanded[item.id] && <div>{item.detail}</div>}
```

### Hover transitions

All interactive elements: `transition: "background 0.15s, color 0.15s, border-color 0.15s"`.
Nothing else. No transforms, no shadows on hover.

### Keyboard focus

Every interactive element must have visible focus. Inject once via a style tag:

```jsx
useEffect(() => {
  const style = document.createElement("style");
  style.textContent = `
    .nb-focus:focus-visible {
      outline: 2px solid ${T.green};
      outline-offset: 2px;
      border-radius: 3px;
    }
    @media (prefers-reduced-motion: reduce) {
      * { transition: none !important; }
    }
  `;
  document.head.appendChild(style);
  return () => style.remove();
}, []);
```

Add `className="nb-focus"` to every `<button>`.

---

## Layout rules

**Header** — always present. Contains:
1. Eyebrow: `CONTEXT · DOMAIN` (e.g., `PARAGON INTERVIEW PREP · INFRASTRUCTURE`)
2. H1 in Space Grotesk 700, sentence case
3. Subtitle in Inter 13px, T.inkDim

**Tabs** — horizontal pill row below header when content has multiple views.
Use JetBrains Mono uppercase. Gap: 8px. Wrap on narrow viewports.

**Two-column layout** — nav/list on left (fixed ~200–260px), detail on right (flex: 1).
Both panels scroll independently. Left panel: background `T.bg`, right: `T.bg`.
Divider: `1px solid T.rule`.

**Single-column** — max-width 760px for prose-heavy content. Padding 28px.

**Footer / status bar** — optional single-line summary at bottom. T.surface background,
mono text, T.inkDim color.

---

## Semantic accent assignment

Pick 2–3 accents per artifact based on what they encode. Be consistent within one artifact.

| Concept type | Accent |
|---|---|
| Success, primary action, structural | green |
| Error, danger, rollback, removal | rust |
| Warning, override, caution, change | amber |
| AI model, template, generative | purple |
| Command, agent, informational, link | blue |
| Neutral, disabled, secondary | inkDim |

Never use an accent purely decoratively. It must encode meaning.

---

## What to avoid

- No drop shadows (not in the field-notebook aesthetic)
- No gradients on surfaces (the paper is flat)
- No emoji in content
- No bold text inside prose (use Space Grotesk headings instead)
- No dark backgrounds, dark cards, or inverted panels
- No generic sans-serif fallback without Space Grotesk / Inter loaded
- Do not use more than 3 accent colors in a single view
- Do not put the AirGap on more than 1–2 places per tab — it is a signature, not a pattern

---

## Process for generating an artifact

1. Read the user's prompt and identify: content domain, data shape, interaction model needed
2. Choose which tabs / sections the artifact needs
3. Assign semantic accents to content types (2–3 max per view)
4. Decide where the AirGap annotation belongs (one key insight per tab)
5. Build the artifact following the token system, components, and layout rules above
6. Inject fonts and global CSS via `useEffect` at component mount
7. Every interactive element gets `className="nb-focus"` and `transition: "... 0.15s"`

The artifact must be a self-contained React component with a default export.
All tokens, components, and styles are defined inline — no external imports beyond React.

## Output delivery — required

**Always write the artifact to a `.jsx` file. Never output it inline in chat.**

Steps:
1. Derive a short snake_case filename from the user's request (e.g. `helm_explainer.jsx`, `k8s_glossary.jsx`)
2. Write the complete component to a `.jsx` file using the Write tool
3. Report the file path to the user

Do not paste the JSX into the conversation. Do not use a code block as the deliverable.
The file is the deliverable.

---

## Quick-reference template

```jsx
import { useState, useEffect } from "react";

const T = { /* paste token block here */ };

export default function App() {
  const [tab, setTab] = useState("overview");

  useEffect(() => {
    // Inject fonts + global CSS (focus ring, reduced-motion)
  }, []);

  return (
    <div style={{ background: T.bg, minHeight: "100vh", color: T.ink,
                  fontFamily: "'Inter', sans-serif" }}>
      {/* Header */}
      <header style={{ padding: "20px 28px 16px", borderBottom: `1px solid ${T.rule}` }}>
        <Eyebrow>context · domain</Eyebrow>
        <h1 style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: "22px",
                     fontWeight: 700, color: T.ink }}>
          Artifact title
        </h1>
      </header>

      {/* Tabs */}
      <div style={{ display: "flex", gap: "8px", padding: "14px 28px",
                    borderBottom: `1px solid ${T.rule}`, flexWrap: "wrap" }}>
        {["Overview", "Detail", "Reference"].map(t => (
          <TabBtn key={t} active={tab === t.toLowerCase()} onClick={() => setTab(t.toLowerCase())}>
            {t}
          </TabBtn>
        ))}
      </div>

      {/* Content */}
      <main style={{ padding: "24px 28px", maxWidth: "760px" }}>
        {/* Cards, AirGap, CodeBlocks here */}
      </main>
    </div>
  );
}
```
