Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d0350bf
Paint the Gooey Color Picker with theme tokens instead of black.
radiumcoders Oct 11, 2026
1da9ce3
Give Radiant Lines a light palette and let it follow the theme.
radiumcoders Oct 11, 2026
86197b1
Let Live Orb pick its material from the theme.
radiumcoders Oct 11, 2026
1bc6386
Format the picker and the Radiant Lines demo.
radiumcoders Oct 11, 2026
03f41e8
Rebuild ASCII Logo: transparent stage, theme ink, supersampled edges,…
radiumcoders Oct 11, 2026
387ab5a
Declare ASCII Logo's font and assemble props in Svelte and only fly i…
radiumcoders Oct 11, 2026
f3eaeb5
Add Iso Lattice.
radiumcoders Oct 14, 2026
b4d9911
Keep Iso Lattice tiles inside the figure's silhouette.
radiumcoders Oct 14, 2026
f01ceae
Cap Ring Tower at five rings and let each ring be set on its own.
radiumcoders Oct 14, 2026
f537c47
Put component previews on a full-height stage with the props beside it.
radiumcoders Oct 14, 2026
2d2598a
Move Phosphor Score and Typewriter into a new Experiments section.
radiumcoders Oct 14, 2026
fe174ab
Redesign the docs sidebar around a sheet-tone chip for the open page.
radiumcoders Oct 14, 2026
5633f03
Let the page scroll on once Folio's sheet reaches its end.
radiumcoders Oct 14, 2026
5dde689
Let the page scroll from over the props panel.
radiumcoders Oct 14, 2026
1a909fe
Line the sidebar header up with the top of the stage.
radiumcoders Oct 14, 2026
f5531c1
Put the sidebar toggle at the right of the header, across from the wo…
radiumcoders Oct 14, 2026
05ef491
Fold the docs sidebar away completely, with a sliding open and close.
radiumcoders Oct 14, 2026
e0fa388
Ease the docs sidebar's width, holding the live preview still until i…
radiumcoders Oct 14, 2026
02f0e39
Redraw canvas components the moment they resize.
radiumcoders Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .cursor/skills/23rd/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ flowchart TD
atmo -->|Screenshots on a spinning tower| rt[ring-tower]
atmo -->|Hyperspace streaks, scroll warp| rl[radiant-lines]
atmo -->|Hair-line explosion| lb[logo-burst]
atmo -->|Isometric figures built from tiles| il[iso-lattice]
atmo -->|Falling sheet music| ps[phosphor-score]
foot -->|Rubber overscroll aurora| st[stretchy-footer]
mark -->|Eyes that track the pointer| orb[live-orb]
Expand All @@ -119,6 +120,7 @@ Disambiguation that agents get wrong:
| Page leans while scrolling | `folio` | `stretchy-footer` (that one stretches, it does not tilt) |
| Stars that speed up when you scroll | `radiant-lines` | `logo-burst` |
| Logo explodes into lines | `logo-burst` | `ascii-logo` |
| Isometric cube, stairs, bar chart, pixel-tile figure | `iso-lattice` | `logo-burst` (lines, not a figure) |
| ASCII letters that shove, scatter, and fall | `ascii-logo` | `ascii-fluid` (trails, not a wordmark) |
| CRT notation, phosphor, staves | `phosphor-score` | `shader-sky` |
| Chrome, liquid metal, gold ribbons | `shader-metal` | `shader-gradient` (a color wash, not a material) |
Expand All @@ -139,7 +141,7 @@ Backgrounds and shaders fill the parent. They do not create a page.
</section>
```

Same shell for `ShaderAnimeFire`, `ShaderFire`, `ShaderSky`, `AsciiFluid`, `LogoBurst`, `PhosphorScore`, and `Dithered404`. Give the parent a height. Put UI in a later stacking context (`relative z-10`).
Same shell for `ShaderAnimeFire`, `ShaderFire`, `ShaderSky`, `AsciiFluid`, `LogoBurst`, `PhosphorScore`, `IsoLattice`, and `Dithered404`. Give the parent a height. Put UI in a later stacking context (`relative z-10`).

`ShaderMetal` uses the same shell but is transparent: the ribbons and their shadows sit on the parent's background, so set one (`bg-background`). The ribbons cross the middle; put copy top-left.

Expand Down Expand Up @@ -181,6 +183,7 @@ Categories match `content/docs/components/meta.json`.
| `phosphor-score` | Background | Vertical CRT score; notes fall, bloom, flare | React + Svelte |
| `radiant-lines` | Background | Hyperspace streaks; warp follows scroll | React + Svelte |
| `ascii-fluid` | Background | Pointer trails quantized to an ASCII brightness ramp | React + Svelte |
| `iso-lattice` | Background | Isometric tiles build solid figures from the floor up | React + Svelte |
| `ring-tower` | Background | three.js tower of tilted image bands; drag to spin, neighbors counter-turn | React + Svelte |
| `shader-gradient` | Shaders | Quiet WebGL wash behind heroes and empty states | React + Svelte |
| `shader-anime-fire` | Shaders | Cel-shaded flames licking up from the bottom | React + Svelte |
Expand Down
44 changes: 37 additions & 7 deletions .cursor/skills/23rd/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,31 @@ Constants: `LIGHT_COLOR` `#3F3F46`, `DARK_COLOR` `#D6D2CA`. `DEFAULT_COLOR` is d

Vanilla `LogoBurstInstance`: `{ setOptions, replay, destroy }`. Not returned to app code by the React component.

## iso-lattice

```ts
type IsoLatticeTheme = "light" | "dark" | "auto"
type IsoLatticeShape = "cube" | "steps" | "pillars" | "coil"

type IsoLatticeProps = {
shapes?: IsoLatticeShape[] // ["cube", "steps", "pillars", "coil"]; one item holds still
interval?: number // 4 seconds per figure
color?: string
size?: number // 0.6 of the shorter side, 0.2–1
spacing?: number // 8 CSS px between tiles, 4–24
glow?: number // 0.6, 0–1
speed?: number // 1, glint and shimmer only
floor?: boolean // true
interactive?: boolean // true
theme?: IsoLatticeTheme // "auto"
className?: string
}
```

Constants: `LIGHT_COLOR` `#C2560C`, `DARK_COLOR` `#FFB25B`, `SHAPES`. Each build takes a fixed 1.6 s; it is not a prop.

Vanilla `IsoLatticeInstance`: `{ setOptions, destroy }`. Not returned to app code by the React component.

## phosphor-score

```ts
Expand All @@ -92,15 +117,16 @@ type PhosphorScoreProps = {

```ts
type RadiantLinesProps = {
colors?: string[]
colors?: string[] // omit → theme palette
theme?: "light" | "dark" | "auto" // "auto"
starCount?: number // 420
displacement?: number // 1, values < 0 clamp to 0
containerRef?: RefObject<HTMLElement | null> // omit → window
className?: string
}
```

`DEFAULT_COLORS`: `#FF6B4A`, `#2DD4BF`, `#FBBF24`, `#60A5FA`, `#F472B6`, `#A3E635`, `#94A3B8`.
`DEFAULT_COLORS` (dark pages): `#FF6B4A`, `#2DD4BF`, `#FBBF24`, `#60A5FA`, `#F472B6`, `#A3E635`, `#94A3B8`. `LIGHT_COLORS` (light pages): the same hues, deeper. A set `colors` wins in both themes.

Instance: `{ setOptions, destroy }`.

Expand Down Expand Up @@ -289,7 +315,8 @@ type StretchyFooterPlayDetail = {
type LiveOrbVariant = "white" | "black" | "webgl" | "custom"

type LiveOrbProps = {
variant?: LiveOrbVariant // "white"
variant?: LiveOrbVariant // omit → "white" on dark pages, "black" on light
theme?: "light" | "dark" | "auto" // "auto"
color?: string // custom body, default #7C5CFF
eyeColor?: string // custom eyes, default #FAFAFA
colors?: string[] // webgl stops
Expand All @@ -315,11 +342,14 @@ type AsciiLogoProps = {
fit?: number // 0.82
cellSize?: number // 11
cellGap?: number // 2
charset?: string // DEFAULT_CHARSET
threshold?: number // 0.2
charset?: string // DEFAULT_CHARSET, cells inside the shape
edgeCharset?: string // EDGE_CHARSET, light marks on the rim
threshold?: number // 0.2, coverage a cell needs
invert?: boolean // true if src set, else false
color?: string
backgroundColor?: string // "transparent" skips fill
color?: string // omit → LIGHT_INK / DARK_INK by theme
backgroundColor?: string // "transparent"
font?: string // omit → root's computed font; wrapper sets font-mono
assemble?: boolean // true, glyphs fly in on build
hoverRadius?: number // 7
hoverPush?: number // 2.6
hoverEase?: number // 0.18
Expand Down
36 changes: 32 additions & 4 deletions .cursor/skills/23rd/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ export function Mark() {

Pitfalls: `color` overrides theme ink. Light filament is `#3F3F46`, dark is `#D6D2CA`. `DEFAULT_COLOR` is deprecated (it is the dark color). Reduced motion draws the settled field and skips the explosion. Click replays unless `replayOnClick={false}`.

## iso-lattice

Background. A fixed lattice of isometric diamond tiles lights up solid figures (cube, steps, pillars, coil) on a faint floor grid, and builds each one over the last from the floor up. A level glint rises through the figure, the pointer lights the lattice like a lamp. Transparent canvas.

Best fit: an infrastructure or developer-tool hero that wants one luminous object under the headline; a section divider with a figure that changes.

Not this: hair-line explosion (`logo-burst`), scroll starfield (`radiant-lines`), images on a tower (`ring-tower`).

Deps: none (WebGL). Key props: `shapes` (`["cube", "steps", "pillars", "coil"]`), `interval` (4s), `color`, `theme` (`auto`), `size` (0.6), `spacing` (8px), `glow` (0.6), `speed` (1), `floor` (true), `interactive` (true).

```tsx
"use client"
import { IsoLattice } from "@/components/ui/iso-lattice"

export function Hero() {
return (
<section className="relative isolate flex min-h-svh flex-col overflow-hidden bg-background">
<h1 className="relative z-10 pt-24 text-center text-4xl font-medium">Ship the build</h1>
<div className="relative min-h-[420px] flex-1">
<IsoLattice />
</div>
</section>
)
}
```

Pitfalls: the figure is centered in the canvas, so give the lattice its own box below the copy instead of centering copy over it. `color` overrides the theme amber; light is `#C2560C`, dark `#FFB25B`. Unknown names in `shapes` are dropped; an empty list falls back to all four. Reduced motion holds the first figure with no build, glint or lamp. Without WebGL the canvas stays empty.

## phosphor-score

Background. Two vertical staves. Notes fall, bloom at the playhead, then flare. Not a piano roll.
Expand Down Expand Up @@ -71,7 +99,7 @@ Best fit: a long scrolling page that should feel like hyperspace. Not a static p

Not this: `logo-burst` (one-shot explosion), `shader-gradient` (no scroll coupling).

Deps: none. Key props: `colors`, `starCount` (420), `displacement` (1), `containerRef` / Svelte `container`.
Deps: none. Key props: `colors` (omit it and the palette follows the theme), `theme`, `starCount` (420), `displacement` (1), `containerRef` / Svelte `container`.

```tsx
"use client"
Expand Down Expand Up @@ -288,7 +316,7 @@ Best fit: a mascot, an empty state, a corner presence.

Not this: a full-bleed background. It is `size` pixels square (default 280).

Deps: none. The only `variant` in the registry: `"white"` (default), `"black"`, `"webgl"`, `"custom"`. Also `size`, `color`, `eyeColor`, `colors`, `interactive` (true), `blink` (true).
Deps: none. The only `variant` in the registry: `"white"`, `"black"`, `"webgl"`, `"custom"`. Omit it and the orb follows the theme (white on dark pages, black on light). Also `theme`, `size`, `color`, `eyeColor`, `colors`, `interactive` (true), `blink` (true).

```tsx
"use client"
Expand All @@ -303,13 +331,13 @@ Pitfalls: `color` / `eyeColor` apply only for `variant="custom"` (defaults `#7C5

## ascii-logo

Character. Glyphs sample `text` or `src`. Hover shoves them in the `logo` phase. Clicks cycle `logo → scattered → fallen → returning → logo`.
Character. Glyphs sample `text` or `src` (4x supersampled, so rim cells get light marks and fade). They fly in on build, hover shoves them in the `logo` phase, and clicks cycle `logo → scattered → fallen → returning → logo`. The stage is transparent; ink follows the theme.

Best fit: an interactive wordmark (max 5 letters) or a sampled mark.

Not this: mouse-trail fluid (`ascii-fluid`).

Deps: none. Key props: `text` (`"23rd"`), `src`, `fit` (0.82), `cellSize` (11), `cellGap` (2), `charset`, `threshold` (0.2), `invert`, `color`, `backgroundColor`, hover/scatter/gravity props, `interactive` (true), `theme`, `label`, `onPhaseChange`.
Deps: none. Key props: `text` (`"23rd"`), `src`, `fit` (0.82), `cellSize` (11), `cellGap` (2), `charset`, `edgeCharset`, `threshold` (0.2), `invert`, `color`, `backgroundColor` (transparent), `font`, `assemble` (true), hover/scatter/gravity props, `interactive` (true), `theme`, `label`, `onPhaseChange`.

```tsx
"use client"
Expand Down
2 changes: 2 additions & 0 deletions .cursor/skills/23rd/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ Demos:
- `*-demo.tsx` is for the docs site only. It is not a registry file.
- Demo stages and docs examples use theme tokens (`bg-background`, `from-background`, `text-foreground`), never a hard-coded black or white stage. Check the demo in both themes before shipping.
- Use `ComponentPreview`, `ComponentControls`, and `usePreviewProps` the way `shader-gradient-demo.tsx` does.
- One `ComponentPreview` per page. On a component page it moves into the full-height stage at the top of the docs, and the controls into the props panel beside it. Size the demo's root with `h-[var(--preview-h,56svh)]` so it fills the stage there and keeps 56svh anywhere else.
- Do not put a `## Preview` heading above the demo; the preview is not in the page flow.
- `gooey-color-picker` has no demo file; the MDX renders `<GooeyColorPicker />` inside `ComponentPreview`. Either pattern is acceptable. Do not publish the demo.

## Docs
Expand Down
1 change: 1 addition & 0 deletions .cursor/skills/23rd/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Swap the component, keep the shell:
| ASCII trails | `AsciiFluid` | `interactive` |
| CRT score | `PhosphorScore` | parent `overflow-hidden`; do not expect a `d` key |
| Burst, no mark | `LogoBurst` | no children |
| Glowing isometric figure | `IsoLattice` | lattice in its own box under the copy |
| Burst around a logo | `LogoBurst` | pass the mark as `children` |

Svelte: `import ShaderGradient from "$lib/components/ui/shader-gradient.svelte"` and `class` on the section if you style the component itself.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ npx skills add radiumcoders/23rd.dev --skill 23rd -g
| [Dithered 404](https://23rd.dev/docs/components/dithered-404) | `@23rd/dithered-404` | Bayer-pixel 404; a fireball cursor scorches it into embers and smoke, then it reforms |
| [Folio](https://23rd.dev/docs/components/folio) | `@23rd/folio` | The page leans back in perspective while you scroll, blurs, then springs flat |
| [Gooey Color Picker](https://23rd.dev/docs/components/gooey-color-picker) | `@23rd/gooey-color-picker` | Floating swatch → hue wheel, alpha, hex, joined by an SVG gooey filter |
| [Iso Lattice](https://23rd.dev/docs/components/iso-lattice) | `@23rd/iso-lattice` | Glowing isometric tiles build solid figures, one over the last, from the floor up |
| [Live Orb](https://23rd.dev/docs/components/live-orb) | `@23rd/live-orb` | An evenly lit sphere with two capsule eyes that follow the pointer |
| [Logo Burst](https://23rd.dev/docs/components/logo-burst) | `@23rd/logo-burst` | Hair-line tentacles explode from the center, then breathe, light and dark |
| [Phosphor Score](https://23rd.dev/docs/components/phosphor-score) | `@23rd/phosphor-score` | Vertical CRT sheet music; notes fall, bloom at the playhead, then exit in a flare |
Expand Down
128 changes: 82 additions & 46 deletions app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Metadata } from "next"
import { notFound } from "next/navigation"

import { DocsPager } from "@/components/docs-pager"
import { DocsStage } from "@/components/docs-stage"
import { DocsToc } from "@/components/docs-toc"
import { JsonLd } from "@/components/json-ld"
import { getMDXComponents } from "@/components/mdx"
Expand All @@ -29,55 +30,85 @@ export default async function Page(props: {
const component = isComponentPage(params.slug)
const markdown = await page.data.getText("processed")

return (
<div className="flex w-full items-start gap-8 px-2 md:ps-0 md:pe-3 md:pt-3 xl:pe-8">
<div
data-slot="docs-sheet"
className="@container/page min-h-[calc(100svh-0.75rem)] min-w-0 flex-1 rounded-t-2xl border border-b-0 bg-background [--background:var(--sheet)] px-5 py-10 md:px-10 md:py-12"
>
<JsonLd
data={docsJsonLd({
title: page.data.title,
description: page.data.description,
path: docsPath(params.slug),
slug: params.slug,
const jsonLd = (
<JsonLd
data={docsJsonLd({
title: page.data.title,
description: page.data.description,
path: docsPath(params.slug),
slug: params.slug,
})}
/>
)

const article = (
<article className="mx-auto w-full max-w-[52rem] min-w-0">
<header className={component ? "mb-10" : "mb-12"}>
<div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-4">
<h1
className={cn(
"min-w-0",
component
? "font-display text-[clamp(2.5rem,6.5cqw,3.6rem)] leading-[0.95]"
: "text-4xl font-semibold tracking-tight"
)}
>
{page.data.title}
</h1>
<PageActions
markdown={markdown}
pageUrl={absoluteUrl(page.url)}
sourceUrl={`${getGithubRepoUrl()}/blob/main/content/docs/${page.path}`}
className="sm:mt-1"
/>
</div>
{page.data.description ? (
<p className="mt-5 max-w-2xl text-[17px] leading-relaxed text-muted-foreground">
{page.data.description}
</p>
) : null}
</header>
<div className="prose">
<MDX
components={getMDXComponents({
a: createRelativeLink(source, page),
})}
/>
<article className="mx-auto w-full min-w-0 max-w-[52rem]">
<header className="mb-12">
<div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-4">
<h1
className={cn(
"min-w-0",
component
? "font-display text-[clamp(2.5rem,6.5cqw,3.6rem)] leading-[0.95]"
: "text-4xl font-semibold tracking-tight"
)}
>
{page.data.title}
</h1>
<PageActions
markdown={markdown}
pageUrl={absoluteUrl(page.url)}
sourceUrl={`${getGithubRepoUrl()}/blob/main/content/docs/${page.path}`}
className="sm:mt-1"
/>
</div>
<DocsPager tree={source.getPageTree()} url={page.url} />
</article>
)

// A component page: the preview fills a stage at the top, then the page.
// The outline rides inside the sheet, beside the article.
if (component) {
return (
<DocsStage>
{/* Same gutter and radius as the stage above it. */}
<div className="px-2 md:px-3">
<div
data-slot="docs-sheet"
className="@container/page min-h-svh rounded-t-[1.25rem] bg-background px-5 pt-10 pb-10 [--background:var(--sheet)] md:px-10 md:pt-12 md:pb-12"
>
{jsonLd}
<div className="flex items-start gap-10">
{article}
<DocsToc items={page.data.toc} className="top-6 pt-1" />
</div>
{page.data.description ? (
<p className="mt-5 max-w-2xl text-[17px] leading-relaxed text-muted-foreground">
{page.data.description}
</p>
) : null}
</header>
<div className="prose">
<MDX
components={getMDXComponents({
a: createRelativeLink(source, page),
})}
/>
</div>
<DocsPager tree={source.getPageTree()} url={page.url} />
</article>
</div>
</DocsStage>
)
}

return (
<div className="flex w-full items-start gap-8 px-2 md:px-3 md:pt-3 xl:pe-8">
<div
data-slot="docs-sheet"
className="@container/page min-h-[calc(100svh-0.75rem)] min-w-0 flex-1 rounded-t-2xl border border-b-0 bg-background px-5 py-10 [--background:var(--sheet)] md:px-10 md:py-12"
>
{jsonLd}
{article}
</div>
<DocsToc items={page.data.toc} />
</div>
Expand All @@ -97,7 +128,12 @@ export async function generateMetadata(props: {

const path = docsPath(params.slug)
const extraKeywords = isComponentPage(params.slug)
? [page.data.title, "shadcn component", "React component", "Svelte component"]
? [
page.data.title,
"shadcn component",
"React component",
"Svelte component",
]
: [page.data.title]

return buildPageMetadata({
Expand Down
Loading
Loading