Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .cursor/skills/23rd/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,13 @@ type RingTowerProps = {
speed?: number // DEFAULT_SPEED 1, 0-4 crawl speed
draggable?: boolean // true
parallax?: number // DEFAULT_PARALLAX 24, degrees, 0-90
backface?: number // DEFAULT_BACKFACE 0.86, inside darkness, 0-1
backface?: number // DEFAULT_BACKFACE 0.86, inside fade, 0-1
theme?: "light" | "dark" | "auto" // "auto"
className?: string
}
```

No `theme`; the canvas is transparent and the images carry the color. Instance: `{ setOptions, destroy }`.
The inside of the bands fades toward black in dark mode and white in light mode. The canvas is transparent; the images carry the color. Instance: `{ setOptions, destroy }`.

## ascii-fluid

Expand Down
4 changes: 2 additions & 2 deletions .cursor/skills/23rd/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ Best fit: a showcase hero that puts screenshots or cards on display.

Not this: `shader-metal` (ribbons of material, no images).

Deps: `three`. Key props: `images`, `rings` (12), `speed` (1), `draggable` (true), `parallax` (24), `backface` (0.86).
Deps: `three`. Key props: `images`, `rings` (12), `speed` (1), `draggable` (true), `parallax` (24), `backface` (0.86), `theme`.

```tsx
"use client"
import { RingTower } from "@/components/ui/ring-tower"

export function Showcase() {
return (
<section className="relative isolate h-svh overflow-hidden bg-black">
<section className="relative isolate h-svh overflow-hidden bg-background">
<RingTower images={["/bands/0.jpg", "/bands/1.jpg", "/bands/2.jpg"]} />
</section>
)
Expand Down
3 changes: 3 additions & 0 deletions .cursor/skills/23rd/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@ Vanilla:

- No React, no Svelte, no `@/` imports.
- Export an options type, an instance type `{ setOptions, destroy }`, and `create<Name>(canvas | root, options)`.
- Every component works in light and dark mode. Expose `theme?: "light" | "dark" | "auto"` (default `"auto"`) and give anything theme-sensitive (ink, fades, backfaces, glows) a light and a dark value. Watch `html` attributes and `prefers-color-scheme` so `auto` follows the page live.
- Theme helper: `html.dark` / `html.light`, then `data-theme`, then `prefers-color-scheme`. Copy `isDarkTheme` from `logo-burst-vanilla.ts` rather than inventing a new contract.
- Honor `prefers-reduced-motion: reduce`.
- Custom `colors` or `color`, when set, win over the theme palette.

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.
- `gooey-color-picker` has no demo file; the MDX renders `<GooeyColorPicker />` inside `ComponentPreview`. Either pattern is acceptable. Do not publish the demo.

Expand Down Expand Up @@ -171,6 +173,7 @@ pnpm typecheck
## Do not

- Publish a React-only item. Tests require a Svelte port.
- Ship a component that only looks right in one theme.
- Import `./<name>-vanilla` from a file that is not the wrapper the build inlines.
- Put site chrome (`components/docs-shell.tsx`, `components/ui/button.tsx`) into the registry. `components/ui/*` in this repo is the docs site’s shadcn kit, not the published set.
- Add a `variant` prop unless the design really has discrete materials. Only `live-orb` has one.
Expand Down
13 changes: 7 additions & 6 deletions content/docs/components/ring-tower.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Installs `three` alongside the component.

## Usage

Pin it inside a relatively positioned section with a height. The canvas is transparent, so give the section a background. Each band leans a quarter turn from the last, so the stack zig-zags past the edges of the frame.
Pin it inside a relatively positioned section with a height. The canvas is transparent, so give the section a background. The inside of the bands fades toward black in dark mode and white in light mode, following the page unless you set `theme`. Each band leans a quarter turn from the last, so the stack zig-zags past the edges of the frame.

<FrameworkCode>
<FrameworkReact>
Expand All @@ -34,9 +34,9 @@ const bands = ["/bands/0.jpg", "/bands/1.jpg", "/bands/2.jpg"]

export function Hero() {
return (
<section className="relative isolate h-svh overflow-hidden bg-black">
<section className="relative isolate h-svh overflow-hidden bg-background">
<RingTower images={bands} />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-black to-transparent" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-background to-transparent" />
</section>
)
}
Expand All @@ -50,9 +50,9 @@ export function Hero() {
const bands = ["/bands/0.jpg", "/bands/1.jpg", "/bands/2.jpg"]
</script>

<section class="relative isolate h-svh overflow-hidden bg-black">
<section class="relative isolate h-svh overflow-hidden bg-background">
<RingTower images={bands} />
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-black to-transparent"></div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-background to-transparent"></div>
</section>
```
</FrameworkSvelte>
Expand Down Expand Up @@ -84,6 +84,7 @@ Every band in step, like the original:
| `speed` | `number` | `1`, 0–4 crawl speed |
| `draggable` | `boolean` | `true` |
| `parallax` | `number` | `24`, degrees, 0–90 |
| `backface` | `number` | `0.86`, inside darkness, 0–1 |
| `backface` | `number` | `0.86`, inside fade, 0–1 |
| `theme` | `"light" \| "dark" \| "auto"` | `auto` |
| `className` | `string` | React only |
| `class` | `string` | Svelte only |
2 changes: 1 addition & 1 deletion public/r/ring-tower-svelte.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/r/ring-tower.json

Large diffs are not rendered by default.

81 changes: 66 additions & 15 deletions registry/ring-tower/ring-tower-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useMemo } from "react"

import { Button } from "@/components/ui/button"
import {
ComponentControls,
ControlSlider,
Expand Down Expand Up @@ -34,15 +35,35 @@ function card(body: string) {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

const text = (x: number, y: number, size: number, value: string, weight = 400) =>
const text = (
x: number,
y: number,
size: number,
value: string,
weight = 400
) =>
`<text x="${x}" y="${y}" font-size="${size}" font-weight="${weight}" fill="${INK}" stroke="none">${value}</text>`

const lines = (x: number, y: number, widths: number[]) =>
widths
.map((w, i) => `<path d="M${x} ${y + i * 26}h${w}" stroke-opacity=".55"/>`)
.join("")

const DEMO_IMAGES = [
const PHOTOS = [
"1506905925346-21bda4d32df4",
"1501785888041-af3ef285b470",
"1441974231531-c6227db76b6e",
"1470071459604-3b5ec3a7fe05",
"1507525428034-b723cf961d3e",
"1472214103451-9374bd1c798e",
"1433086966358-54859d0ed716",
"1469474968028-56623f02e42e",
].map(
(id) =>
`https://images.unsplash.com/photo-${id}?w=1200&h=800&fit=crop&q=75&auto=format`
)

const CARDS = [
card(
text(56, 150, 40, "Ship the sharper default", 500) +
lines(56, 196, [500, 470, 380]) +
Expand Down Expand Up @@ -98,6 +119,14 @@ const DEMO_IMAGES = [
),
]

type Source = "photos" | "cards" | "none"

const SOURCES: { id: Source; label: string; images?: string[] }[] = [
{ id: "photos", label: "Photos", images: PHOTOS },
{ id: "cards", label: "Cards", images: CARDS },
{ id: "none", label: "None" },
]

export function RingTowerDemo() {
const defaults = useMemo(
() => ({
Expand All @@ -106,23 +135,23 @@ export function RingTowerDemo() {
parallax: DEFAULT_PARALLAX,
backface: DEFAULT_BACKFACE,
draggable: true,
images: true,
source: "photos" as Source,
}),
[]
)

const { props, updateProp, resetProps, hasChanges } =
usePreviewProps(defaults)

const images = props.images ? DEMO_IMAGES : undefined
const images = SOURCES.find((s) => s.id === props.source)?.images

return (
<>
<ComponentPreview
title="Ring Tower"
stageClassName="min-h-0 overflow-hidden p-0"
>
<div className="relative h-[64svh] w-full overflow-hidden rounded-[inherit] bg-black">
<div className="relative h-[64svh] w-full overflow-hidden rounded-[inherit] bg-background">
<RingTower
images={images}
rings={props.rings}
Expand All @@ -133,8 +162,18 @@ export function RingTowerDemo() {
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-black via-black/60 to-transparent"
className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-background via-background/60 to-transparent"
/>
{props.source === "photos" ? (
<a
href="https://unsplash.com"
target="_blank"
rel="noreferrer"
className="absolute right-3 bottom-3 text-xs text-foreground/50 transition-colors hover:text-foreground"
>
Photos from Unsplash
</a>
) : null}
</div>
</ComponentPreview>

Expand All @@ -143,9 +182,10 @@ export function RingTowerDemo() {
onReset={resetProps}
component="RingTower"
snippetProps={{
images: props.images
? ["/bands/0.jpg", "/bands/1.jpg", "/bands/2.jpg"]
: undefined,
images:
props.source === "none"
? undefined
: ["/bands/0.jpg", "/bands/1.jpg", "/bands/2.jpg"],
rings: props.rings === DEFAULT_RINGS ? undefined : props.rings,
speed: props.speed === DEFAULT_SPEED ? undefined : props.speed,
parallax:
Expand All @@ -155,6 +195,23 @@ export function RingTowerDemo() {
draggable: props.draggable ? undefined : false,
}}
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<span className="text-sm text-foreground/85">Images</span>
<div className="flex flex-wrap gap-1">
{SOURCES.map((item) => (
<Button
key={item.id}
type="button"
size="xs"
variant={props.source === item.id ? "default" : "outline"}
aria-pressed={props.source === item.id}
onClick={() => updateProp("source", item.id)}
>
{item.label}
</Button>
))}
</div>
</div>
<ControlSlider
label="Rings"
value={props.rings}
Expand Down Expand Up @@ -193,12 +250,6 @@ export function RingTowerDemo() {
checked={props.draggable}
onChange={(v) => updateProp("draggable", v)}
/>
<ControlSwitch
label="Images"
description="Off shows the built-in grid band"
checked={props.images}
onChange={(v) => updateProp("images", v)}
/>
</ComponentControls>
</>
)
Expand Down
Loading
Loading