Build Signal Desk with Storybook

Build a React notification center with Storybook visual tests and Chromatic CI.

Introduction

30 Second Summary

Important updates lose their urgency when they blend into one crowded screen. A small layout mistake can also make that screen harder to scan without anyone noticing.

In this project, you will build Signal Desk from typed React components. Its Storybook examples become visual regression tests that Chromatic runs through GitHub Actions.

What You'll Build

You'll demo Signal Desk by dismissing an alert and watching the unread total respond instantly.

By the end of this project, you'll have:

  • An interactive notification center where you dismiss typed alerts. The unread total updates immediately.
  • A component workshop where you open every alert state. Controls let you change props without editing application code.
  • An automated visual safety net that compares Storybook snapshots in Chromatic. GitHub Actions runs the check on pushes and pull requests.
  • Secret Mission: Add an unread-only filter. Capture it as a new visual state.

Are there any prerequisites?

You'll need comfort with TypeScript plus Node.js, Visual Studio Code, Git, GitHub, and Chromatic. No prior React or Storybook experience is required.

Before We Start

This step locks in the operations users Signal Desk serves. It also captures why protecting Storybook states with visual regression testing matters before any hands-on work begins.

Set Up Signal Desk

Signal Desk needs a predictable local toolchain. A mismatched Node.js version can cause setup errors that distract from component design.

A manual Visual Studio Code workspace keeps every file predictable. Pinned React dependencies give later Storybook checks a stable foundation.

In this step, get ready to:
  • Verify the local development tools.
  • Create the Signal Desk workspace files.
  • Launch the Vite app plus the Storybook workshop.
Verify the local toolchain

The workspace uses Node.js 24.20.0 LTS. Git tracks the project history once the workspace is ready.

  • Press the Windows key to open search.
  • Type PowerShell into the search field.
  • Press Enter to open PowerShell.
  • Check the installed Node.js version plus the Git version by running these commands:
node --version
git --version

What do these checks prove?

  • The first command prints the Node.js version available to your terminal.
  • The second command confirms that Git can record future project changes.

✔️ I see v24.20.0

Your Node.js runtime matches this project. A Git version in the next line confirms that both tools are ready.

ⓧ I see a different version

This project uses Node.js 24.20.0 LTS so every pinned dependency runs against the same runtime.

  • Visit the official Node.js download page.
  • Download Node.js 24.20.0 LTS for Windows.
  • Complete the installer with its default options.
  • Close PowerShell.
  • Reopen PowerShell through Windows search.
  • Check the installed version again by running this command:
node --version

You should now see v24.20.0.

ⓧ Command not found

PowerShell cannot currently find Node.js. Installing the pinned LTS release adds the runtime required by the workspace.

  • Visit the official Node.js download page.
  • Download Node.js 24.20.0 LTS for Windows.
  • Complete the installer with its default options.
  • Close PowerShell.
  • Reopen PowerShell through Windows search.
  • Check the installation by running this command:
node --version

You should now see v24.20.0.

Git did not print a version?

Restart PowerShell first so it can reload the system path. If Git still has no version output, confirm that your existing Git installation is available from Windows search.

Ask for help with the local setup: help me diagnose why Git is unavailable in PowerShell

Create the workspace files

Manual configuration gives Signal Desk a deterministic file tree. It also keeps the pinned TypeScript plus Vite settings visible.

  • Press the Windows key to open search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Select File from the menu bar.
  • Select Open Folder....
  • Select your Desktop location.
  • Select New Folder.
  • Enter signal-desk as the folder name.
  • Select Select Folder to open signal-desk as the workspace.
  • Select Yes, I trust the authors if the Workspace Trust prompt appears.

You should now see the empty signal-desk folder in the Explorer sidebar.

  • Select New Folder in the Explorer sidebar.
  • Enter .storybook as the folder name.
  • Select New Folder in the Explorer sidebar.
  • Enter src as the folder name.

The Explorer should now show .storybook plus src inside signal-desk.

  • Select New File... in the Explorer sidebar.
  • Enter package.json as the file name.
  • Fill package.json with the pinned package configuration below:
{
  "name": "signal-desk",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "chromatic": "chromatic"
  },
  "dependencies": {
    "react": "19.2.8",
    "react-dom": "19.2.8"
  },
  "devDependencies": {
    "@chromatic-com/storybook": "5.3.0",
    "@storybook/addon-docs": "10.5.10",
    "@storybook/react-vite": "10.5.10",
    "@types/react": "19.2.18",
    "@types/react-dom": "19.2.5",
    "@vitejs/plugin-react": "6.1.1",
    "chromatic": "18.6.1",
    "storybook": "10.5.10",
    "typescript": "7.0.2",
    "vite": "8.2.2"
  }
}

What does this package file control?

  • The dependency section pins React plus React DOM to 19.2.8.
  • The development dependencies pin Storybook to 10.5.10.
  • The development dependencies pin Chromatic to 18.6.1.
  • The scripts provide consistent commands for development, builds, stories, plus visual testing.
  • Save package.json.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter tsconfig.json as the file name.
  • Fill tsconfig.json with the compiler configuration below:
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src", ".storybook", "vite.config.ts"]
}

What does the compiler configuration do?

  • Strict mode checks the component contracts for unsafe TypeScript values.
  • Bundler resolution matches the way Vite loads modules.
  • The include list covers source files, Storybook configuration, plus Vite configuration.
  • Save tsconfig.json.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter vite.config.ts as the file name.
  • Fill vite.config.ts with the React plugin configuration below:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
});

How does Vite connect to React?

The React plugin lets Vite transform the project's TSX components. defineConfig keeps the configuration typed.

  • Save vite.config.ts.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter index.html as the file name.
  • Fill index.html with the page shell below:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Signal Desk</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

How does the browser reach the app?

  • The root element gives React a browser mount point.
  • The module script loads src/main.tsx as the application entry point.
  • Save index.html.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter .gitignore as the file name.
  • Fill .gitignore with the generated-directory rules below:
node_modules
dist
storybook-static

Why ignore these directories?

Dependencies plus generated builds can be recreated from the project configuration. Keeping them out of commits makes the repository smaller.

  • Save .gitignore.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter .storybook/main.ts as the file path.
  • Fill .storybook/main.ts with the Storybook configuration below:
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: ['@storybook/addon-docs', '@chromatic-com/storybook'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
};

export default config;

What does Storybook load?

  • The story pattern searches src for TypeScript story files.
  • The Docs addon generates component documentation.
  • The Chromatic addon adds visual testing support.
  • Save .storybook/main.ts.
  • Confirm the file appears inside .storybook in Explorer.
  • Select New File... in the Explorer sidebar.
  • Enter .storybook/preview.ts as the file path.
  • Fill .storybook/preview.ts with the shared preview configuration below:
import type { Preview } from '@storybook/react-vite';
import '../src/styles.css';

const preview: Preview = {};

export default preview;

Why import the stylesheet here?

The preview imports the same CSS used by the browser app. Every future story therefore starts with the shared Signal Desk styling.

  • Save .storybook/preview.ts.
  • Confirm the file appears inside .storybook in Explorer.
  • Select New File... in the Explorer sidebar.
  • Enter src/env.d.ts as the file path.
  • Fill src/env.d.ts with the CSS declaration below:
declare module '*.css';

What does the declaration enable?

The declaration lets TypeScript accept stylesheet imports in the application files. Vite handles the CSS when it builds the app.

  • Save src/env.d.ts.
  • Confirm the file appears inside src in Explorer.
  • Select New File... in the Explorer sidebar.
  • Enter src/main.tsx as the file path.
  • Fill src/main.tsx with the browser entry point below:
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './styles.css';

const rootElement = document.getElementById('root');

if (!rootElement) {
  throw new Error('Root element not found');
}

createRoot(rootElement).render(<App />);

What does the entry point do?

  • The root lookup connects the script to the element in index.html.
  • The missing-root error stops the app with a clear cause if the page shell changes.
  • The final line renders App into the browser.
  • Save src/main.tsx.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter src/App.tsx as the file path.
  • Fill src/App.tsx with the starter page below:
export function App() {
  return (
    <main className="app-shell">
      <header className="hero">
        <p className="eyebrow">Operations workspace</p>
        <h1>Signal Desk</h1>
        <p>Review important product events without losing the signal.</p>
      </header>
    </main>
  );
}

What does the starter component show?

The starter component renders the Signal Desk heading plus its operations-focused description. This gives you a visible React result before the notification components arrive.

  • Save src/App.tsx.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Select New File... in the Explorer sidebar.
  • Enter src/styles.css as the file path.
  • Start src/styles.css with the global page styles below:
:root {
  color: #172033;
  background: #eef2f7;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button {
  font: inherit;
}

What do the global styles establish?

  • The root rules establish the page colours plus the system font stack.
  • The universal rule keeps element sizing predictable.
  • The body rules let the page fill the browser height.
  • Save src/styles.css.
  • Confirm its editor tab no longer shows an unsaved indicator.
  • Append the starter layout styles below to src/styles.css.
.app-shell {
  min-height: 100vh;
  padding: 48px 20px;
  background:
    radial-gradient(circle at top left, #dbeafe 0, transparent 34%),
    #eef2f7;
}

.hero,
.notification-center {
  width: min(760px, 100%);
  margin: 0 auto;
}

.hero {
  margin-bottom: 24px;
}

.hero h1,
.notification-center h2,
.alert-card h3 {
  margin: 0;
}

.hero > p:last-child {
  margin-bottom: 0;
  color: #526076;
}

What do the layout styles add?

The shell creates a full-height background around the page. The hero rules keep the starter content centred within a readable width.

  • Save src/styles.css.
  • Confirm its editor tab no longer shows an unsaved indicator.

✔️ Awesome, I've got everything!

Your manually configured workspace is ready for dependency installation. Double check that every editor tab is saved.

ⓧ I'd like to double check the full code

Compare each saved file with the complete starter workspace below:

The full package.json file:

{
  "name": "signal-desk",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "chromatic": "chromatic"
  },
  "dependencies": {
    "react": "19.2.8",
    "react-dom": "19.2.8"
  },
  "devDependencies": {
    "@chromatic-com/storybook": "5.3.0",
    "@storybook/addon-docs": "10.5.10",
    "@storybook/react-vite": "10.5.10",
    "@types/react": "19.2.18",
    "@types/react-dom": "19.2.5",
    "@vitejs/plugin-react": "6.1.1",
    "chromatic": "18.6.1",
    "storybook": "10.5.10",
    "typescript": "7.0.2",
    "vite": "8.2.2"
  }
}

The full tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src", ".storybook", "vite.config.ts"]
}

The full vite.config.ts file:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
});

The full index.html file:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Signal Desk</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

The full .gitignore file:

node_modules
dist
storybook-static

The full .storybook/main.ts file:

import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: ['@storybook/addon-docs', '@chromatic-com/storybook'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
};

export default config;

The full .storybook/preview.ts file:

import type { Preview } from '@storybook/react-vite';
import '../src/styles.css';

const preview: Preview = {};

export default preview;

The full src/env.d.ts file:

declare module '*.css';

The full src/main.tsx file:

import { createRoot } from 'react-dom/client';
import { App } from './App';
import './styles.css';

const rootElement = document.getElementById('root');

if (!rootElement) {
  throw new Error('Root element not found');
}

createRoot(rootElement).render(<App />);

The full starter src/App.tsx file:

export function App() {
  return (
    <main className="app-shell">
      <header className="hero">
        <p className="eyebrow">Operations workspace</p>
        <h1>Signal Desk</h1>
        <p>Review important product events without losing the signal.</p>
      </header>
    </main>
  );
}

The full starter src/styles.css file:

:root {
  color: #172033;
  background: #eef2f7;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button {
  font: inherit;
}

.app-shell {
  min-height: 100vh;
  padding: 48px 20px;
  background:
    radial-gradient(circle at top left, #dbeafe 0, transparent 34%),
    #eef2f7;
}

.hero,
.notification-center {
  width: min(760px, 100%);
  margin: 0 auto;
}

.hero {
  margin-bottom: 24px;
}

.hero h1,
.notification-center h2,
.alert-card h3 {
  margin: 0;
}

.hero > p:last-child {
  margin-bottom: 0;
  color: #526076;
}

The package-lock.json file appears after the dependency installation in the next substep.

Install and launch the workspace

The source files describe the workspace. The dependency installation now downloads every pinned package plus generates the lockfile used by future continuous integration.

  • Select View from the Visual Studio Code menu bar.
  • Select Terminal to open the integrated terminal at the signal-desk folder.
  • Select PowerShell from the terminal profile list if another shell opens.
  • Install the exact pinned dependencies by running this command:
npm install

The first installation can take a few minutes while npm downloads the dependency tree. Terminal activity means the process is still working.

  • Wait for PowerShell to return to its prompt.
  • Confirm package-lock.json now appears in the Explorer sidebar.

Dependency installation failed?

Confirm that PowerShell is running inside the signal-desk workspace. Check that package.json matches the pinned configuration before trying the installation again.

Get targeted setup help: help me diagnose my npm installation failure

Before you launch the first server, what do you expect the browser to show from the starter App component?

  • Start the Vite development server by running this command:
npm run dev
  • Open the local address shown in the terminal.

You should see the Signal Desk heading plus the sentence about reviewing important product events. That visible page proves the React entry point, Vite configuration, plus shared stylesheet work together.

Starter page did not load?

Check the terminal for the first file path mentioned by the build error. Confirm that index.html loads /src/main.tsx.

Get help with the runtime output: help me debug why the Vite starter page is not loading

  • Return to Visual Studio Code.
  • Select Terminal from the menu bar.
  • Select New Terminal to keep the Vite server running separately.

Before you launch the component workshop, do you expect it to reuse the stylesheet imported by the browser app?

  • Start Storybook in the second PowerShell terminal by running this command:
npm run storybook
  • Open the Storybook address shown in the terminal if the browser does not open automatically.
  • Confirm the browser address uses port 6006.

You should see the Storybook component workshop in a separate browser tab. The Vite page remains available from the first terminal.

Storybook did not open?

Keep the Vite server in its original terminal. Confirm that the second terminal is also running from the signal-desk workspace.

Ask for help with the Storybook process: help me debug why Storybook is not starting on port 6006

Your workspace now runs the browser app plus the component workshop. Next, you will turn the starter page into a typed notification center built from reusable components.

Build the Typed Component Set

Your local workspace now runs both the Vite app and component workshop. The starter page still hides the reusable React design that Signal Desk needs.

Typed props give each visual primitive a clear contract. You will compose those primitives inside a stateful notification center using TypeScript.

In this step, get ready to:
  • Create typed badge and button components.
  • Compose alerts inside a stateful notification center.
  • Style the finished app and test its dismiss behavior.
Build the typed visual primitives

A prop contract defines the values that a component accepts. The StatusTone union also limits every badge to one of three supported visual states.

  • Create a folder named components inside src from the Explorer sidebar.
  • Create StatusBadge.tsx inside src/components.
  • Add the badge prop contract and component by pasting this code:
export type StatusTone = 'info' | 'success' | 'warning';

interface StatusBadgeProps {
  tone: StatusTone;
  children: string;
}

export function StatusBadge({ tone, children }: StatusBadgeProps) {
  return (
    <span className={`status-badge status-badge--${tone}`}>
      {children}
    </span>
  );
}

What does this component do?

  • The StatusTone union permits the info, success, and warning values.
  • The prop interface requires a tone plus the text displayed inside the badge.
  • The component turns the tone into a modifier class that the stylesheet can target.
  • Save src/components/StatusBadge.tsx.
  • Create ActionButton.tsx inside src/components.
  • Add the button prop contract and component by pasting this code:
interface ActionButtonProps {
  label: string;
  onClick: () => void;
}

export function ActionButton({ label, onClick }: ActionButtonProps) {
  return (
    <button className="action-button" type="button" onClick={onClick}>
      {label}
    </button>
  );
}

How does the button contract help?

  • The label prop supplies the visible button text.
  • The onClick prop requires a callback with no arguments or return value.
  • The component keeps button markup consistent wherever Signal Desk needs an action.
  • Save src/components/ActionButton.tsx.
  • Confirm both prop contracts compile by running this command in the PowerShell terminal for signal-desk:
npm run build

What does this check prove?

The build script checks every included TypeScript file before Vite creates the production bundle. A successful build confirms that both component contracts are valid.

You should see the build complete successfully. Your first two reusable components now pass the same type check as the complete app.

Build failing on a primitive?

Check that both files sit inside src/components. Confirm that StatusTone is exported from StatusBadge.tsx.

Use this prompt if the build still fails: help me debug the TypeScript prop contracts in StatusBadge.tsx and ActionButton.tsx

Compose the stateful notification center

Each alert needs a shared data shape before the center can manage a list of them. AlertItem becomes that contract for the card and its parent.

  • Create AlertCard.tsx inside src/components.
  • Add the imports and alert contracts by pasting this first part:
import { ActionButton } from './ActionButton';
import { StatusBadge, type StatusTone } from './StatusBadge';

export interface AlertItem {
  id: number;
  tone: StatusTone;
  title: string;
  message: string;
  timestamp: string;
  unread: boolean;
}

interface AlertCardProps {
  item: AlertItem;
  onDismiss: (id: number) => void;
}

const toneLabels: Record<StatusTone, string> = {
  info: 'Info',
  success: 'Success',
  warning: 'Warning',
};

What do these contracts control?

  • The exported AlertItem interface gives every notification the same six fields.
  • The onDismiss callback receives the numeric ID of the selected alert.
  • The toneLabels record guarantees a visible label for every supported tone.
  • Add the card component directly below toneLabels by pasting this code:
export function AlertCard({ item, onDismiss }: AlertCardProps) {
  return (
    <article
      className={`alert-card${item.unread ? ' alert-card--unread' : ''}`}
      aria-label={`${item.title} notification`}
    >
      <div className="alert-card__content">
        <div className="alert-card__meta">
          <StatusBadge tone={item.tone}>{toneLabels[item.tone]}</StatusBadge>
          <span>{item.timestamp}</span>
        </div>
        <h3>{item.title}</h3>
        <p>{item.message}</p>
      </div>
      <ActionButton label="Dismiss" onClick={() => onDismiss(item.id)} />
    </article>
  );
}

How is the card composed?

  • The unread modifier class gives unread cards a distinct visual treatment.
  • The StatusBadge receives the alert tone and its matching label.
  • The ActionButton sends the current alert ID back to the parent when clicked.
  • Save src/components/AlertCard.tsx.
  • Confirm the composed card passes its type check by running:
npm run build

What does this build check?

The compiler now follows the prop flow across three files. Success proves that the badge tone and dismiss callback connect through the card correctly.

You should see another successful build. The card contract is ready for a parent component to supply real alert data.

Seeing a card type error?

Check that StatusTone uses a type-only import. Confirm that onDismiss accepts a number in AlertCardProps.

Use this prompt for a focused comparison: help me trace the prop type mismatch in AlertCard.tsx

The notification center owns the changing array. Its state starts with sample alerts and derives the unread total from the current items.

  • Create NotificationCenter.tsx inside src/components.
  • Add the imports and three sample alerts by pasting this code:
import { useState } from 'react';
import { AlertCard, type AlertItem } from './AlertCard';

export const sampleAlerts: AlertItem[] = [
  {
    id: 1,
    tone: 'warning',
    title: 'Payment retries increased',
    message: 'Retry volume is 18% above the morning baseline.',
    timestamp: '2 min ago',
    unread: true,
  },
  {
    id: 2,
    tone: 'success',
    title: 'Deployment completed',
    message: 'Version 2.4.0 is healthy in the production environment.',
    timestamp: '12 min ago',
    unread: true,
  },
  {
    id: 3,
    tone: 'info',
    title: 'Weekly report is ready',
    message: 'The latest reliability summary is available for review.',
    timestamp: '1 hr ago',
    unread: false,
  },
];

What does the sample data establish?

The array covers all three badge tones. Two items are unread, which gives the finished component a concrete count to derive.

  • Save src/components/NotificationCenter.tsx.
  • Confirm every sample object satisfies AlertItem by running:
npm run build

What does this build confirm?

The explicit array type checks every sample field against AlertItem. A successful build confirms that all three objects match the shared contract.

You should see the build complete successfully. The notification data now has a verified shape.

Sample alerts failing to compile?

Compare each object with the six fields in AlertItem. Check that each tone matches one of the three values in StatusTone.

Ask for help with the exact compiler output here: help me find the invalid field in my sampleAlerts array

The component function spans state logic and returned markup. Paste the next two parts consecutively before saving the file.

  • Add the prop contract and state logic directly below sampleAlerts by pasting this first part:
interface NotificationCenterProps {
  initialItems?: AlertItem[];
}

export function NotificationCenter({
  initialItems = sampleAlerts,
}: NotificationCenterProps) {
  const [items, setItems] = useState<AlertItem[]>(initialItems);
  const unreadCount = items.filter((item) => item.unread).length;

  function dismissAlert(id: number) {
    setItems((currentItems) =>
      currentItems.filter((item) => item.id !== id),
    );
  }

How does the state update work?

  • The optional initialItems prop lets the component start with supplied alerts or the sample array.
  • The items state holds the current notification list.
  • The filter call returns a new array without the dismissed ID, which keeps the state update immutable.
  • Complete the open NotificationCenter function by pasting this second part immediately below dismissAlert:
  return (
    <section className="notification-center" aria-labelledby="notification-title">
      <div className="notification-center__heading">
        <div>
          <p className="eyebrow">Live alerts</p>
          <h2 id="notification-title">Notification center</h2>
        </div>
        <p className="summary" aria-live="polite">
          {unreadCount} unread
        </p>
      </div>

      <div className="alert-list" aria-live="polite">
        {items.length > 0 ? (
          items.map((item) => (
            <AlertCard key={item.id} item={item} onDismiss={dismissAlert} />
          ))
        ) : (
          <p className="empty-state">All caught up.</p>
        )}
      </div>
    </section>
  );
}

How does the center render state?

  • The live summary displays the unread count derived from the current array.
  • The map creates one AlertCard for every remaining item.
  • The empty state appears after every alert has been dismissed.
  • Save src/components/NotificationCenter.tsx.
  • Confirm the complete stateful component compiles by running:
npm run build

What does this build prove?

The compiler now verifies the complete data path from sampleAlerts through NotificationCenter into each AlertCard. Success confirms that the parent and child contracts agree.

You should see the build finish successfully. Signal Desk now has a complete stateful feature ready to mount on the page.

Notification center not compiling?

Confirm that the second code part sits inside the open NotificationCenter function. Check the closing braces around dismissAlert and the returned markup.

Use this prompt to inspect the structure: help me find the misplaced brace in NotificationCenter.tsx

Assemble and style Signal Desk

The component tree is ready. The page now needs to mount NotificationCenter inside the Signal Desk shell before the stylesheet can shape the finished interface.

  • Replace the starter contents of src/App.tsx with this complete page component:
import { NotificationCenter } from './components/NotificationCenter';

export function App() {
  return (
    <main className="app-shell">
      <header className="hero">
        <p className="eyebrow">Operations workspace</p>
        <h1>Signal Desk</h1>
        <p>Review important product events without losing the signal.</p>
      </header>
      <NotificationCenter />
    </main>
  );
}

How is the page assembled?

The page shell supplies the product heading and supporting text. NotificationCenter owns the changing notification behavior beneath it.

  • Save src/App.tsx.
  • Start the local app in a free PowerShell terminal by running:
npm run dev

What does this command start?

This starts the Vite development server for the local React app. Keep this terminal running while you edit the stylesheet.

  • Open the local address printed in the PowerShell terminal.

You should see the Signal Desk heading above three notification cards. The starter stylesheet still controls their appearance.

Local app not opening?

Check that the PowerShell terminal is inside the signal-desk folder. If another Vite process is already running, return to its printed local address.

Use this prompt if the page remains unavailable: help me diagnose why my Vite app is not opening locally

The stylesheet builds the interface in visible layers. Each saved chunk gives you a concrete browser change to inspect.

  • Replace the starter contents of src/styles.css with these global foundations:
:root {
  color: #172033;
  background: #eef2f7;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button {
  font: inherit;
}

What do the global rules establish?

These rules set the page palette and typography. Universal border sizing keeps padding inside each element's calculated width.

  • Save src/styles.css.
  • Refresh the Signal Desk browser page.

You should see the page adopt a pale background and consistent system typography.

Global styles not appearing?

Confirm that src/main.tsx still imports ./styles.css. Check that you replaced the starter rules inside the existing stylesheet.

Ask for a focused check here: help me trace why src/styles.css is not affecting my Vite page

  • Add the page shell and hero layout at the bottom of src/styles.css by pasting:
.app-shell {
  min-height: 100vh;
  padding: 48px 20px;
  background:
    radial-gradient(circle at top left, #dbeafe 0, transparent 34%),
    #eef2f7;
}

.hero,
.notification-center {
  width: min(760px, 100%);
  margin: 0 auto;
}

.hero {
  margin-bottom: 24px;
}

.hero h1,
.notification-center h2,
.alert-card h3 {
  margin: 0;
}

.hero > p:last-child {
  margin-bottom: 0;
  color: #526076;
}

How does the shell frame the app?

The shell adds page spacing and a soft background accent. The shared width rule keeps the hero aligned with the notification center.

  • Save src/styles.css.
  • Refresh the browser page.

You should see the content centered inside a narrower column with a blue glow in the top-left corner.

Layout still spanning the page?

Check that both .hero and .notification-center share the width rule. Confirm that the class names match src/App.tsx.

Use this prompt for help: help me debug the app-shell and shared width styles

  • Add the eyebrow and notification container styles at the bottom of src/styles.css by pasting:
.eyebrow {
  margin: 0 0 6px;
  color: #4f46e5;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.notification-center {
  padding: 24px;
  border: 1px solid #d8e0eb;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 18px 50px rgba(30, 41, 59, 0.1);
}

What changes in this layer?

The eyebrow becomes a compact section label. The notification center becomes a raised panel that separates alerts from the page background.

  • Save src/styles.css.
  • Refresh the browser page.

You should see uppercase purple eyebrow labels and a rounded white notification panel.

Panel missing its card surface?

Confirm that the selector starts with .notification-center and includes the leading full stop. Check the browser page after saving the stylesheet.

Ask for help here: help me debug the notification-center panel styles

  • Add the center heading and alert list layout at the bottom of src/styles.css by pasting:
.notification-center__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}

.summary {
  margin: 0;
  padding: 8px 12px;
  border-radius: 999px;
  background: #eef2ff;
  color: #3730a3;
  font-weight: 700;
}

.alert-list {
  display: grid;
  gap: 12px;
}

How is the notification header arranged?

The flex layout places the heading opposite the unread summary. The alert list uses a grid gap to keep card spacing consistent.

  • Save src/styles.css.
  • Refresh the browser page.

You should see the unread total inside a rounded purple pill beside the notification heading.

Unread total below the heading?

Check that display: flex belongs to .notification-center__heading. Confirm the markup uses the same class name.

Use this prompt to compare the files: help me align the notification heading and unread summary

  • Add the card structure styles at the bottom of src/styles.css by pasting:
.alert-card {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 18px;
  padding: 18px;
  border: 1px solid #d8e0eb;
  border-radius: 14px;
  background: #ffffff;
}

.alert-card--unread {
  border-left: 5px solid #4f46e5;
}

.alert-card__content {
  min-width: 0;
}

Why use this card grid?

The first grid column lets alert content use the available width. The second column stays only as wide as the dismiss button.

Unread cards gain a thicker colored edge. That visual cue comes directly from the item's Boolean state.

  • Save src/styles.css.
  • Refresh the browser page.

You should see three separate white cards with their dismiss buttons aligned on the right. The first two cards should have purple left borders.

Cards missing their two-column layout?

Confirm that grid-template-columns is inside .alert-card. Check that every card receives the alert-card class in AlertCard.tsx.

Ask for help with the layout here: help me debug the alert-card grid

  • Add the alert metadata and message styles at the bottom of src/styles.css by pasting:
.alert-card__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  color: #667085;
  font-size: 0.82rem;
}

.alert-card h3 {
  font-size: 1rem;
}

.alert-card p {
  margin: 6px 0 0;
  color: #526076;
  line-height: 1.5;
}

What does the text hierarchy show?

Metadata stays compact above each title. Muted message text keeps the alert title as the strongest content inside each card.

  • Save src/styles.css.
  • Refresh the browser page.

You should see each timestamp aligned beside its badge with clearer spacing around the alert title and message.

Metadata still stacked vertically?

Check that the selector is .alert-card__meta with two underscore characters. Confirm that its declaration includes the flex layout.

Use this prompt for a selector check: help me find why alert-card metadata is not using flex layout

  • Add the status badge styles at the bottom of src/styles.css by pasting:
.status-badge {
  display: inline-flex;
  padding: 4px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.status-badge--info {
  background: #dbeafe;
  color: #1d4ed8;
}

.status-badge--success {
  background: #dcfce7;
  color: #166534;
}

.status-badge--warning {
  background: #fef3c7;
  color: #92400e;
}

How do the tone classes work?

The base class controls the shared pill shape and typography. Each modifier class supplies the color pair for one StatusTone value.

  • Save src/styles.css.
  • Refresh the browser page.

You should see yellow Warning, green Success, and blue Info badges across the three cards.

Badges all using one color?

Check that StatusBadge.tsx includes the tone in its modifier class. Compare each CSS suffix with the values in StatusTone.

Ask for help with the class mapping here: help me trace the StatusTone modifier classes

  • Add the shared button styles at the bottom of src/styles.css by pasting:
.action-button,
.filter-button {
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 700;
}

.action-button {
  padding: 9px 12px;
  background: #172033;
  color: #ffffff;
}

.action-button:hover,
.filter-button:hover {
  filter: brightness(1.08);
}

What do the button rules provide?

The shared selector keeps button fundamentals in one place. The action-specific rule gives every dismiss control a dark surface with white text.

  • Save src/styles.css.
  • Refresh the browser page.

You should see a dark rounded Dismiss button on the right side of every alert card.

Dismiss buttons still unstyled?

Confirm that ActionButton.tsx uses the action-button class. Check that the button declarations appear after the badge rules.

Use this prompt for help: help me debug the ActionButton class and CSS selector

  • Add the empty state and mobile styles at the bottom of src/styles.css by pasting:
.empty-state {
  margin: 0;
  padding: 32px;
  border: 1px dashed #b8c2d1;
  border-radius: 14px;
  color: #667085;
  text-align: center;
}

@media (max-width: 560px) {
  .app-shell {
    padding: 28px 12px;
  }

  .notification-center {
    padding: 16px;
  }

  .alert-card {
    grid-template-columns: 1fr;
  }

  .action-button {
    width: 100%;
  }
}

How does the layout adapt?

The empty state has its own dashed container when no alerts remain. Below 560px, each card switches to one column and its action spans the available width.

  • Save src/styles.css.
  • Narrow the browser window below 560px.

You should see each alert switch to one column with a full-width Dismiss button.

Mobile layout staying in two columns?

Check that the media query closes after the action button rule. Confirm that grid-template-columns: 1fr targets .alert-card inside that query.

Ask for help with the responsive block here: help me check the mobile media query in src/styles.css

✔️ Awesome, I've got everything!

Your changed files now match the component set shown in the browser.

ⓧ I'd like to double check the full code

  • Compare src/components/StatusBadge.tsx with this full file:
export type StatusTone = 'info' | 'success' | 'warning';

interface StatusBadgeProps {
  tone: StatusTone;
  children: string;
}

export function StatusBadge({ tone, children }: StatusBadgeProps) {
  return (
    <span className={`status-badge status-badge--${tone}`}>
      {children}
    </span>
  );
}

Status badge reference

This complete file owns the supported tone union and renders the matching modifier class.

  • Compare src/components/ActionButton.tsx with this full file:
interface ActionButtonProps {
  label: string;
  onClick: () => void;
}

export function ActionButton({ label, onClick }: ActionButtonProps) {
  return (
    <button className="action-button" type="button" onClick={onClick}>
      {label}
    </button>
  );
}

Action button reference

This complete file owns the reusable label and click callback contract.

  • Compare src/components/AlertCard.tsx with this full file:
import { ActionButton } from './ActionButton';
import { StatusBadge, type StatusTone } from './StatusBadge';

export interface AlertItem {
  id: number;
  tone: StatusTone;
  title: string;
  message: string;
  timestamp: string;
  unread: boolean;
}

interface AlertCardProps {
  item: AlertItem;
  onDismiss: (id: number) => void;
}

const toneLabels: Record<StatusTone, string> = {
  info: 'Info',
  success: 'Success',
  warning: 'Warning',
};

export function AlertCard({ item, onDismiss }: AlertCardProps) {
  return (
    <article
      className={`alert-card${item.unread ? ' alert-card--unread' : ''}`}
      aria-label={`${item.title} notification`}
    >
      <div className="alert-card__content">
        <div className="alert-card__meta">
          <StatusBadge tone={item.tone}>{toneLabels[item.tone]}</StatusBadge>
          <span>{item.timestamp}</span>
        </div>
        <h3>{item.title}</h3>
        <p>{item.message}</p>
      </div>
      <ActionButton label="Dismiss" onClick={() => onDismiss(item.id)} />
    </article>
  );
}

Alert card reference

This complete file defines the shared alert shape and composes the badge with the dismiss action.

  • Compare src/components/NotificationCenter.tsx with this full file:
import { useState } from 'react';
import { AlertCard, type AlertItem } from './AlertCard';

export const sampleAlerts: AlertItem[] = [
  {
    id: 1,
    tone: 'warning',
    title: 'Payment retries increased',
    message: 'Retry volume is 18% above the morning baseline.',
    timestamp: '2 min ago',
    unread: true,
  },
  {
    id: 2,
    tone: 'success',
    title: 'Deployment completed',
    message: 'Version 2.4.0 is healthy in the production environment.',
    timestamp: '12 min ago',
    unread: true,
  },
  {
    id: 3,
    tone: 'info',
    title: 'Weekly report is ready',
    message: 'The latest reliability summary is available for review.',
    timestamp: '1 hr ago',
    unread: false,
  },
];

interface NotificationCenterProps {
  initialItems?: AlertItem[];
}

export function NotificationCenter({
  initialItems = sampleAlerts,
}: NotificationCenterProps) {
  const [items, setItems] = useState<AlertItem[]>(initialItems);
  const unreadCount = items.filter((item) => item.unread).length;

  function dismissAlert(id: number) {
    setItems((currentItems) =>
      currentItems.filter((item) => item.id !== id),
    );
  }

  return (
    <section className="notification-center" aria-labelledby="notification-title">
      <div className="notification-center__heading">
        <div>
          <p className="eyebrow">Live alerts</p>
          <h2 id="notification-title">Notification center</h2>
        </div>
        <p className="summary" aria-live="polite">
          {unreadCount} unread
        </p>
      </div>

      <div className="alert-list" aria-live="polite">
        {items.length > 0 ? (
          items.map((item) => (
            <AlertCard key={item.id} item={item} onDismiss={dismissAlert} />
          ))
        ) : (
          <p className="empty-state">All caught up.</p>
        )}
      </div>
    </section>
  );
}

Notification center reference

This complete file owns the sample data and immutable state update used by every card.

  • Compare src/App.tsx with this full file:
import { NotificationCenter } from './components/NotificationCenter';

export function App() {
  return (
    <main className="app-shell">
      <header className="hero">
        <p className="eyebrow">Operations workspace</p>
        <h1>Signal Desk</h1>
        <p>Review important product events without losing the signal.</p>
      </header>
      <NotificationCenter />
    </main>
  );
}

App reference

This complete file renders the notification center inside the Signal Desk page shell.

  • Compare src/styles.css with this full file:
:root {
  color: #172033;
  background: #eef2f7;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button {
  font: inherit;
}

.app-shell {
  min-height: 100vh;
  padding: 48px 20px;
  background:
    radial-gradient(circle at top left, #dbeafe 0, transparent 34%),
    #eef2f7;
}

.hero,
.notification-center {
  width: min(760px, 100%);
  margin: 0 auto;
}

.hero {
  margin-bottom: 24px;
}

.hero h1,
.notification-center h2,
.alert-card h3 {
  margin: 0;
}

.hero > p:last-child {
  margin-bottom: 0;
  color: #526076;
}

.eyebrow {
  margin: 0 0 6px;
  color: #4f46e5;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.notification-center {
  padding: 24px;
  border: 1px solid #d8e0eb;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 18px 50px rgba(30, 41, 59, 0.1);
}

.notification-center__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}

.summary {
  margin: 0;
  padding: 8px 12px;
  border-radius: 999px;
  background: #eef2ff;
  color: #3730a3;
  font-weight: 700;
}

.alert-list {
  display: grid;
  gap: 12px;
}

.alert-card {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 18px;
  padding: 18px;
  border: 1px solid #d8e0eb;
  border-radius: 14px;
  background: #ffffff;
}

.alert-card--unread {
  border-left: 5px solid #4f46e5;
}

.alert-card__content {
  min-width: 0;
}

.alert-card__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  color: #667085;
  font-size: 0.82rem;
}

.alert-card h3 {
  font-size: 1rem;
}

.alert-card p {
  margin: 6px 0 0;
  color: #526076;
  line-height: 1.5;
}

.status-badge {
  display: inline-flex;
  padding: 4px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.status-badge--info {
  background: #dbeafe;
  color: #1d4ed8;
}

.status-badge--success {
  background: #dcfce7;
  color: #166534;
}

.status-badge--warning {
  background: #fef3c7;
  color: #92400e;
}

.action-button,
.filter-button {
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 700;
}

.action-button {
  padding: 9px 12px;
  background: #172033;
  color: #ffffff;
}

.action-button:hover,
.filter-button:hover {
  filter: brightness(1.08);
}

.empty-state {
  margin: 0;
  padding: 32px;
  border: 1px dashed #b8c2d1;
  border-radius: 14px;
  color: #667085;
  text-align: center;
}

@media (max-width: 560px) {
  .app-shell {
    padding: 28px 12px;
  }

  .notification-center {
    padding: 16px;
  }

  .alert-card {
    grid-template-columns: 1fr;
  }

  .action-button {
    width: 100%;
  }
}

Stylesheet reference

This complete file contains the final pre-filter visual system and responsive card layout.

Before you run the final check, do you expect every prop connection and state update to pass the production build?

  • Run the final production check in a separate PowerShell terminal:
npm run build

What does the final build verify?

The build checks the full component tree and creates the production bundle. Success confirms that the typed contracts agree across every changed file.

You should see the build complete successfully. The component set is valid from the reusable primitives through the page entry point.

  • Return to the browser running the Vite app.
  • Refresh the Signal Desk page.

You should see three notification cards and a summary of 2 unread.

  • Click Dismiss on the Payment retries increased card.

You should see two cards remain and the summary change to 1 unread. The immutable update removed one item while React recalculated the visible count.

Dismiss button not updating the page?

Check that AlertCard passes item.id into onDismiss. Confirm that NotificationCenter passes dismissAlert into every card.

Use this prompt to trace the click path: help me debug why Dismiss does not update NotificationCenter state

That is the component system working as one unit. Your typed alert data now drives both the visible cards and their live interactions.

Your reusable notification center is ready. Next, you will preserve its important states as interactive Storybook examples.

Turn UI States into Stories

Your typed React component set now renders a working notification center. The browser proves that its dismiss interaction works.

Manual app navigation exposes one moment at a time. Storybook stories preserve important component states as repeatable examples.

In this step, get ready to:
  • Create typed AlertCard stories for all three tones.
  • Capture two composed NotificationCenter states.
  • Inspect generated documentation. Change an argument through Controls.
Create AlertCard stories

A story records one useful set of component arguments. Component Story Format keeps those examples beside the component code.

  • Create a second PowerShell terminal in Visual Studio Code.
  • Start Storybook from the signal-desk workspace by running:
npm run storybook

What Does This Command Do?

The script starts the Storybook development server on port 6006. It keeps running so saved story changes can appear in the browser.

You'll see the Storybook component workshop open on port 6006.

Storybook Not Opening?

Confirm the second terminal is using the signal-desk workspace. Keep the process running while you create the stories.

help me diagnose why Storybook is not starting for my Signal Desk project.

  • Create src/components/AlertCard.stories.tsx through the Explorer sidebar.
  • Build the story module plus its first state by pasting this code into src/components/AlertCard.stories.tsx:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { AlertCard } from './AlertCard';

const meta = {
  title: 'Components/AlertCard',
  component: AlertCard,
  tags: ['autodocs'],
  args: {
    onDismiss: () => undefined,
  },
} satisfies Meta<typeof AlertCard>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Info: Story = {
  args: {
    item: {
      id: 1,
      tone: 'info',
      title: 'Weekly report is ready',
      message: 'The latest reliability summary is available for review.',
      timestamp: '1 hr ago',
      unread: false,
    },
  },
};

How Is This Story Typed?

  • The Meta definition connects the story file to AlertCard.
  • The satisfies Meta<typeof AlertCard> check keeps story arguments aligned with the component's TypeScript contract.
  • The shared onDismiss argument supplies the callback required by every story.
  • The Info story provides a complete AlertItem with the info tone.
  • Save src/components/AlertCard.stories.tsx.
  • Switch back to Storybook in your browser.
  • Select the Info story under Components/AlertCard.

You'll see the report alert rendered with an Info badge. The card uses the same component and styles as the Vite app.

Info Story Missing?

Confirm the filename ends with .stories.tsx. Check that the metadata title is Components/AlertCard.

help me find why my AlertCard Info story is missing from Storybook.

  • Add the success state below the Info story by pasting:
export const Success: Story = {
  args: {
    item: {
      id: 2,
      tone: 'success',
      title: 'Deployment completed',
      message: 'Version 2.4.0 is healthy in the production environment.',
      timestamp: '12 min ago',
      unread: true,
    },
  },
};

What Does This Story Preserve?

The Success story captures the completed deployment state. Its unread value also preserves the highlighted card border.

  • Save src/components/AlertCard.stories.tsx.
  • Select the Success story in Storybook.

You'll see a green Success badge on the deployment card. The unread border remains visible.

Success Styling Missing?

Confirm the story uses the success tone. Check that Storybook still loads the shared src/styles.css file.

help me diagnose why my AlertCard Success story has the wrong badge or styling.

  • Add the warning state below the Success story by pasting:
export const Warning: Story = {
  args: {
    item: {
      id: 3,
      tone: 'warning',
      title: 'Payment retries increased',
      message: 'Retry volume is 18% above the morning baseline.',
      timestamp: '2 min ago',
      unread: true,
    },
  },
};

Why Keep a Warning Story?

The Warning story preserves the state that calls for attention. Keeping each tone separate makes visual comparisons focused.

  • Save src/components/AlertCard.stories.tsx.
  • Select the Warning story in Storybook.

You'll see an amber Warning badge on the payment retry card. The sidebar now contains all three AlertCard states.

Warning Story Missing?

Confirm the Warning export sits after the completed Success object. Check each closing brace against the reference file below.

help me fix the structure of my AlertCard Warning story.

✔️ Awesome, I've got everything!

Your AlertCard story file now preserves its info, success, and warning states.

ⓧ I'd like to double check the full code

  • Compare src/components/AlertCard.stories.tsx with this complete reference:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { AlertCard } from './AlertCard';

const meta = {
  title: 'Components/AlertCard',
  component: AlertCard,
  tags: ['autodocs'],
  args: {
    onDismiss: () => undefined,
  },
} satisfies Meta<typeof AlertCard>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Info: Story = {
  args: {
    item: {
      id: 1,
      tone: 'info',
      title: 'Weekly report is ready',
      message: 'The latest reliability summary is available for review.',
      timestamp: '1 hr ago',
      unread: false,
    },
  },
};

export const Success: Story = {
  args: {
    item: {
      id: 2,
      tone: 'success',
      title: 'Deployment completed',
      message: 'Version 2.4.0 is healthy in the production environment.',
      timestamp: '12 min ago',
      unread: true,
    },
  },
};

export const Warning: Story = {
  args: {
    item: {
      id: 3,
      tone: 'warning',
      title: 'Payment retries increased',
      message: 'Retry volume is 18% above the morning baseline.',
      timestamp: '2 min ago',
      unread: true,
    },
  },
};
Compose NotificationCenter stories

Component stories can also preserve composed screens. These examples pass different alert arrays into the existing initialItems prop.

  • Create src/components/NotificationCenter.stories.tsx through the Explorer sidebar.
  • Build its metadata plus mixed inbox state by pasting:
import type { Meta, StoryObj } from '@storybook/react-vite';
import {
  NotificationCenter,
  sampleAlerts,
} from './NotificationCenter';

const meta = {
  title: 'Components/NotificationCenter',
  component: NotificationCenter,
  tags: ['autodocs'],
} satisfies Meta<typeof NotificationCenter>;

export default meta;
type Story = StoryObj<typeof meta>;

export const MixedInbox: Story = {
  args: {
    initialItems: sampleAlerts,
  },
};

How Does the Mixed Inbox Work?

  • The metadata documents NotificationCenter as a composed component.
  • The MixedInbox story reuses the exported sampleAlerts array.
  • The story starts with three cards. Two of those alerts contribute to the unread count.
  • Save src/components/NotificationCenter.stories.tsx.
  • Select the mixed inbox state under Components/NotificationCenter.

You'll see three notification cards. The summary shows two unread alerts.

Mixed Inbox Not Rendering?

Confirm sampleAlerts is imported from ./NotificationCenter. Check that the story passes it through initialItems.

help me diagnose why my NotificationCenter MixedInbox story does not render its alerts.

  • Add the focused warning state below MixedInbox by pasting:
export const SingleWarning: Story = {
  args: {
    initialItems: [sampleAlerts[0]],
  },
};

Why Use One Array Item?

The SingleWarning story passes only sampleAlerts[0] into the component. This isolates the warning card while preserving the NotificationCenter layout.

  • Save src/components/NotificationCenter.stories.tsx.
  • Select the single-warning state in Storybook.

You'll see one warning card. The summary shows one unread alert.

Single Warning Showing Three Cards?

Confirm sampleAlerts[0] remains inside an array. Passing sampleAlerts uses every sample alert.

help me fix why my SingleWarning story shows the wrong number of cards.

✔️ Awesome, I've got everything!

Your NotificationCenter stories now preserve both a full inbox and a focused warning state.

ⓧ I'd like to double check the full code

  • Compare src/components/NotificationCenter.stories.tsx with this complete reference:
import type { Meta, StoryObj } from '@storybook/react-vite';
import {
  NotificationCenter,
  sampleAlerts,
} from './NotificationCenter';

const meta = {
  title: 'Components/NotificationCenter',
  component: NotificationCenter,
  tags: ['autodocs'],
} satisfies Meta<typeof NotificationCenter>;

export default meta;
type Story = StoryObj<typeof meta>;

export const MixedInbox: Story = {
  args: {
    initialItems: sampleAlerts,
  },
};

export const SingleWarning: Story = {
  args: {
    initialItems: [sampleAlerts[0]],
  },
};
Test Docs and Controls

Generated Docs explain the component contract. Controls let you test different arguments without changing application code.

  • Select the Info story under Components/AlertCard.
  • Open the Docs view.

You'll see the rendered AlertCard beside generated documentation. Its documented arguments include item plus onDismiss.

  • Return to the rendered Info story.
  • Expand the Controls panel.
  • Change the tone property in the item object from info to warning.

You'll see the badge change from Info to Warning. The source files remain untouched.

Control Change Not Appearing?

Confirm you changed the nested tone property inside the item control. Use one of the supported values from StatusTone.

help me diagnose why changing AlertCard args in Storybook Controls does not update the component.

  • Before you compare the composed stories, predict which card count each state preserves.
  • Select the MixedInbox story under Components/NotificationCenter.
  • Select the SingleWarning story under Components/NotificationCenter.

The mixed inbox state shows three cards with two unread alerts. The single-warning state shows one card with one unread alert.

Storybook now lists the three AlertCard tones plus the two NotificationCenter compositions. Each state can be reopened without navigating the Vite app.

That is a meaningful milestone. Your component states are now repeatable examples with generated documentation.

Next, you will turn these stories into a visual regression baseline. Then you will watch a deliberate layout change get caught.

Catch a Real Visual Regression

Your Storybook stories now preserve each important notification state. Manual inspection can still miss a layout defect when several stories change at once.

In this step, Chromatic records approved snapshots as a visual baseline. You will introduce a deliberate visual regression to see the comparison catch what a quick review could miss.

In this step, get ready to:
  • Publish the current Signal Desk snapshot to GitHub.
  • Capture approved baselines for every existing story.
  • Expose a card layout regression in Chromatic.
Publish the working project

A Git commit freezes the working component system before the experiment begins. This gives your visual baseline a matching source-code snapshot.

  • Return to the PowerShell terminal for the signal-desk project in Visual Studio Code.
  • Initialize the local repository by running:
git init

What did Git initialize?

This creates the hidden .git directory that stores your project history. Your application files remain unchanged.

PowerShell confirms that it initialized a Git repository inside your existing signal-desk folder.

  • Stage the working project by running:
git add .

What did Git stage?

This prepares the project files for the first commit. The generated package-lock.json is included while the paths in .gitignore stay excluded.

  • Record the working project by running:
git commit -m 'Initial project version'

What did Git record?

The commit stores the staged files as one versioned snapshot. This snapshot contains the working app plus its typed stories.

Git prints the new commit identifier with a summary of the files it recorded. Your working component system now has a stable checkpoint.

Commit blocked by your Git identity?

Git may ask you to configure an author name or email before it can create the commit. Follow the commands shown in your terminal before running the commit again.

If another message appears, help me diagnose the failed initial commit.

The remote repository gives GitHub the same clean snapshot that Chromatic will test. Create it without starter files because those files already exist locally.

  • Return to your signed-in GitHub account in the browser.
  • Select the plus menu in the upper-right corner.
  • Click New repository.
  • Select your personal account from the Owner menu.
  • Enter signal-desk in the Repository name field.
  • Select Public under Choose visibility.

Keep the remote repository empty

Your local project already contains README alternatives such as its package metadata. Leaving the initialization options unchanged prevents a separate remote commit from blocking your first push.

  • Leave Add README turned off.
  • Leave the .gitignore option unchanged.
  • Leave the license option unchanged.
  • Click Create repository.
  • Record the repository URL here: https://github.com/YOUR_USERNAME/signal-desk.git.
  • Connect the local repository to your GitHub repository by running:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/signal-desk.git"]]

What did the remote command do?

This stores your GitHub repository URL under the remote name origin. Future pushes can use that short remote name.

Your first push may open a browser sign-in so GitHub can confirm that this repository belongs to you.

  • Push the current branch by running:
git push --set-upstream origin HEAD

What did the first push do?

This uploads the current branch to origin. It also configures that remote branch as the upstream destination for later pushes.

  • Complete the browser authentication if GitHub requests it.
  • Refresh the signal-desk repository page on GitHub.

You should see the project files in the repository. The file list includes package-lock.json from the dependency installation.

Push rejected or repository not found?

Confirm that the GitHub repository is named signal-desk. Check that your saved repository URL belongs to the account you selected as the owner.

For a different push error, help me connect and push my Signal Desk repository.

Capture the visual baseline

A visual baseline is the approved snapshot that future builds use for comparison. Each existing story needs one before a layout change can be classified as a regression.

  • Return to the running Storybook page from the previous step.
  • Open the Visual Tests panel.
  • Complete the Chromatic sign-in flow shown in the panel.
  • Select your existing Chromatic account.
  • Select an existing project or create a new project for signal-desk.
  • Click Catch a UI change to create the first visual build.

Chromatic captures baselines for Info, Success, Warning, MixedInbox, and SingleWarning. Those snapshots now represent the approved component layouts.

Visual build cannot start?

Confirm that Storybook still shows the Visual Tests panel. Return to the panel if the authentication flow opened Chromatic in another browser tab.

If the project cannot be linked, help me connect Storybook Visual Tests to Chromatic.

Trigger the layout regression

The approved snapshots give the next run something concrete to challenge. A one-line CSS change now tests whether Chromatic can identify damaged card layouts across the story set.

  • Return to src/styles.css in Visual Studio Code.
  • Find this declaration inside the .alert-card rule:
grid-template-columns: 1fr auto;

What does the current declaration protect?

The first column takes the available space. The second column stays sized to the dismiss button.

  • Replace the declaration with this deliberately broken value:
grid-template-columns: 80px 1fr;

What changed in the grid?

The alert content is forced into an 80px first column. The remaining space moves to the button column.

  • Save src/styles.css.

Before you run the visual tests, which AlertCard or NotificationCenter stories do you think will reveal the grid change?

  • Return to the Visual Tests panel in Storybook.
  • Click the run button in the top-right corner of the panel.

Chromatic highlights the affected stories in yellow. Opening one of those stories shows the pixels that changed from its baseline.

  • Select an affected story from the Storybook sidebar.
  • Inspect the changed pixels in the Visual Tests panel.
  • Leave the detected changes unaccepted.

No visual changes detected?

Confirm that src/styles.css contains grid-template-columns: 80px 1fr inside the .alert-card rule. Save the file before rerunning the visual tests.

If every story still passes, help me diagnose why Chromatic missed my alert-card CSS change.

✔️ Awesome, I've got everything!

Great. Your stylesheet keeps the deliberate card-layout defect while Chromatic holds the visual changes for review.

ⓧ I'd like to double check the full code

Compare your complete src/styles.css file with this version. The .alert-card declaration must remain deliberately broken for the next step.

:root {
  color: #172033;
  background: #eef2f7;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button {
  font: inherit;
}

.app-shell {
  min-height: 100vh;
  padding: 48px 20px;
  background:
    radial-gradient(circle at top left, #dbeafe 0, transparent 34%),
    #eef2f7;
}

.hero,
.notification-center {
  width: min(760px, 100%);
  margin: 0 auto;
}

.hero {
  margin-bottom: 24px;
}

.hero h1,
.notification-center h2,
.alert-card h3 {
  margin: 0;
}

.hero > p:last-child {
  margin-bottom: 0;
  color: #526076;
}

.eyebrow {
  margin: 0 0 6px;
  color: #4f46e5;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.notification-center {
  padding: 24px;
  border: 1px solid #d8e0eb;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 18px 50px rgba(30, 41, 59, 0.1);
}

.notification-center__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}

.summary {
  margin: 0;
  padding: 8px 12px;
  border-radius: 999px;
  background: #eef2ff;
  color: #3730a3;
  font-weight: 700;
}

.alert-list {
  display: grid;
  gap: 12px;
}

.alert-card {
  display: grid;
  grid-template-columns: 80px 1fr;
  align-items: center;
  gap: 18px;
  padding: 18px;
  border: 1px solid #d8e0eb;
  border-radius: 14px;
  background: #ffffff;
}

.alert-card--unread {
  border-left: 5px solid #4f46e5;
}

.alert-card__content {
  min-width: 0;
}

.alert-card__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  color: #667085;
  font-size: 0.82rem;
}

.alert-card h3 {
  font-size: 1rem;
}

.alert-card p {
  margin: 6px 0 0;
  color: #526076;
  line-height: 1.5;
}

.status-badge {
  display: inline-flex;
  padding: 4px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.status-badge--info {
  background: #dbeafe;
  color: #1d4ed8;
}

.status-badge--success {
  background: #dcfce7;
  color: #166534;
}

.status-badge--warning {
  background: #fef3c7;
  color: #92400e;
}

.action-button,
.filter-button {
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 700;
}

.action-button {
  padding: 9px 12px;
  background: #172033;
  color: #ffffff;
}

.action-button:hover,
.filter-button:hover {
  filter: brightness(1.08);
}

.empty-state {
  margin: 0;
  padding: 32px;
  border: 1px dashed #b8c2d1;
  border-radius: 14px;
  color: #667085;
  text-align: center;
}

@media (max-width: 560px) {
  .app-shell {
    padding: 28px 12px;
  }

  .notification-center {
    padding: 16px;
  }

  .alert-card {
    grid-template-columns: 1fr;
  }

  .action-button {
    width: 100%;
  }
}

How to compare this file

The stylesheet matches the working visual design except for the desktop .alert-card grid declaration. Keeping that single difference makes the visual test result easy to trace.

Chromatic caught the deliberate regression across your reusable states. Next, you will repair the layout before making this same review loop automatic.

Fix the Regression and Automate Chromatic

Your deliberate CSS change proved that Chromatic can catch a visual regression before it reaches users. Detection becomes useful engineering feedback once the approved layout is restored.

This step closes that loop with GitHub Actions. The same Chromatic check will run on every push and pull request.

In this step, get ready to:
  • Restore the approved alert card layout.
  • Store the Chromatic project token as a repository secret.
  • Automate visual tests with a passing continuous integration workflow.
Restore the approved card layout

The approved baseline records the card layout before the deliberate regression. The intended grid gives the alert text flexible space.

  • Switch back to Visual Studio Code.
  • In src/styles.css, replace the existing .alert-card selector with the corrected selector below:
.alert-card {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 18px;
  padding: 18px;
  border: 1px solid #d8e0eb;
  border-radius: 14px;
  background: #ffffff;
}

What restores the layout?

  • The 1fr column gives the alert content the remaining horizontal space.
  • The auto column keeps the Dismiss button at its natural width.
  • The remaining declarations preserve the approved card spacing.
  • Save src/styles.css.

Before you rerun the visual tests, do you expect the affected stories to match their approved baselines?

  • Return to the Visual Tests panel in Storybook.
  • Start a new visual test run for the existing stories.

That is the visual regression repaired. Chromatic no longer reports the unexpected card-layout difference.

Still seeing the card-layout difference?

Confirm that the .alert-card selector contains grid-template-columns: 1fr auto. Save the stylesheet before starting another visual test run.

Ask for help if the corrected stylesheet still produces a visual difference: help me compare my alert card CSS with the approved Signal Desk layout

✔️ Awesome, I've got everything!

Your corrected alert card selector is saved. The existing Chromatic stories now match their approved layout.

ⓧ I'd like to double check the full code

  • Compare your complete src/styles.css file with this reference:
:root {
  color: #172033;
  background: #eef2f7;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button {
  font: inherit;
}

.app-shell {
  min-height: 100vh;
  padding: 48px 20px;
  background:
    radial-gradient(circle at top left, #dbeafe 0, transparent 34%),
    #eef2f7;
}

.hero,
.notification-center {
  width: min(760px, 100%);
  margin: 0 auto;
}

.hero {
  margin-bottom: 24px;
}

.hero h1,
.notification-center h2,
.alert-card h3 {
  margin: 0;
}

.hero > p:last-child {
  margin-bottom: 0;
  color: #526076;
}

.eyebrow {
  margin: 0 0 6px;
  color: #4f46e5;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.notification-center {
  padding: 24px;
  border: 1px solid #d8e0eb;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 18px 50px rgba(30, 41, 59, 0.1);
}

.notification-center__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}

.summary {
  margin: 0;
  padding: 8px 12px;
  border-radius: 999px;
  background: #eef2ff;
  color: #3730a3;
  font-weight: 700;
}

.alert-list {
  display: grid;
  gap: 12px;
}

.alert-card {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 18px;
  padding: 18px;
  border: 1px solid #d8e0eb;
  border-radius: 14px;
  background: #ffffff;
}

.alert-card--unread {
  border-left: 5px solid #4f46e5;
}

.alert-card__content {
  min-width: 0;
}

.alert-card__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  color: #667085;
  font-size: 0.82rem;
}

.alert-card h3 {
  font-size: 1rem;
}

.alert-card p {
  margin: 6px 0 0;
  color: #526076;
  line-height: 1.5;
}

.status-badge {
  display: inline-flex;
  padding: 4px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.status-badge--info {
  background: #dbeafe;
  color: #1d4ed8;
}

.status-badge--success {
  background: #dcfce7;
  color: #166534;
}

.status-badge--warning {
  background: #fef3c7;
  color: #92400e;
}

.action-button,
.filter-button {
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 700;
}

.action-button {
  padding: 9px 12px;
  background: #172033;
  color: #ffffff;
}

.action-button:hover,
.filter-button:hover {
  filter: brightness(1.08);
}

.empty-state {
  margin: 0;
  padding: 32px;
  border: 1px dashed #b8c2d1;
  border-radius: 14px;
  color: #667085;
  text-align: center;
}

@media (max-width: 560px) {
  .app-shell {
    padding: 28px 12px;
  }

  .notification-center {
    padding: 16px;
  }

  .alert-card {
    grid-template-columns: 1fr;
  }

  .action-button {
    width: 100%;
  }
}

What should match?

This is the complete pre-secret stylesheet. The desktop .alert-card grid uses 1fr auto.

The responsive rule still switches the card to one column on screens below 560px.

Store the Chromatic project token

Project tokens deserve care because they authorize Chromatic builds. This one stays out of every file you commit.

  • Return to your Chromatic project's Manage page.
  • Select Configure.
  • Copy the project token.

Why use a repository secret?

A repository secret stores the token separately from the project files. The workflow refers to the secret by name.

This keeps the credential out of your source code. It also keeps the token out of the commit you push.

  • Return to the public signal-desk repository on GitHub.
  • Select Settings.
  • Select Secrets and variables.
  • Select Actions.

You are now in the repository-level secret area used by Actions workflows.

  • Select Secrets.
  • Select New repository secret.
  • Enter CHROMATIC_PROJECT_TOKEN in the Name field.
  • Paste the copied token into the Secret field.
  • Select Add secret.

Your repository now lists CHROMATIC_PROJECT_TOKEN as an Actions secret. The credential remains outside the project files.

Automate the Chromatic check

A workflow turns the successful manual check into a repeatable repository check. Pushes and pull requests now receive the same visual protection.

  • Switch back to Visual Studio Code.
  • Create .github/workflows/chromatic.yml from the file sidebar.
  • Paste the workflow below into .github/workflows/chromatic.yml:
name: Chromatic

on: [push, pull_request]

permissions:
  contents: read

jobs:
  visual-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: 24.20.0
          cache: npm
      - name: Install dependencies
        run: npm ci
      - name: Publish Storybook and run visual tests
        run: npm run chromatic
        env:
          CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Why run the pinned CLI?

  • The on configuration runs the workflow for push events plus pull_request events.
  • The contents: read permission gives the job read access to repository contents.
  • The actions/checkout@v7 step fetches the repository history required by Chromatic.
  • The actions/setup-node@v7 step configures Node.js 24.20.0 with the npm cache.
  • The npm ci step performs a clean installation from the committed lockfile.
  • The npm run chromatic step invokes the pinned Chromatic CLI. It reads CHROMATIC_PROJECT_TOKEN from the repository secret.
  • Save .github/workflows/chromatic.yml.
  • Confirm the file sidebar lists chromatic.yml inside .github/workflows.

Workflow file missing?

Check that the file path is exactly .github/workflows/chromatic.yml. A workflow outside .github/workflows cannot run as a GitHub Actions workflow.

Ask for help if the nested folders are difficult to create: help me create the Chromatic workflow in the correct Signal Desk folder

✔️ Awesome, I've got everything!

Your workflow file is saved in the location GitHub Actions uses for repository workflows.

ⓧ I'd like to double check the full code

  • Compare your complete .github/workflows/chromatic.yml file with this reference:
name: Chromatic

on: [push, pull_request]

permissions:
  contents: read

jobs:
  visual-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: 24.20.0
          cache: npm
      - name: Install dependencies
        run: npm ci
      - name: Publish Storybook and run visual tests
        run: npm run chromatic
        env:
          CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

What should this workflow contain?

The workflow contains one visual-tests job. That job installs the locked dependencies before publishing Storybook to Chromatic.

The token appears only as the secrets.CHROMATIC_PROJECT_TOKEN reference. Its value never appears in this file.

  • Use the Git controls in Visual Studio Code to stage src/styles.css.
  • Stage .github/workflows/chromatic.yml.
  • Create one commit containing the regression fix plus the workflow.
  • Push the current branch to the existing origin remote.

Before you inspect the run, do you expect the visual-tests job to pass with the corrected baseline?

  • Return to the signal-desk repository on GitHub.
  • Select Actions.
  • Select the newest Chromatic workflow run.
  • Select the visual-tests job.

The visual-tests job completes successfully. Chromatic provides the published Storybook build from the workflow.

That is the safety net in place. Your repository now checks visual changes automatically.

Visual tests job failing?

If dependency installation fails, confirm that package-lock.json is committed. The npm ci command requires that lockfile.

If Chromatic cannot authenticate, confirm that the repository secret is named exactly CHROMATIC_PROJECT_TOKEN. Copy the token again from the Chromatic project configuration if needed.

Ask for help with the failing step: help me diagnose my Signal Desk visual-tests job

Secret mission

Add an Unread-Only View

Busy notification centers need a quick way to hide messages that have already been read. Add an unread-only filter, preserve the total unread count, and capture the new state as a Chromatic snapshot.

Clean Up Your Resources

Clean Up Your Resources

Your local Signal Desk files have no ongoing cost. Standard runners for GitHub Actions are free for public repositories.

Chromatic Free costs $0 per month with 5,000 billed snapshots. Decide whether to keep your resources running, pause automated tests, or delete them entirely.

Resources you used:

  • Local signal-desk project folder containing the source files plus package-lock.json.
  • Public GitHub repository named signal-desk containing .github/workflows/chromatic.yml plus the CHROMATIC_PROJECT_TOKEN repository secret.
  • Chromatic project containing published Storybook builds plus approved visual baselines.

Keep everything running

No action needed. Choose this if you plan to keep improving Signal Desk or reuse its component patterns.

  • Keep the local signal-desk folder for future component changes.
  • Leave the public signal-desk repository active for future pushes.
  • Keep the Chromatic project to preserve its published builds plus visual baselines.
  • Monitor billed snapshot usage in Chromatic when you run more visual tests.

Pause - I'll come back to this later

This option stops future Chromatic runs from pushes. Your existing work stays available.

  • Return to .github/workflows/chromatic.yml in Visual Studio Code.
  • Rename the file to .github/workflows/chromatic.yml.paused.
  • Commit the renamed workflow file with Git.
  • Push that commit to the public signal-desk repository.
  • Confirm GitHub now shows .github/workflows/chromatic.yml.paused.
  • Leave the Chromatic project in place to preserve its existing baselines.

Future pushes stop launching the Chromatic workflow. Your published Storybook builds remain available.

Delete - I don't want to use this again

This option removes every Signal Desk resource for a fresh start. These deletions are permanent.

Delete the GitHub repository:

  • Return to the public signal-desk repository from earlier.
  • Open the repository project settings.
  • Use the repository deletion option to delete signal-desk.
  • Confirm the former repository URL no longer displays Signal Desk.

That clears the GitHub resources. The workflow plus its CHROMATIC_PROJECT_TOKEN secret are removed with the repository.

Delete the Chromatic project:

  • Return to the Chromatic project from earlier.
  • Open its project settings.
  • Use the project deletion option to remove the project containing your Signal Desk builds.
  • Confirm the project no longer appears in your Chromatic project list.

Your published builds, visual snapshots, and approved baselines are now removed.

Delete the local project folder:

  • Close Signal Desk in Visual Studio Code.
  • Use File Explorer to locate the existing signal-desk folder.
  • Select the signal-desk folder.
  • Use File Explorer's permanent delete action.
  • Approve the Windows deletion confirmation.
  • Confirm the signal-desk folder no longer appears in its former location.

Your Signal Desk resources are now fully removed.

Nice Work!

Nice Work!

You did it! Your typed React notification center is now protected by an automated visual regression workflow.

You've learned how to:

  • Built a typed React component system from reusable UI pieces. Used props to give each component an explicit contract. Used state with immutable filtering so dismissals keep the unread total accurate.
  • Captured reusable UI states as typed Storybook CSF stories. Used controls to change props without editing application code. Generated docs preserve each example for repeatable review.
  • Created a Chromatic visual baseline for approved UI states. Detected a deliberate card-layout regression through changed pixels. Added a GitHub Actions workflow that publishes Storybook. The same workflow reruns visual tests after repository changes.
  • Completed the optional Secret Mission by adding an unread-only filter. Kept the unread total accurate across both views. Captured UnreadOnly as a new Storybook visual state protected by Chromatic.

Ready to quiz yourself?