Free tools hero storyDesign system
VisualSite

Free tools hero story

Dot animation of tools processing a URL, with captions.

npx context-ds add story-free-tools-hero
On the site

Usage

Use it for

The free tools page hero on the site. Plays only on screen; shows the tool's results with reduced motion.

Avoid

  • Stories as the main content of a page

Dependencies: story-parts, dot-engine

Example

examples/story-free-tools-hero.tsx
import { Card } from "@/components/ui/card";import { FreeToolsHeroStory } from "@/components/visuals/stories/free-tools-hero-story";export function Site() {  return (    <Card variant="ring" className="min-h-[22rem] w-full max-w-xl items-center justify-center">      <div className="relative w-full max-w-[26rem]">        <FreeToolsHeroStory />      </div>    </Card>  );}

Source

components/visuals/stories/free-tools-hero-story.tsx
'use client';import { FREE_TOOLS_RESTING } from '@/components/visuals/stories/free-tools-hero-story-scenes';import { HERO_STORY_GRID } from '@/components/visuals/stories/hero-story-kit';import { DotStory, type LoadDotStory } from '@/components/visuals/dot-engine/dot-story';const loadFreeToolsHeroStory: LoadDotStory = async (stage) => {	const { createFreeToolsHeroStoryPlayer } = await import('@/components/visuals/stories/free-tools-hero-story-player');	return createFreeToolsHeroStoryPlayer(stage);};/** * The free tools hero's picture: a shelf of tools lighting up one by one, then one of them run on a URL. With * reduced motion it shows the tool's results. */export function FreeToolsHeroStory({ className }: { className?: string }) {	return <DotStory grid={HERO_STORY_GRID} resting={FREE_TOOLS_RESTING} load={loadFreeToolsHeroStory} className={className} />;}
components/visuals/stories/free-tools-hero-story-player.ts
import { FINDING_COUNT, FREE_TOOLS_CAPTIONS, SHELF_LIT, SHELF_UNLIT, TILE_COUNT, TOOL_DONE, TOOL_WAITING, paintShelf, paintTool } from '@/components/visuals/stories/free-tools-hero-story-scenes';import { HERO_STORY_GRID, stepAt } from '@/components/visuals/stories/hero-story-kit';import { captionedBeat } from '@/components/visuals/dot-engine/story-caption';import { createBeatStoryPlayer } from '@/components/visuals/dot-engine/dot-story-beats';import type { DotStoryPlayer, DotStoryStage } from '@/components/visuals/dot-engine/dot-story-player';/** * The free tools, played as a loop: the shelf lights up a tool at a time, then melts into one tool with a URL in * its field, and its results come in line by line. */const LIGHTING = { startsAt: 0.4, everySeconds: 0.16 };const LIT_AT = LIGHTING.startsAt + (TILE_COUNT - 1) * LIGHTING.everySeconds;const FINDING = { startsAt: 0.9, everySeconds: 0.35 };const FOUND_AT = FINDING.startsAt + (FINDING_COUNT - 1) * FINDING.everySeconds;export function createFreeToolsHeroStoryPlayer(stage: DotStoryStage): DotStoryPlayer {	return createBeatStoryPlayer(stage, {		grid: HERO_STORY_GRID,		resting: { beat: 1, heldSeconds: FOUND_AT + 0.8 },		beats: [			captionedBeat({				picture: SHELF_UNLIT,				departure: SHELF_LIT,				caption: FREE_TOOLS_CAPTIONS.shelf,				holdSeconds: LIT_AT + 1.6,				isTyped: true,				animate: (states, heldSeconds) => void paintShelf(states, stepAt(heldSeconds, LIGHTING, TILE_COUNT - 1) + 1),			}),			captionedBeat({				picture: TOOL_WAITING,				departure: TOOL_DONE,				caption: FREE_TOOLS_CAPTIONS.tool,				holdSeconds: FOUND_AT + 2,				isTyped: true,				animate: (states, heldSeconds) => void paintTool(states, stepAt(heldSeconds, FINDING, FINDING_COUNT - 1) + 1),			}),		],	});}
components/visuals/stories/free-tools-hero-story-scenes.ts
import { HERO_STORY_GRID, between, blankScreen, heroCaption, pictureOfStates } from '@/components/visuals/stories/hero-story-kit';import { DOT_ABSENT, DOT_FILLED, DOT_HOLLOW } from '@/components/visuals/dot-engine/dot-morph-dots';import { isInDotBox, paintShape, rings, roundedBox, solidWithSpeckle, type DotBox } from '@/components/visuals/dot-engine/dot-story-cells';/** The free tools hero's pictures, in the brand dot grid: a shelf of tools, and one of them run on a URL. */const TILE = { size: 6, left: 3, top: 6, everyColumn: 7, everyRow: 9, perRow: 4 };export const TILE_COUNT = 12;const tileBox = (index: number): DotBox => {	const left = TILE.left + (index % TILE.perRow) * TILE.everyColumn;	const top = TILE.top + Math.floor(index / TILE.perRow) * TILE.everyRow;	return { left, right: left + TILE.size - 1, top, bottom: top + TILE.size - 1 };};/** The shelf with its first `lit` tools picked out: a lit tool is solid, the rest rings. */export function paintShelf(states: Uint8Array, lit: number) {	for (let index = 0; index < TILE_COUNT; index++) paintShape(HERO_STORY_GRID, states, roundedBox(tileBox(index), 1), index < lit ? solidWithSpeckle : rings);	return states;}export const SHELF_UNLIT = pictureOfStates(paintShelf(blankScreen(), 0));export const SHELF_LIT = pictureOfStates(paintShelf(blankScreen(), TILE_COUNT));const FIELD: DotBox = { left: 3, right: 29, top: 5, bottom: 10 };const URL = { rows: [7, 8], left: 6, right: 17 };const BUTTON: DotBox = { left: 21, right: 27, top: 7, bottom: 8 };const PANEL: DotBox = { left: 3, right: 29, top: 14, bottom: 33 };const FINDING_KEY = { left: 6, right: 9 };const FINDING_VALUE_LEFT = 11;const FINDINGS = [	{ row: 17, right: 24 },	{ row: 20, right: 20 },	{ row: 23, right: 26 },	{ row: 26, right: 18 },	{ row: 29, right: 23 },];export const FINDING_COUNT = FINDINGS.length;/** The tool with a URL in its field and the first `found` lines of its results in. */export function paintTool(states: Uint8Array, found: number) {	paintShape(HERO_STORY_GRID, states, roundedBox(FIELD, 2), (column, row) => {		if (isInDotBox(BUTTON, column, row)) return DOT_FILLED;		return URL.rows.includes(row) && between(column, URL.left, URL.right) ? DOT_HOLLOW : DOT_ABSENT;	});	return paintShape(HERO_STORY_GRID, states, roundedBox(PANEL, 2), (column, row) => {		const finding = FINDINGS.findIndex((candidate) => candidate.row === row);		if (finding === -1 || finding >= found) return DOT_ABSENT;		if (between(column, FINDING_KEY.left, FINDING_KEY.right)) return DOT_FILLED;		return between(column, FINDING_VALUE_LEFT, FINDINGS[finding].right) ? DOT_HOLLOW : DOT_ABSENT;	});}export const TOOL_WAITING = pictureOfStates(paintTool(blankScreen(), 0));export const TOOL_DONE = pictureOfStates(paintTool(blankScreen(), FINDING_COUNT));export const FREE_TOOLS_CAPTIONS = {	shelf: heroCaption('pick a tool', 'free', 'no account for basic use'),	tool: heroCaption('paste a URL', 'results', 'powered by the API'),};/** The tool with its results in: what the picture shows before it plays, and instead of playing with reduced motion. */export const FREE_TOOLS_RESTING = { ...TOOL_DONE, texts: FREE_TOOLS_CAPTIONS.tool };

Reference: site src/components/sections/page-heroes/free-tools-hero-story.tsx.