# Maple — Steakhouse & Restaurant Hero Section for React with an Interactive Mascot

> Steakhouse & restaurant hero section for React and Next.js. Maple, a filmed cartoon cow in a knitted scarf, follows your guest's cursor across six photographed steaks, smiles at their pick and has her say. Brass doneness probe, kitchen ticket, five palettes in light and dark.

- Kind: Component (Sliders)
- Price: comes with the Pro plan
- License: UpFork Standard License. Use it in unlimited personal and commercial projects. Don't resell or redistribute it on its own.
- Developer: UpFork
- Tags: slider, cursor, hero, restaurant, steakhouse, hero-section, interactive-mascot, cursor-follow, webgl, food-menu, nextjs
- npm packages: motion
- Published: 2026-10-22
- Updated: 2026-10-22
- Page: https://upfork.dev/products/restaurant-hero-section-react

## How to get it

Install it into a React + Tailwind project with the shadcn CLI:

```bash
npx shadcn@latest add @upfork/restaurant-hero-section-react
```

It installs with the member's personal CLI token (UPFORK_TOKEN), made on their UpFork account page. It comes with UpFork's Pro plan and the plans after it.

## About

A hero section for a premium steakhouse. **Maple**, a cartoon Hereford cow in a chunky mustard scarf, stands at the
right end of the counter and watches the board of steaks:

- she looks down at the cut the guest points at, and follows the pointer along the plates;
- she keeps watching the guest's plate while they work the doneness probe;
- she lifts her head into a big smile when they pick a cut or commit a doneness, or look back up at the headline;
- she has opinions, set like a quotation beside her head with a brass leader to her cheek: *"Hi, I'm Maple. Don't
  worry, I'm the critic, not the special."*

Around her: a doneness card with a cross-section lens, a brass probe rail and a kitchen ticket that prints out of a
slot; six plates on a walnut counter with a brass rail; a live open/closed pill in New York time; five palettes, light
and dark, that cross-fade (the scarf is recoloured live in WebGL).

The restaurant, **Copperhorn**, is invented. Every brand fact, price, count, address and phone number is marked
`// PLACEHOLDER — replace with real data` in `src/components/maple-hero/hero-settings.ts`.

## Run it

```bash
pnpm install
pnpm dev          # http://localhost:6101
pnpm build && pnpm start   # http://localhost:6100
```

`pnpm lint`, `pnpm typecheck`, `pnpm test` (vitest), `pnpm build`.

Demo routes: `/` (Red Banquette, with the palette switcher), `/butcher` (Butcher Paper, fixed), `/auto` (follows the
system or a `.dark` class). Debug: `?debug` (frame times and state on `window.__maple`), `?debug=mask` (shows the
scarf mask), `?gl=0` (Canvas 2D fallback), `?tier=mobile` / `?tier=desktop`, `?sharp=0.6` and `?rim=0` (to compare the
sharpening and the rim light).

## Use it

```tsx
import { MapleHero } from "@/components/maple-hero/MapleHero";
import { SteakhouseJsonLd } from "@/components/maple-hero/SteakhouseJsonLd";
import { heroSettings } from "@/components/maple-hero/hero-settings";

<SteakhouseJsonLd settings={heroSettings} url="https://example.com" />
<MapleHero palette="banquette" units="F" onReserve={({ cut, doneness }) => openBooking(cut, doneness)} />
```

| Prop | |
| --- | --- |
| `palette` | `"banquette"` (default), `"butcher"`, `"tribeca"`, `"gramercy"`, `"hudson"`, or `"auto"` (dark → Red Banquette, light → Butcher Paper; a `.dark` / `.light` class or `data-theme` on `<html>` wins over the system setting) |
| `settings` | everything it says and shows (`hero-settings.ts`) |
| `units` | `"F"` or `"C"` to start with |
| `onReserve` | called by "Book a table" and "Reserve this cut"; when given, those links do not navigate |

The component is self-contained: plain CSS (`maple-hero.css`, every name prefixed `mh-`), its own fonts in `fonts/`
(Fraunces, Inter Tight, JetBrains Mono, self-hosted), `motion` for UI animation. Maple's frames and posters live in
`public/maple/` and are named by the generated `maple-manifest.ts`.

## Re-skin it

All copy, the menu, the doneness table, Maple's lines, the palettes, the gaze calibration and every timing are in
`hero-settings.ts`. Nothing is hard-coded in the components.

- **Palettes**: each has a background pair, ink, muted ink, accent, card colours, glow, shadow, vignette, focus, a rim
  light for Maple and a scarf colour (`null` keeps the native mustard). Colours become `--mh-*` tokens; registered
  colour properties make a switch cross-fade over 600 ms. If you add a scarf colour, run `pnpm maple:frames` so the
  posters (shown before WebGL starts, and with reduced motion) carry it too.
- **Cuts**: name, weights, price, aging badge, grade, tasting note, chef's doneness, the ticket's lines, how many are
  left tonight.
- **Hours**: per weekday, in `brand.timeZone`; the status pill says "Open now · last seating 10:30 PM" or "Opens at
  5 PM".

## How Maple moves

An AI-generated video (1280×720, 24 fps, 361 frames) is turned into keyed, recolourable frames by
`pnpm maple:frames` (`scripts/maple-frames/`):

1. **Extract** every frame with ffmpeg-static (decoded with the HD matrix, as browsers do).
1b. **Restore** (optional, how the shipped frames were made): the video is 720p at 1.9 Mbps, so her fur and the knit
   came out smeared. `scripts/maple-frames/restore.mjs` runs Real-ESRGAN on the region around her in every frame the
   clips use (realesr-general-x4v3 blended half and half with its light-clean-up sibling, 4× up, then Lanczos back to
   the source size) and `pnpm maple:frames -- --frames <that folder>` builds from it. Same size, same weight, much
   crisper; about 45 min on a laptop CPU (Python with torch, spandrel and Pillow; the weights from
   github.com/xinntao/Real-ESRGAN/releases).
2. **Key** the flat background (`keyer.mjs`, unit-tested): a flood fill from the border through clear background,
   enclosed holes between the legs, the baked contact shadow grown away from the background in the floor zone, a
   soft edge, colour decontamination that leans on the fur inside and caps the render's baked rim light, speck
   removal, and a temporal median on still edges.
3. **Measure**: the union crop, the background colour, the nose track (by hue), the junction frame.
4. **Thin** each clip by equal motion (cumulative frame difference), so scrubbing feels even.
5. **Pack** frames coarse to fine into WebP atlases (lossy with alpha) for a desktop and a mobile tier, and bake
   posters per scarf colour. Write `src/components/maple-hero/maple-manifest.ts`.

`--review` writes contact sheets, face sheets (blink check), 400% crops and the scarf mask over every palette to
`.review/maple-frames/`. Keyed frames are cached in the temp folder, keyed by the video and the keyer's source.

| Clip | Source frames | Use |
| --- | --- | --- |
| `lower` | 31 → 91 | neutral → looking down at the board (intro) |
| `axis` | 150 → 229 | the gaze axis, far left → slightly right, scrubbed |
| `rise` | 229 → 301 | lifts the head into the smile; backwards it closes the smile |
| posters | 18, 320 | neutral (LCP image), smile (resting and reduced motion) |

At runtime (`src/components/maple-hero/engine/`, framework-free TypeScript):

- `frame-store.ts` picks the tier, fetches atlases (axis, rise and lower coarse passes first, four at a time, after
  load and in idle time) and keeps a device-sized budget of them decoded (160 / 96 / 48 MB), releasing those off
  Maple's route first; until a frame is in, the nearest decoded one stands in. The intro's lowering is let go once
  it has played (`retire`).
- `controller.ts` is the clip graph (idle, looking, smiling, transit). After the intro she lives on one track: the
  gaze axis and the lift into the smile end to end. One critically damped spring with a top acceleration moves her
  along it, so a look, a smile and the way back down start and end gently and pass from the turn into the lift
  without stopping. The intro and approvals (a picked cut, a committed doneness) are scripts; input priority:
  scripts, then a plate pointed at or focused, then the pointer over the board, then a smile after 450 ms above it.
- `gaze.ts` maps the angle from her muzzle to the pointer onto the axis through the measured yaw table.
- `renderer-gl.ts` (WebGL2) blends two exact neighbouring frames (each alone for the first and last quarter of the
  way, a short crossfade between, so a turn never reads as a double image), sharpens them a little (contrast-adaptive,
  never on the silhouette; `render.sharpness`, for screens at 125–150% that draw her larger than she is), recolours
  the scarf (HSV mask plus a neighbour rule for its edges, keeping the knit's shading) and adds a rim light; `renderer-2d.ts` is the fallback **without the
  recolour and rim** (every palette keeps the native mustard scarf there).
- `runtime.ts` runs one `requestAnimationFrame` loop, paused off-screen, in hidden tabs and while she rests (woken by
  any input), and redraws only when the frame, the blend, the palette or the size changed. It also keeps her speech
  beside her head, on whichever side has room (or above it on phones), clear of the copy column. No React state
  changes per frame or per pointer move.

## The steak photography

Plates and doneness slices are photographs (see `assets/photos/README.md`), all twelve in: the plates shot at the
board's 45°, the slices cut from a doneness grid and lined up.

```bash
node scripts/photos/generate.mjs   # makes the missing ones with the GapGPT tools (plates as edits of a template)
node scripts/photos/prompts.mjs    # or prints the prompts, to make them elsewhere
pnpm photos:build                  # cuts out and exports them; writes photo-manifest.ts
```

A plate's cut-out is its gold rim: the key finds the plate, a circle is fitted to its outline and then moved onto
the gold line as an ellipse (`findRim`, `ellipseFrom` in `keyer.mjs`), because a white plate in shade keys away with a
grey background and its soft shadow clings to the rim. Until a photo exists, the hero draws a stand-in in SVG
(`parts/PlateArt.tsx`, `parts/SliceArt.tsx`).

## Accessibility and fallbacks

- A real `<h1>`; plates are buttons (`aria-pressed`) in a labelled group, and links to the menu without JavaScript;
  the probe is a `role="slider"` with arrows, Home and End and a spoken value ("Medium rare, 135 degrees
  Fahrenheit"); the ticket is a `role="status"`; Maple's canvas is hidden from assistive tech, with a hidden line in
  its place; her lines go to a polite live region only when the guest caused them.
- Focus rings are SVG strokes on pill shapes (`parts/PillRing.tsx`).
- `prefers-reduced-motion`: no intro, no scrubbing, no embers; the smile poster, her lines still show (beside the
  poster's head), short fades.
- Without JavaScript: the neutral poster, the copy, the board as links to the menu, the buttons.
- The phone layout opens the doneness card in a bottom sheet (Esc, the backdrop or a swipe down closes it).

## Measured (2026-10-21/22, this laptop: Iris Plus, installed Chrome)

- Frames: desktop 4.2 MB in 12 atlases (468×716 a frame, every one of the axis's 80, restored with Real-ESRGAN),
  mobile 1.6 MB in 7 atlases; posters 39 KB / 26 KB. Decoded on the GPU after the intro: 142 MB (the intro's
  frames are let go).
- Fonts: 289 KB for all four (Fraunces and Inter Tight cut to the weights and softness the hero uses, unused
  OpenType features dropped; `scripts/fonts-trim.py`); 348 KB before.
- Pacing while sweeping the pointer over the board for 10 s: p50 16.7 ms, p95 17.0 ms, max 17.3 ms, no React commits
  (an occasional single 33 ms frame, about one run in six, with no part of the page to blame).
- At rest, the loop sleeps: script time 17 ms in 6 s (52 ms when it ran every frame).
- JavaScript: 32.4 KB gzipped eager for the hero (React and Next excluded); motion's animation features (45.8 KB) and
  the frame engine (10.4 KB) load after first paint.
- LCP on fast 4G (9 Mbps, 150 ms): 1.6–2.0 s on desktop, about 1.1 s on a phone; the poster is the LCP element.
  CLS under 0.001 (fallback faces matched to the web fonts' metrics).
- axe: 0 violations on every palette and on the open phone sheet; rendered-pixel text contrast ≥ 4.5:1 everywhere.
- UpFork: the site's importer takes `registry/` (38 files, 2 demos, 63 assets, 7.1 MB, no warnings) and its builder
  makes the preview (`ok`, 596 kB).
- Doneness slices: 22–24 KB each (360 px WebP), loaded lazily with the card.

## Decisions that differ from the brief (measured on the real frames)

- **Scarf mask.** The brief's band (hue 38–56°, S ≥ 0.42) missed most of the knit, which sits at hue 27–45°. The mask
  runs 26–48°, asks for scarf-level saturation and value below 30–35° (the irises, horns and cream belly live
  there), and takes in blended edge pixels next to scarf so no mustard outline is left.
- **Nose track.** The brief's RGB pink test also caught the cream face in warm light, so the nose is found by hue
  (4–23.5°, S 0.22–0.5, V ≥ 0.72). Neutral muzzle: (623, 190) source px; the axis sweeps it 513 → 701 px. The yaw
  table is an isotonic fit of that track.
- **Flood fill.** An unlimited d < 22 flood wandered into shaded white fur; it runs through clear background (d < 10)
  and only 3 px further through the soft edge.
- **Edges.** The render has a rim light baked into the fur's outline (invisible on its grey background, a pale halo on
  dark palettes): edge colours lean on the fur inside and may not be brighter than it by more than 36 levels.
- **Shadow zone** 95.5% of her height (94% reached the fetlock fur).
- **Tribeca accent** #F07A45 and **Gramercy accent** #243B2A, and **Medium-well rail** #A47A6A, for contrast and for a
  rail gradient without a lightness plateau.
- **Gaze calibration** −76° … +14° (the brief's −62°, and −68° once the plates sat on the counter at 45°, left
  the two leftmost plates on the same frame in a short, wide hero such as 1519×629).
- **Plates at 45°, not from above.** The brief's top-down plates hid a steak's thickness and floated like medallions;
  at 45° they sit on a counter top seen in perspective (the deck), Maple stands on it, and hover lifts a plate off
  it. The photographs are shot at 45° too, and every plate is placed by its rim (`photo-manifest.ts`).
- **Short desktops.** Under 880 px of height the doneness card stands beside the headline, at the foot of the
  column, with the ticket on one line (the brief folded it under 722 px), so her speech has the room above the card;
  under 800 px "left tonight" shows on the guest's own plate only.
- **Her speech is a quotation, not a bubble.** A cream bubble over the stage covered her face; her lines are set
  beside her head like a pull quote (brass quote mark, italic line, her name) with a brass leader drawn from her
  cheek, never over her. The plates' tasting notes moved from a floating card over the plates (it hid the embers) to
  the board's caption line.
- **Picking a cut** makes her lift her head and smile too (the brief smiled only on a doneness), holding the smile
  while her line types.
- **The hooves.** The floor-shadow cut grew into a hoof's outer toe, as light a grey as the shadow: it now stops at
  a hard edge (a shadow fades, a hoof has an outline).
- **Ports** 6100/6101: Chrome refuses port 6000.
