Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 45 additions & 39 deletions .cursor/skills/23rd/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ metadata:

This folder is checked in twice and the copies must stay identical. `skills/23rd/` is the path the skills CLI and [skills.sh](https://skills.sh) discover. `.cursor/skills/23rd/` is what Cursor loads as `/23rd` in this repo.

| File | Read it when |
| --- | --- |
| [components.md](components.md) | Picking a component, pitfalls, one working example |
| [apis.md](apis.md) | Writing props, callbacks, or theme behavior |
| [recipes.md](recipes.md) | Assembling a hero, 404, footer, or accent |
| [conventions.md](conventions.md) | Adding a component to this repo |
| File | Read it when |
| -------------------------------- | -------------------------------------------------- |
| [components.md](components.md) | Picking a component, pitfalls, one working example |
| [apis.md](apis.md) | Writing props, callbacks, or theme behavior |
| [recipes.md](recipes.md) | Assembling a hero, 404, footer, or accent |
| [conventions.md](conventions.md) | Adding a component to this repo |

Do not invent props, CSS variables, or variants. If a prop is not in [apis.md](apis.md), it does not exist.

Expand Down Expand Up @@ -72,10 +72,10 @@ import { ShaderGradient } from "@/components/ui/shader-gradient"

npm dependencies the registry declares:

| Item | React | Svelte |
| --- | --- | --- |
| `gooey-color-picker`, `tangle-footer`, `stretchy-footer` | `motion` | none |
| every other item | none | none |
| Item | React | Svelte |
| -------------------------------------------------------- | -------- | ------ |
| `gooey-color-picker`, `tangle-footer`, `stretchy-footer` | `motion` | none |
| every other item | none | none |

Published index: `https://23rd.dev/r/registry.json`. Docs: `https://23rd.dev/docs`.

Expand All @@ -90,6 +90,7 @@ flowchart TD
pick -->|Footer or page-end| foot{Treatment}
pick -->|Full page 404| d404[dithered-404]
pick -->|Scroll makes the page lean| folio[folio]
pick -->|Image peels away on scroll| peel[image-peel]
pick -->|Character or wordmark| mark{Which}
atmo -->|Quiet wash| sg[shader-gradient]
atmo -->|Fire from the bottom| sf[shader-fire]
Expand All @@ -106,20 +107,21 @@ flowchart TD

Disambiguation that agents get wrong:

| User says | Use | Not |
| --- | --- | --- |
| Color picker, swatch, hex, hue, alpha, eyedropper | `gooey-color-picker` | Any shader. Shaders are not controls. |
| Soft gradient behind a headline | `shader-gradient` | `gooey-color-picker`, `tangle-footer` |
| Fire, embers, heat under a hero | `shader-fire` | `dithered-404` unless the page is a 404 |
| 404 that burns under the cursor | `dithered-404` | `shader-fire` |
| Footer of nested spinning sentences | `tangle-footer` | `stretchy-footer` |
| Overscroll rubber band, aurora at the bottom | `stretchy-footer` | `tangle-footer` |
| 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` |
| ASCII letters that shove, scatter, and fall | `ascii-logo` | `ascii-fluid` (trails, not a wordmark) |
| CRT notation, phosphor, staves | `phosphor-score` | `shader-sky` |
| A face / orb / mascot | `live-orb` | `logo-burst` |
| User says | Use | Not |
| ------------------------------------------------- | -------------------- | -------------------------------------------------------- |
| Color picker, swatch, hex, hue, alpha, eyedropper | `gooey-color-picker` | Any shader. Shaders are not controls. |
| Soft gradient behind a headline | `shader-gradient` | `gooey-color-picker`, `tangle-footer` |
| Fire, embers, heat under a hero | `shader-fire` | `dithered-404` unless the page is a 404 |
| 404 that burns under the cursor | `dithered-404` | `shader-fire` |
| Footer of nested spinning sentences | `tangle-footer` | `stretchy-footer` |
| Overscroll rubber band, aurora at the bottom | `stretchy-footer` | `tangle-footer` |
| Page leans while scrolling | `folio` | `stretchy-footer` (that one stretches, it does not tilt) |
| Image or sticker peels / curls off as you scroll | `image-peel` | `folio` (the page leans, it does not peel) |
| Stars that speed up when you scroll | `radiant-lines` | `logo-burst` |
| Logo explodes into lines | `logo-burst` | `ascii-logo` |
| ASCII letters that shove, scatter, and fall | `ascii-logo` | `ascii-fluid` (trails, not a wordmark) |
| CRT notation, phosphor, staves | `phosphor-score` | `shader-sky` |
| A face / orb / mascot | `live-orb` | `logo-burst` |

There is one variant enum in the whole registry: `LiveOrb` `variant` is `"white" | "black" | "webgl" | "custom"`. Nothing else has `variant`.

Expand All @@ -140,6 +142,8 @@ Same shell for `ShaderFire`, `ShaderSky`, `AsciiFluid`, `LogoBurst`, `PhosphorSc

`Folio` and `StretchyFooter` are the scroller by default. Put the page in `children`. For a real document, set `windowScroll` and mark the tilting or lifting element (`data-folio-page` or `data-stretchy-page`).

`ImagePeel` is a tall sticky section (`h-[240vh]` unless you override it). Pass `src`. The sheet sticks to the nearest scroll parent and curls off as that scroller moves. `children` is what shows underneath. `side` is the corner that lifts (`top-left`, `top-right`, `bottom-left`, `bottom-right`). `amount` is how much of the image peels away at the end of the scroll (`1` clears it). The back of the sticker is `#FFFFFF`. There is no drop shadow. Transparent pixels stay transparent, so a die-cut sticker peels in its own shape. Inspiration: [React Bits Sticker Peel](https://reactbits.dev/animations/sticker-peel), which peels on hover. Image Peel peels on scroll. A local copy of that reference lives in `tmp-demos/sticker-peel` and is not a registry item.

`TangleFooter` is a `<footer>`. It is not a background. Place it after the page.

`GooeyColorPicker` is an inline control. It opens upward from the trigger. Give it room (`overflow-visible`); do not clip it in `overflow-hidden`.
Expand All @@ -162,28 +166,30 @@ Exceptions:
- `TangleFooter` paints `--tangle-ribbon` and `--tangle-text` on itself when `ribbon` / `textColor` are omitted. Pass those props to override. `background` omitted uses `#EFEAE2` / `#121210`.
- `PhosphorScore` canvas is transparent in light mode (`LIGHT_BG`) and `#050505` in dark mode (`DARK_BG`). A dark phosphor field is a hard rectangle. Clip the parent (`overflow-hidden rounded-*`) if the square edge matters. Open issue: the docs preview border looks boxed ([#28](https://github.com/radiumcoders/23rd.dev/issues/28)).
- `Folio` tilt peaks at an internal 16°. It is not a prop. On a Mac trackpad the lean is easy to miss ([#29](https://github.com/radiumcoders/23rd.dev/issues/29)). Do not add a tilt prop that does not exist. `playFolioDemo` only previews the lean; it is not the interaction.
- `ImagePeel` has no `theme` prop and no edge sides (`top`, `right`, `bottom`, `left`). The back of the sticker is `#FFFFFF`. Corners curl on the diagonal. There is no drop shadow.

`prefers-reduced-motion: reduce` is honored by the canvas and motion components (still frame, no tilt, no stretch, no tangle spin). Do not add a `reducedMotion` prop.

## Catalog

Categories match `content/docs/components/meta.json`.

| Name | Category | One line | Frameworks |
| --- | --- | --- | --- |
| `logo-burst` | Background | Hair-line tentacles explode from center, then breathe | React + Svelte |
| `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 |
| `shader-gradient` | Shaders | Quiet WebGL wash behind heroes and empty states | React + Svelte |
| `shader-fire` | Shaders | Sparse fire tongues rising from the bottom | React + Svelte |
| `shader-sky` | Shaders | Clear sky or rain; optional dotted window glass | React + Svelte |
| `tangle-footer` | Footers | Five nested SVG text ribbons | React + Svelte |
| `stretchy-footer` | Footers | Dia-style rubber overscroll with an aurora floor | React + Svelte |
| `live-orb` | Characters | Lit sphere; eyes follow the pointer | React + Svelte |
| `ascii-logo` | Characters | ASCII wordmark: hover shove, click scatter / fall / gather | React + Svelte |
| `dithered-404` | Pages | Bayer 404 burned by a fireball cursor, then reforms | React + Svelte |
| `folio` | Sections | Page leans on scroll, then springs flat | React + Svelte |
| Name | Category | One line | Frameworks |
| -------------------- | ---------- | ------------------------------------------------------------- | -------------- |
| `logo-burst` | Background | Hair-line tentacles explode from center, then breathe | React + Svelte |
| `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 |
| `shader-gradient` | Shaders | Quiet WebGL wash behind heroes and empty states | React + Svelte |
| `shader-fire` | Shaders | Sparse fire tongues rising from the bottom | React + Svelte |
| `shader-sky` | Shaders | Clear sky or rain; optional dotted window glass | React + Svelte |
| `tangle-footer` | Footers | Five nested SVG text ribbons | React + Svelte |
| `stretchy-footer` | Footers | Dia-style rubber overscroll with an aurora floor | React + Svelte |
| `live-orb` | Characters | Lit sphere; eyes follow the pointer | React + Svelte |
| `ascii-logo` | Characters | ASCII wordmark: hover shove, click scatter / fall / gather | React + Svelte |
| `dithered-404` | Pages | Bayer 404 burned by a fireball cursor, then reforms | React + Svelte |
| `folio` | Sections | Page leans on scroll, then springs flat | React + Svelte |
| `image-peel` | Sections | Image peels away as you scroll | React + Svelte |
| `gooey-color-picker` | Components | Swatch opens into hue, alpha, and hex under an SVG goo filter | React + Svelte |

## Minimal installs that must be right
Expand Down
69 changes: 50 additions & 19 deletions .cursor/skills/23rd/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Types below are what the React modules export. Svelte components take the same fields with these renames:

| React | Svelte |
| --- | --- |
| `className` | `class` |
| `children?: ReactNode` | `children?: Snippet` |
| `scrollRef?: RefObject<HTMLElement \| null>` | `scrollEl?: HTMLElement` |
| React | Svelte |
| ----------------------------------------------- | ------------------------- |
| `className` | `class` |
| `children?: ReactNode` | `children?: Snippet` |
| `scrollRef?: RefObject<HTMLElement \| null>` | `scrollEl?: HTMLElement` |
| `containerRef?: RefObject<HTMLElement \| null>` | `container?: HTMLElement` |

Vanilla `onThemeChange` and `onHasGl` are not React props. The wrappers subscribe internally. `Dithered404` vanilla `onHideCursor` is also internal to the wrapper.
Expand All @@ -31,13 +31,13 @@ type GooeyColorPickerProps = {

Also exported from the React module: `parseColor`, type `GooeyColor`.

| Behavior | Rule |
| --- | --- |
| Controlled | `value !== undefined`. Updates come from the parent. |
| Uncontrolled | `useState(() => parseColor(defaultValue ?? value))`. |
| Behavior | Rule |
| ------------ | ----------------------------------------------------------------------------- |
| Controlled | `value !== undefined`. Updates come from the parent. |
| Uncontrolled | `useState(() => parseColor(defaultValue ?? value))`. |
| Omitted both | `parseColor(undefined)` → `{ h: 320, s: 90, l: 58, a: 1 }` (`DEFAULT_COLOR`). |
| `onChange` | Called with the next `GooeyColor` and `toCss` (`hsla(H S% L% / A)`). |
| Strings | hex or `hsl()` / `hsla()`. Alpha in `hsla` may be 0–1 or a percent. |
| `onChange` | Called with the next `GooeyColor` and `toCss` (`hsla(H S% L% / A)`). |
| Strings | hex or `hsl()` / `hsla()`. Alpha in `hsla` may be 0–1 or a percent. |

Not props: open state, eyedropper, goo filter id.

Expand Down Expand Up @@ -243,11 +243,11 @@ type StretchyFooterProps = {

Modes:

| Props | Behavior |
| --- | --- |
| Props | Behavior |
| -------------------------------------- | ------------------------------------------------------ |
| neither `scrollRef` nor `windowScroll` | This element is the scroller. `children` are the page. |
| `scrollRef` set | Overlay only. Listeners bind to that element. |
| `windowScroll` | Fixed bottom aurora. Lifts `contentSelector`. |
| `scrollRef` set | Overlay only. Listeners bind to that element. |
| `windowScroll` | Fixed bottom aurora. Lifts `contentSelector`. |

React exports `playStretchyFooterDemo(detail?)` and `STRETCHY_FOOTER_PLAY` (`"stretchy-footer:play"`).

Expand Down Expand Up @@ -364,11 +364,42 @@ type FolioPlayDetail = {

React exports `playFolioDemo`, `FOLIO_PLAY` (`"folio:play"`), `applyFolioFrame`. The Svelte component listens for that window event. Reduced motion: no tilt, no blur.

## image-peel

```ts
type ImagePeelSide =
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"

type ImagePeelProps = {
src: string
alt?: string // ""
side?: ImagePeelSide // "bottom-right"
amount?: number // 1, clamped 0–1
className?: string
children?: ReactNode
demoId?: string
}
```

Not props: curl radius, scroll length, progress, grid size, paper color, edge sides. Scroll position drives the peel. `amount` is how far the sheet lifts at the end of the section, not a live scrubber. The curl leaves along the diagonal from `side`. The back of the sheet is `#FFFFFF` (`PEEL_BACK`). There is no drop shadow behind the sticker. Svelte uses `class` instead of `className`. Inspiration: [React Bits Sticker Peel](https://reactbits.dev/animations/sticker-peel).

```ts
type ImagePeelPlayDetail = {
target?: string
}
```

React exports `playImagePeel`, `IMAGE_PEEL_PLAY` (`"image-peel:play"`), `IMAGE_PEEL_GRID`, `imagePeelPose`, `PEEL_BACK`. Reduced motion: no peel.

## Events that are not props

| Name | Constant | Who listens |
| --- | --- | --- |
| `folio:play` | `FOLIO_PLAY` | Folio instances. `target` must match `demoId` when set. |
| `stretchy-footer:play` | `STRETCHY_FOOTER_PLAY` | Stretchy footers. Same `target` / `demoId` rule. |
| Name | Constant | Who listens |
| ---------------------- | ---------------------- | ------------------------------------------------------------ |
| `folio:play` | `FOLIO_PLAY` | Folio instances. `target` must match `demoId` when set. |
| `image-peel:play` | `IMAGE_PEEL_PLAY` | Image Peel instances. `target` must match `demoId` when set. |
| `stretchy-footer:play` | `STRETCHY_FOOTER_PLAY` | Stretchy footers. Same `target` / `demoId` rule. |

Use them to preview. Do not replace real scroll or overscroll with them in production UI.
25 changes: 25 additions & 0 deletions .cursor/skills/23rd/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,31 @@ export function Page() {

Pitfalls: there is no `tilt`, `angle`, or `intensity` prop. Reduced motion disables tilt and blur. React re-exports `playFolioDemo`, `FOLIO_PLAY` (`"folio:play"`), and `applyFolioFrame`. At the bottom the down-lean releases; at the top the up-lean releases.

## image-peel

Section. An image sticks to the scrollport and peels away as you scroll. The curl starts at `side` — a corner — and travels until `amount` of the sheet has lifted. Children are the surface underneath. The back of the sticker is white.

Best fit: a sticker, poster, or photo that should curl off the page.

Not this: a page that leans (`folio`). Image Peel does not tilt the document.

Deps: none. Key props: `src` (required), `alt` (`""`), `side` (`"bottom-right"` — `"top-left" | "top-right" | "bottom-left" | "bottom-right"`), `amount` (`1`, clamped `0`–`1`), `children`, `className`, `demoId`.

```tsx
"use client"
import { ImagePeel } from "@/components/ui/image-peel"

export function Sticker() {
return (
<ImagePeel src="/sticker.png" alt="Sticker" side="bottom-right" amount={1}>
<div className="h-full bg-background" />
</ImagePeel>
)
}
```

Pitfalls: default height is `h-[240vh]` so the sheet has room to stick. Inside a nested scroller, pass `className="h-[240%]"` instead. There is no `progress` prop — scroll drives the peel. There is no `top`, `right`, `bottom`, or `left` side. `amount` below `1` parks the curl; it does not change the radius. Reduced motion keeps the sticker flat. The back is `#FFFFFF`. There is no drop shadow. React re-exports `playImagePeel` and `IMAGE_PEEL_PLAY` (`"image-peel:play"`) for the docs preview. The curl radius is internal. Transparent images are masked, so a die-cut sticker does not peel as a rectangle. Inspiration is [React Bits Sticker Peel](https://reactbits.dev/animations/sticker-peel); a local copy is in `tmp-demos/sticker-peel` and is not published.

## gooey-color-picker

Component. A swatch opens into a hue wheel, alpha slider, and hex field. Blobs share an SVG goo filter. The trigger becomes close when open.
Expand Down
Loading
Loading