Hero monitorDesign system
VisualSite

Hero monitor

Dot-rendered terminal containing an animated demo.

npx context-ds add hero-monitor
On the site

Usage

Use it for

A product page hero's demo when it sits in a data flow: the story goes in as children. caseAttributes and caseOverlay line things up with the screen, such as the data flow's anchor.

Avoid

  • Content other than a 58 by 38 dot story on the screen
  • A monitor and the blue demo card in one hero

Dependencies: dot-engine

Example

examples/hero-monitor.tsx
import { HeroMonitor } from "@/components/visuals/hero-monitor";import { SearchStory } from "@/components/visuals/stories/search-story";export function Site() {  return (    <div className="mx-auto w-full max-w-xl">      <HeroMonitor>        <SearchStory />      </HeroMonitor>    </div>  );}

Source

components/visuals/hero-monitor.tsx
import { DOT_FILLED, DOT_HOLLOW } from '@/components/visuals/dot-engine/dot-morph-dots';import { polygon, rectangle, ringsWithSpeckle, solid, solidWithSpeckle, stack, type DotPainter } from '@/components/visuals/dot-engine/dot-story-cells';import { cn } from '@/lib/utils';import type { ReactNode } from 'react';// The same dot grid as the stories, so a story set on the screen lines up with the monitor dot for dot.const PITCH = 10;const DOT_RADIUS = 3.2;const RING_STROKE = 1;const SCREEN_CONTENT = { columns: 58, rows: 38 };const CASE_CORNER = 2;const CASE = { left: 2, right: 71, top: 1, bottom: 50 };const SCREEN = { left: 6, right: 67, top: 4, bottom: 45 };const CONTENT = { left: 8, top: 6 };const CONTROLS = { top: 47, bottom: 49, knobs: [64, 67], vents: { from: 9, to: 25 }, row: 48 };const NECK = { left: 33, right: 40, top: 51, bottom: 52 };const BASE = { left: 26, right: 47, top: 53, bottom: 55, flare: 2 };const KEYBOARD = { left: 6, right: 67, top: 57, bottom: 62, flare: 4 };const SPACE_BAR = { from: 22, to: 51, row: KEYBOARD.bottom - 1 };const KEY_PITCH = 3;const COLUMNS = 74;const ROWS = 64;const isWithin = ({ left, right, top, bottom }: { left: number; right: number; top: number; bottom: number }, column: number, row: number) => column >= left && column <= right && row >= top && row <= bottom;// The tube's case, its corners taken off: a box that size with square corners reads as a window, not a monitor.const monitorCase = polygon(	[		{ column: CASE.left + CASE_CORNER, row: CASE.top },		{ column: CASE.right - CASE_CORNER, row: CASE.top },		{ column: CASE.right, row: CASE.top + CASE_CORNER },		{ column: CASE.right, row: CASE.bottom - CASE_CORNER },		{ column: CASE.right - CASE_CORNER, row: CASE.bottom },		{ column: CASE.left + CASE_CORNER, row: CASE.bottom },		{ column: CASE.left, row: CASE.bottom - CASE_CORNER },		{ column: CASE.left, row: CASE.top + CASE_CORNER },	],	solidWithSpeckle);const monitor: DotPainter = (column, row) => {	if (isWithin(SCREEN, column, row)) return column === SCREEN.left || column === SCREEN.right || row === SCREEN.top || row === SCREEN.bottom ? DOT_HOLLOW : 0;	const shell = monitorCase(column, row);	if (shell === 0 || row < CONTROLS.top || row > CONTROLS.bottom || column <= CASE.left + 1 || column >= CASE.right - 1) return shell;	if (row !== CONTROLS.row) return DOT_HOLLOW;	const isVent = column >= CONTROLS.vents.from && column <= CONTROLS.vents.to && column % 2 === 1;	return CONTROLS.knobs.includes(column) || isVent ? DOT_FILLED : DOT_HOLLOW;};const stand = stack(	rectangle(NECK, solid),	polygon(		[			{ column: BASE.left, row: BASE.top },			{ column: BASE.right, row: BASE.top },			{ column: BASE.right + BASE.flare, row: BASE.bottom },			{ column: BASE.left - BASE.flare, row: BASE.bottom },		],		ringsWithSpeckle	));// Seen from the front and a little above, so it widens toward the typist; its keys are rings with gaps between them.const keyboardDeck = polygon(	[		{ column: KEYBOARD.left, row: KEYBOARD.top },		{ column: KEYBOARD.right, row: KEYBOARD.top },		{ column: KEYBOARD.right + KEYBOARD.flare, row: KEYBOARD.bottom },		{ column: KEYBOARD.left - KEYBOARD.flare, row: KEYBOARD.bottom },	],	(column, row) => {		if (row === SPACE_BAR.row) return column >= SPACE_BAR.from && column <= SPACE_BAR.to ? DOT_FILLED : 0;		return (column + row) % KEY_PITCH === 0 ? 0 : DOT_HOLLOW;	});const terminal = stack(monitor, stand, keyboardDeck);// Everything the monitor covers, gaps between its keys and its dark screen included, so what runs behind it stays hidden.const keyboardOutline = polygon(	[		{ column: KEYBOARD.left, row: KEYBOARD.top },		{ column: KEYBOARD.right, row: KEYBOARD.top },		{ column: KEYBOARD.right + KEYBOARD.flare, row: KEYBOARD.bottom },		{ column: KEYBOARD.left - KEYBOARD.flare, row: KEYBOARD.bottom },	],	() => DOT_HOLLOW);const isCovered = (column: number, row: number) => terminal(column, row) !== 0 || isWithin(SCREEN, column, row) || keyboardOutline(column, row) !== 0;// One rectangle per run of covered cells in a row, not one per cell: this path is inlined into every page with a hero// monitor, twice (HTML and RSC payload), and per-cell squares made it about 75KB.function silhouettePath() {	const segments: string[] = [];	for (let row = 0; row < ROWS; row++) {		let column = 0;		while (column < COLUMNS) {			if (!isCovered(column, row)) {				column++;				continue;			}			const runStart = column;			while (column < COLUMNS && isCovered(column, row)) column++;			const runWidth = (column - runStart) * PITCH;			segments.push(`M${runStart * PITCH} ${row * PITCH}h${runWidth}v${PITCH}h${-runWidth}z`);		}	}	return segments.join('');}function dotsPath(filled: boolean) {	const radius = filled ? DOT_RADIUS : DOT_RADIUS - RING_STROKE / 2;	const segments: string[] = [];	for (let row = 0; row < ROWS; row++) {		for (let column = 0; column < COLUMNS; column++) {			if (terminal(column, row) !== (filled ? DOT_FILLED : DOT_HOLLOW)) continue;			const cx = (column + 0.5) * PITCH;			const cy = (row + 0.5) * PITCH;			segments.push(`M${cx - radius} ${cy}a${radius} ${radius} 0 1 0 ${radius * 2} 0a${radius} ${radius} 0 1 0 ${-radius * 2} 0`);		}	}	return segments.join('');}const FILLED_PATH = dotsPath(true);const HOLLOW_PATH = dotsPath(false);const SILHOUETTE_PATH = silhouettePath();const share = (cells: number, of: number) => `${(cells / of) * 100}%`;const SCREEN_CONTENT_BOX = {	left: share(CONTENT.left, COLUMNS),	top: share(CONTENT.top, ROWS),	width: share(SCREEN_CONTENT.columns, COLUMNS),};const CASE_BOX = {	left: share(CASE.left, COLUMNS),	top: share(CASE.top, ROWS),	width: share(CASE.right + 1 - CASE.left, COLUMNS),	height: share(CASE.bottom + 1 - CASE.top, ROWS),};interface HeroMonitorProps {	/** A 58 by 38 dot story (or anything that size), set on the screen. */	children: ReactNode;	/** Attributes for a box over the monitor's case alone, without its stand and keyboard. */	caseAttributes?: Record<string, string>;	/** Drawn in that box, for anything that has to line up with the screen rather than the whole picture. */	caseOverlay?: ReactNode;	/** The color behind the monitor, which hides whatever runs behind it. A brand token or any CSS color. */	surfaceColor?: string;	className?: string;}/** * An old terminal drawn in the brand dot grid, the same one the Search story types into, with `children` (a 58 by 38 * dot story) set on its screen. `caseAttributes` and `caseOverlay` go on a box over the monitor's case alone, without * its stand and keyboard, for anything that has to line up with the screen rather than the whole picture. The dots * take the current text color (`text-brand` by default); the monitor itself is static, so it needs no reduced motion. */export function HeroMonitor({ children, caseAttributes, caseOverlay, surfaceColor = 'var(--ds-color-bg-default)', className }: HeroMonitorProps) {	return (		<div className={cn('relative w-full text-brand', className)} style={{ aspectRatio: `${COLUMNS} / ${ROWS}` }}>			<svg aria-hidden="true" viewBox={`0 0 ${COLUMNS * PITCH} ${ROWS * PITCH}`} className="pointer-events-none absolute inset-0 size-full">				<path d={SILHOUETTE_PATH} style={{ fill: surfaceColor }} />				<path d={FILLED_PATH} fill="currentColor" />				<path d={HOLLOW_PATH} fill="none" stroke="currentColor" strokeWidth={RING_STROKE} />			</svg>			<div aria-hidden="true" className="pointer-events-none absolute" style={CASE_BOX} {...caseAttributes}>				{caseOverlay}			</div>			<div className="absolute" style={SCREEN_CONTENT_BOX}>				{children}			</div>		</div>	);}

Reference: src/components/sections/shared/hero-monitor.tsx.