Repository navigation
21st demos that sell each component #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
radiumcoders
wants to merge
14
commits into
main
Choose a base branch
from
21st-demos
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
24758d7
Add a bundler for publishing components to 21st.dev
radiumcoders 83e7f2a
Let Dither Scan hold the scan line mid-sweep under reduced motion
radiumcoders c201e84
Add the 21st demo for Dither Scan
radiumcoders 1fd77ea
Add the 21st demo for Iso Lattice
radiumcoders 521043b
Add peek to Stretchy Footer so the aurora can show before any scroll
radiumcoders ffc254c
Add the 21st demo for Stretchy Footer
radiumcoders 7d3af0a
Add the 21st demo for Shader Anime Fire
radiumcoders 1386789
Add the 21st demo for Shader Sky
radiumcoders 8bd5b7d
Lint 21st demos as standalone pages
radiumcoders c1e896f
Start Snake 404 mid-meal under reduced motion and enlarge its score
radiumcoders ba51fcb
Add the 21st demo for Snake 404
radiumcoders f200522
Add defaultShortcut to Mech Keyboard to seed the recorder
radiumcoders 10236ad
Add the 21st demo for Mech Keyboard
radiumcoders ed82090
Add the 21st demo for Prompt Box
radiumcoders File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| "use client" | ||
|
|
||
| import { useState } from "react" | ||
| import { RotateCcw } from "lucide-react" | ||
|
|
||
| import { DitherScan } from "./dither-scan" | ||
|
|
||
| const settings = { | ||
| passes: 4, | ||
| duration: 1400, | ||
| pixelSize: 6, | ||
| blur: 6, | ||
| angle: 45, | ||
| alternate: true, | ||
| loop: true, | ||
| } | ||
|
|
||
| const W = 1500 | ||
| const H = 1000 | ||
|
|
||
| /** A seeded random stream, so the scene is the same on every load. */ | ||
| function seeded(seed: number) { | ||
| let s = seed | ||
| return () => { | ||
| s = (s * 16807) % 2147483647 | ||
| return s / 2147483647 | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A mountain ridge: a valley around `center` (0–1 across) that climbs | ||
| * `depth` px toward the sides, roughened by midpoint displacement. | ||
| */ | ||
| function ridge( | ||
| seed: number, | ||
| base: number, | ||
| depth: number, | ||
| amp: number, | ||
| center = 0.5 | ||
| ) { | ||
| const rand = seeded(seed) | ||
| const n = 128 | ||
| const noise = new Array<number>(n + 1).fill(0) | ||
| const split = (a: number, b: number, h: number) => { | ||
| if (b - a < 2) return | ||
| const m = (a + b) >> 1 | ||
| noise[m] = (noise[a]! + noise[b]!) / 2 + (rand() - 0.5) * h | ||
| split(a, m, h * 0.6) | ||
| split(m, b, h * 0.6) | ||
| } | ||
| noise[0] = (rand() - 0.5) * amp | ||
| noise[n] = (rand() - 0.5) * amp | ||
| split(0, n, amp) | ||
| const pts = noise.map((v, i) => { | ||
| const t = i / n | ||
| const side = Math.min( | ||
| 1, | ||
| Math.abs(t - center) / Math.max(center, 1 - center) | ||
| ) | ||
| const y = base - depth * side ** 1.7 + v | ||
| return `${(t * W).toFixed(1)},${y.toFixed(1)}` | ||
| }) | ||
| return `M0,${H} L${pts.join(" L")} L${W},${H} Z` | ||
| } | ||
|
|
||
| /** A stand of pines between `from` and `to`, tallest at the outer edge. */ | ||
| function pines(seed: number, from: number, to: number, y: number, h: number) { | ||
| const rand = seeded(seed) | ||
| let d = "" | ||
| const count = 16 | ||
| for (let i = 0; i < count; i++) { | ||
| const t = i / (count - 1) | ||
| const x = from + t * (to - from) + (rand() - 0.5) * 16 | ||
| const th = h * (1 - 0.55 * t) * (0.75 + rand() * 0.4) | ||
| const w = th * 0.2 | ||
| const b = y + rand() * 10 | ||
| const top = b - th | ||
| // Three tiers, so each tree reads as a pine, not a spike. | ||
| for (let k = 0; k < 3; k++) { | ||
| const ty = top + (th * k) / 4 | ||
| const by = top + (th * (k + 2)) / 4 | ||
| const bw = w * (0.6 + k * 0.25) | ||
| d += `M${x.toFixed(1)},${ty.toFixed(1)} L${(x + bw).toFixed(1)},${by.toFixed(1)} L${(x - bw).toFixed(1)},${by.toFixed(1)} Z ` | ||
| } | ||
| d += `M${(x - 3).toFixed(1)},${(b - th / 4).toFixed(1)} h6 V${(b + 4).toFixed(1)} h-6 Z ` | ||
| } | ||
| return d | ||
| } | ||
|
|
||
| const LAYERS = [ | ||
| { d: ridge(11, 640, 300, 190, 0.5), top: "#EFA3A3", bottom: "#F8C6A9" }, | ||
| { d: ridge(23, 700, 230, 150, 0.36), top: "#BE6489", bottom: "#DE8D93" }, | ||
| { d: ridge(37, 745, 170, 110, 0.64), top: "#7E3E75", bottom: "#A85A82" }, | ||
| { d: ridge(41, 780, 100, 60, 0.5), top: "#46244F", bottom: "#6B3567" }, | ||
| ] | ||
|
|
||
| const starRand = seeded(5) | ||
| const STARS = Array.from({ length: 70 }, () => { | ||
| const rand = starRand | ||
| const x = rand() * W | ||
| const y = rand() * 330 | ||
| const r = 0.8 + rand() * 1.6 | ||
| return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${r.toFixed(2)}" opacity="${(0.3 + rand() * 0.6).toFixed(2)}"/>` | ||
| }).join("") | ||
|
|
||
| const GLARE = Array.from({ length: 16 }, (_, i) => { | ||
| const w = 250 - i * 12 + ((i * 37) % 34) | ||
| const x = 750 - w / 2 + ((i * 53) % 23) - 11 | ||
| return `<rect x="${x}" y="${792 + i * 11}" width="${w}" height="4" rx="2"/>` | ||
| }).join("") | ||
|
|
||
| const SCENE = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}"> | ||
| <defs> | ||
| <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> | ||
| <stop offset="0" stop-color="#17153F"/><stop offset="0.24" stop-color="#4A2580"/> | ||
| <stop offset="0.44" stop-color="#B23C7E"/><stop offset="0.58" stop-color="#F0704C"/> | ||
| <stop offset="0.7" stop-color="#FDBE6C"/></linearGradient> | ||
| <radialGradient id="halo" cx="0.5" cy="0.55" r="0.42"> | ||
| <stop offset="0" stop-color="#FFF3CC" stop-opacity="0.9"/><stop offset="0.25" stop-color="#FFC46A" stop-opacity="0.45"/> | ||
| <stop offset="1" stop-color="#F0704C" stop-opacity="0"/></radialGradient> | ||
| <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"> | ||
| <stop offset="0" stop-color="#FFFCEB"/><stop offset="1" stop-color="#FFD584"/></linearGradient> | ||
| ${LAYERS.map((l, i) => `<linearGradient id="m${i}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${l.top}"/><stop offset="1" stop-color="${l.bottom}"/></linearGradient>`).join("")} | ||
| <linearGradient id="lake" x1="0" y1="0" x2="0" y2="1"> | ||
| <stop offset="0" stop-color="#F6A487"/><stop offset="0.35" stop-color="#9A4A78"/><stop offset="1" stop-color="#1C1136"/></linearGradient> | ||
| <linearGradient id="glare" x1="0" y1="0" x2="0" y2="1"> | ||
| <stop offset="0" stop-color="#FFF0BF"/><stop offset="1" stop-color="#FFB46B" stop-opacity="0"/></linearGradient> | ||
| </defs> | ||
| <rect width="${W}" height="${H}" fill="url(#sky)"/> | ||
| <g fill="#FFFFFF">${STARS}</g> | ||
| <rect width="${W}" height="${H}" fill="url(#halo)"/> | ||
| <g fill="#FFD7C4" opacity="0.5"> | ||
| <ellipse cx="290" cy="330" rx="240" ry="15"/><ellipse cx="420" cy="360" rx="150" ry="9"/> | ||
| <ellipse cx="1200" cy="290" rx="250" ry="14"/><ellipse cx="1080" cy="322" rx="140" ry="8"/></g> | ||
| <circle cx="750" cy="560" r="105" fill="url(#sun)"/> | ||
| ${LAYERS.map((l, i) => `<path d="${l.d}" fill="url(#m${i})"/>`).join("")} | ||
| <rect y="790" width="${W}" height="${H - 790}" fill="url(#lake)"/> | ||
| <g fill="url(#glare)">${GLARE}</g> | ||
| <path d="${ridge(59, 990, 80, 20, 0.5)}" fill="#140B24"/> | ||
| <path d="${pines(7, -10, 340, 985, 260)}" fill="#140B24"/> | ||
| <path d="${pines(19, 1510, 1170, 985, 240)}" fill="#140B24"/> | ||
| <g fill="none" stroke="#3A1E52" stroke-width="3" stroke-linecap="round"> | ||
| <path d="M520,420 q12,-10 24,0 q12,-10 24,0"/><path d="M585,392 q9,-8 18,0 q9,-8 18,0"/><path d="M478,450 q7,-6 14,0 q7,-6 14,0"/></g> | ||
| </svg>` | ||
|
|
||
| const SRC = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SCENE)}` | ||
|
|
||
| export default function DitherScanDemo(props: Partial<typeof settings>) { | ||
| const s = { ...settings, ...props } | ||
| const [replayKey, setReplayKey] = useState(0) | ||
|
|
||
| return ( | ||
| <div className="relative flex min-h-screen w-full flex-col items-center justify-center overflow-hidden bg-background px-4 py-8 sm:px-10"> | ||
| <div | ||
| className="w-full" | ||
| style={{ maxWidth: "min(1120px, calc((100vh - 7rem) * 1.5))" }} | ||
| > | ||
| <DitherScan | ||
| src={SRC} | ||
| alt="Layered mountains over a lake at sunset" | ||
| passes={s.passes} | ||
| duration={s.duration} | ||
| pixelSize={s.pixelSize} | ||
| blur={s.blur} | ||
| angle={s.angle} | ||
| alternate={s.alternate} | ||
| loop={s.loop} | ||
| reducedMotionProgress={0.5} | ||
| replayKey={replayKey} | ||
| className="rounded-2xl shadow-[0_30px_80px_-30px_rgb(0_0_0/0.45)]" | ||
| /> | ||
| <div className="mt-3 flex justify-end"> | ||
| <button | ||
| type="button" | ||
| onClick={() => setReplayKey((key) => key + 1)} | ||
| className="inline-flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" | ||
| > | ||
| <RotateCcw className="size-3.5" aria-hidden /> | ||
| Replay | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| "use client" | ||
|
|
||
| import { IsoLattice, LIGHT_COLOR, type IsoLatticeShape } from "./iso-lattice" | ||
|
|
||
| const settings = { | ||
| color: "#C2560C", | ||
| speed: 1, | ||
| size: 0.8, | ||
| glow: 0.6, | ||
| pillars: true, | ||
| steps: true, | ||
| cube: true, | ||
| coil: true, | ||
| } | ||
|
|
||
| // The bar chart leads: it is the figure that holds under reduced motion. | ||
| const ORDER: IsoLatticeShape[] = ["pillars", "steps", "cube", "coil"] | ||
|
|
||
| export default function IsoLatticeDemo(props: Partial<typeof settings>) { | ||
| const s = { ...settings, ...props } | ||
| const on = { pillars: s.pillars, steps: s.steps, cube: s.cube, coil: s.coil } | ||
| const picked = ORDER.filter((shape) => on[shape]) | ||
| const shapes = picked.length ? picked : ORDER | ||
|
|
||
| // The stock orange follows the theme (burnt on light, amber on dark). | ||
| const color = | ||
| s.color.trim().toUpperCase() === LIGHT_COLOR ? undefined : s.color | ||
|
|
||
| return ( | ||
| <div className="relative min-h-screen w-full overflow-hidden bg-background"> | ||
| <IsoLattice | ||
| shapes={shapes} | ||
| color={color} | ||
| speed={s.speed} | ||
| size={s.size} | ||
| glow={s.glow} | ||
| /> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| "use client" | ||
|
|
||
| import { useEffect, useState } from "react" | ||
|
|
||
| import { MechKeyboard } from "./mech-keyboard" | ||
|
|
||
| const SHORTCUT = ["MetaLeft", "KeyK"] | ||
|
|
||
| const settings = { | ||
| scale: 1, | ||
| platform: "mac", | ||
| monochrome: false, | ||
| recorder: true, | ||
| hint: true, | ||
| interactive: true, | ||
| sound: false, | ||
| } | ||
|
|
||
| /** On wide stages the board fits by height; grow it into the spare width. */ | ||
| function useWideBoost() { | ||
| const [boost, setBoost] = useState(1) | ||
| useEffect(() => { | ||
| const measure = () => { | ||
| const aspect = window.innerWidth / Math.max(1, window.innerHeight) | ||
| setBoost(Math.min(1.2, Math.max(1, aspect / 1.45))) | ||
| } | ||
| measure() | ||
| window.addEventListener("resize", measure) | ||
| return () => window.removeEventListener("resize", measure) | ||
| }, []) | ||
| return boost | ||
| } | ||
|
|
||
| export default function MechKeyboardDemo(props: Partial<typeof settings>) { | ||
| const s = { ...settings, ...props } | ||
| const boost = useWideBoost() | ||
|
|
||
| return ( | ||
| <main className="relative h-screen min-h-[420px] w-full overflow-hidden bg-background"> | ||
| <MechKeyboard | ||
| scale={s.scale * boost} | ||
| platform={s.platform === "pc" ? "pc" : "mac"} | ||
| monochrome={s.monochrome} | ||
| recorder={s.recorder} | ||
| defaultShortcut={SHORTCUT} | ||
| highlight={s.hint ? SHORTCUT : undefined} | ||
| interactive={s.interactive} | ||
| sound={s.sound} | ||
| /> | ||
| </main> | ||
| ) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: radiumcoders/23rd.dev
Length of output: 9908
🏁 Script executed:
Repository: radiumcoders/23rd.dev
Length of output: 41161
🌐 Web query:
21st.dev CLI component demo external npm dependencies import lucide-react preview dependency metadata💡 Result:
Declare
lucide-reactfor the 21st demo.The 21st publish build validates dependencies and builds both the component and demo.
21st/dither-scan.demo.tsx:4importslucide-react, but the nearestpackage.jsondoes not declare it. The bundler copies that import unchanged and emits no dependency metadata. The 21st build can therefore fail to resolve the demo import.Suggested fix
"clsx": "^2.1.1", + "lucide-react": "^1.27.0", "fumadocs-core": "16.12.1",🤖 Prompt for AI Agents