21st.dev 원본
Activity Dropdown · Gamma UI
섹션 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
저장한 HTML에서는 갤러리의 재생 설정이 적용되지 않아요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다. 실행 HTML에는 미리보기를 위한 실행 환경이 들어 있습니다.
baseline-example.tsx
import ActivityDropdown from "@/components/ui/activity-dropdown"
export default function ActivityDropdownDemo() {
return (
<div className="mx-auto">
<ActivityDropdown />
</div>
)
}
LICENSE
MIT License
Copyright (c) Gamma UI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
함께 쓰는 파일 14개 보기
upstream/apps/www/content/docs/components/activity-dropdown.mdx
---
title: Activity Dropdown
date: 2025-11-27
description: A modern, animated activity/notification dropdown with smooth accordion motion and staggered list reveal.
author: mazyar
published: true
---
<ComponentPreview
name="activity-dropdown-demo"
title="Activity Dropdown"
description="An animated dropdown showing recent activity items with staggered transitions."
justify="start"
/>
## Installation
<CodeTabs>
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx shadcn@latest add @gammaui/activity-dropdown
```
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Copy and paste the ActivityDropdown component into your project.</Step>
<ComponentSource
name="activity-dropdown"
title="components/ui/activity-dropdown.tsx"
/>
<Step>Make sure your imports (icons, cn, utils) match your project structure.</Step>
</Steps>
</TabsContent>
</CodeTabs>
---
## Usage
```tsx
import { ActivityDropdown } from "@/components/ui/activity-dropdown"
export default function Example() {
return (
<div className="p-4">
<ActivityDropdown />
</div>
)
}
```
This renders a fully animated notification dropdown with smooth transitions and staggered item animation.
---
## Props
| Prop | Type | Default | Description |
| --------- | -------------------------------------- | ------- | ------------------------------------------- |
| className | `string` | — | Add custom Tailwind classes to the wrapper. |
| ...props | `React.HTMLAttributes<HTMLDivElement>` | — | Pass additional DOM attributes. |
---
## Component Features
- Smooth **accordion-style open/close animation**
- Staggered activity item transitions
- Beautiful **dark mode** support
- Built-in icons using **Tabler Icons**
- Fully responsive layout
- Elegant hover states
- Easy to customize (titles, icons, colors, animations)
---
## Activity Object Shape
```ts
interface Activity {
id: number
icon: React.ReactNode
iconBg: string
title: string
description: string
time: string
}
```
---
## Example: Adding Your Own Activity Items
```tsx
import { ActivityDropdown } from "@/components/ui/activity-dropdown"
const customActivities = [
{
id: 99,
icon: <IconStar className="h-4 w-4" />,
iconBg: "bg-yellow-500/20",
title: "Premium Feature Unlocked",
description: "You now have access to all premium tools.",
time: "1 min ago",
},
]
export default function Example() {
return <ActivityDropdown activities={customActivities} />
}
```
---
## Tips
- Ideal for dashboards, notifications, and activity feeds.
- You can adjust animation speed by modifying `transitionDelay`.
- Wrap it inside a card or navbar dropdown for more complex layouts.
- Works beautifully with shadcn/ui and Tailwind themes.
upstream/apps/www/registry/example/activity-dropdown-demo.tsx
import ActivityDropdown from "@/registry/gammaui/activity-dropdown"
export default function ActivityDropdownDemo() {
return (
<div className="mx-auto">
<ActivityDropdown />
</div>
)
}
author/apps/www/registry/gammaui/activity-dropdown.tsx
"use client"
import type React from "react"
import { useState } from "react"
import {
IconAward,
IconBell,
IconCalendar,
IconCheckbox,
IconChevronUp,
IconMessageCircle,
IconTag,
} from "@tabler/icons-react"
import { cn } from "@/lib/utils"
interface Activity {
id: number
icon: React.ReactNode
iconBg: string
title: string
description: string
time: string
}
const activities: Activity[] = [
{
id: 1,
icon: <IconMessageCircle className="h-4 w-4" />,
iconBg: "bg-neutral-700 dark:bg-neutral-700 bg-neutral-200",
title: "New Message!",
description: "Sarah sent you a message.",
time: "Just Now",
},
{
id: 2,
icon: <IconAward className="h-4 w-4" />,
iconBg: "bg-neutral-700 dark:bg-neutral-700 bg-neutral-200",
title: "Level Up!",
description: "You've unlocked a new achievement.",
time: "2 min ago",
},
{
id: 3,
icon: <IconCalendar className="h-4 w-4" />,
iconBg: "bg-neutral-700 dark:bg-neutral-700 bg-neutral-200",
title: "Reminder: Meeting Today",
description: "Your team meeting starts in 30 minutes.",
time: "3 hour ago",
},
{
id: 4,
icon: <IconTag className="h-4 w-4" />,
iconBg: "bg-neutral-700 dark:bg-neutral-700 bg-neutral-200",
title: "Special Offer!",
description: "Save 20% off on subscription upgrade.",
time: "12 hours ago",
},
{
id: 5,
icon: <IconCheckbox className="h-4 w-4" />,
iconBg: "bg-neutral-700 dark:bg-neutral-700 bg-neutral-200",
title: "Task Assigned!",
description: "A new task is awaiting your action.",
time: "Yesterday",
},
]
export default function ActivityDropdown() {
const [isOpen, setIsOpen] = useState(false)
return (
<div
className={cn(
"w-full max-w-md cursor-pointer overflow-hidden rounded-2xl shadow-2xl select-none",
"bg-white dark:bg-neutral-900",
"shadow-xl shadow-black/10 dark:shadow-black/50",
"transition-all duration-500 ease-in-out",
isOpen ? "rounded-3xl" : "rounded-2xl"
)}
onClick={() => setIsOpen(!isOpen)}
>
{/* Header */}
<div className="flex items-center gap-4 p-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-100 transition-colors duration-300 dark:bg-neutral-800">
<IconBell className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
</div>
<div className="flex-1 overflow-hidden">
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">
5 New Activities
</h3>
<p
className={cn(
"text-sm text-neutral-500 dark:text-neutral-400",
"transition-all duration-500 ease-in-out",
isOpen ? "mt-0 max-h-0 opacity-0" : "mt-0.5 max-h-6 opacity-100"
)}
>
What's happening around you
</p>
</div>
<div className="flex h-8 w-8 items-center justify-center">
<IconChevronUp
className={cn(
"h-5 w-5 text-neutral-400 transition-transform duration-500 ease-in-out",
isOpen ? "rotate-0" : "rotate-180"
)}
/>
</div>
</div>
{/* Activity List */}
<div
className={cn(
"grid",
"transition-all duration-500 ease-in-out",
isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
)}
>
<div className="overflow-hidden">
<div className="px-2 pb-4">
<div className="space-y-1">
{activities.map((activity, index) => (
<div
key={activity.id}
className={cn(
"flex items-start gap-3 rounded-xl p-3",
"transition-all duration-500 ease-in-out",
"hover:bg-neutral-100 dark:hover:bg-neutral-800/50",
isOpen
? "translate-y-0 opacity-100"
: "translate-y-4 opacity-0"
)}
style={{
transitionDelay: isOpen ? `${index * 75}ms` : "0ms",
}}
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-neutral-100 transition-colors duration-300 dark:bg-neutral-700">
<span className="text-neutral-600 dark:text-neutral-300">
{activity.icon}
</span>
</div>
<div className="min-w-0 flex-1">
<h4 className="text-sm font-semibold text-neutral-900 dark:text-white">
{activity.title}
</h4>
<p className="truncate text-sm text-neutral-500 dark:text-neutral-400">
{activity.description}
</p>
</div>
<span className="shrink-0 pt-0.5 text-xs text-neutral-400 dark:text-neutral-500">
{activity.time}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}
author/apps/www/lib/utils.ts
import process from "process"
import { Metadata } from "next"
import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export const pluralize = (count: number, word: string) =>
`${count} ${word}${count === 1 ? "" : "s"}`
export function humanize(name: string): string {
return name
.replace(/-/g, " ")
.replace(/([A-Z])/g, " $1")
.trim()
.split(/\s+/)
.map((word) => word[0].toUpperCase() + word.substring(1).toLowerCase())
.join(" ")
}
export const truncate = (str: string | null, length: number) => {
if (!str || str.length <= length) return str
return `${str.slice(0, length - 3)}...`
}
export const fetcher = (...args: Parameters<typeof fetch>) =>
fetch(...args).then((res) => res.json())
/**
* Capitalizes first letters of words in string.
* @param {string} str String to be modified
* @param {boolean=false} lower Whether all other letters should be lowercased
* @return {string}
* @see https://stackoverflow.com/questions/2332811/capitalize-words-in-string/7592235#7592235
* @usage
* capitalize('fix this string'); // -> 'Fix This String'
* capitalize('javaSCrIPT'); // -> 'JavaSCrIPT'
* capitalize('javaSCrIPT', true); // -> 'Javascript'
*/
export const capitalize = (str: string, lower = false) =>
(lower ? str.toLowerCase() : str).replace(/(?:^|\s|["'([{])+\S/g, (match) =>
match.toUpperCase()
)
export function formatDate(input: string | number): string {
const date = new Date(input)
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
export const calculateReadingTime = (content: string): number => {
const words = content?.trim().split(/\s+/).filter(Boolean).length
return Math.max(1, Math.ceil(words / 200))
}
export const normalizeTag = (tag: unknown): string[] => {
if (!tag) return []
return Array.isArray(tag)
? tag.filter((t): t is string => typeof t === "string")
: [String(tag)]
}
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}
export function constructMetadata({
title = "Gamma UI - Modern React + Tailwind CSS components & Templates",
description = "Beautifully designed landing page components built with React & Tailwind CSS & Motion.",
image = absoluteUrl("/og"),
...props
}: {
title?: string
description?: string
image?: string
[key: string]: Metadata[keyof Metadata]
}): Metadata {
return {
title,
description,
keywords: [
"GammaUI",
"UI Library",
"Component Library",
"React Components",
"Next.js",
"Tailwind CSS",
"Open Source",
"Frontend",
"Design System",
"Mazyar Kawa",
"GammaUI by Aman",
"Copy and Paste Components",
"Developer Tools",
"Frontend Engineer",
"Beautiful UI",
"React UI Kit",
"Free React Components",
"Open Source UI Kit",
"Tailwind UI Components",
"Headless UI",
"Reusable React Components",
"UI Templates",
"Accessible React Components",
"Copy-Paste UI",
"Next.js Component Library",
"Open Source Developer Tools",
"Frontend Design System",
"Minimal UI Kit",
"Clean React UI",
],
openGraph: {
title,
description,
type: "website",
images: [
{
url: image,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [image],
creator: "@mazyar_kawa",
},
icons: "/favicon.ico",
metadataBase: new URL("https://gammaui.com"),
authors: [
{
name: "mazyar_kawa",
url: "https://twitter.com/mazyar_kawa",
},
],
creator: "mazyar_kawa",
...props,
}
}
author/apps/www/app/globals.css
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Open Sans, sans-serif;
--font-mono: Menlo, monospace;
--font-serif: Georgia, serif;
--radius: 1.3rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
--tracking-normal: var(--tracking-normal);
--shadow-2xl: var(--shadow-2xl);
--shadow-xl: var(--shadow-xl);
--shadow-lg: var(--shadow-lg);
--shadow-md: var(--shadow-md);
--shadow: var(--shadow);
--shadow-sm: var(--shadow-sm);
--shadow-xs: var(--shadow-xs);
--shadow-2xs: var(--shadow-2xs);
--spacing: var(--spacing);
--letter-spacing: var(--letter-spacing);
--shadow-offset-y: var(--shadow-offset-y);
--shadow-offset-x: var(--shadow-offset-x);
--shadow-spread: var(--shadow-spread);
--shadow-blur: var(--shadow-blur);
--shadow-opacity: var(--shadow-opacity);
--color-shadow-color: var(--shadow-color);
--color-destructive-foreground: var(--destructive-foreground);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
alternate;
--animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
@keyframes shimmer-slide {
to {
transform: translate(calc(100cqw - 100%), 0);
}
}
@keyframes spin-around {
0% {
transform: translateZ(0) rotate(0);
}
15%,
35% {
transform: translateZ(0) rotate(90deg);
}
65%,
85% {
transform: translateZ(0) rotate(270deg);
}
100% {
transform: translateZ(0) rotate(360deg);
}
}
}
:root {
--header-height: 64px;
--radius: 1.3rem;
--card: oklch(0.9784 0.0011 197.1387);
--card-foreground: oklch(0.1884 0.0128 248.5103);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1884 0.0128 248.5103);
--primary: oklab(67.23% -0.06787 -0.14566);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.1884 0.0128 248.5103);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.9222 0.0013 286.3737);
--muted-foreground: oklch(0.1884 0.0128 248.5103);
--accent: oklch(0.9392 0.0166 250.8453);
--accent-foreground: oklch(0.6723 0.1606 244.9955);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.9317 0.0118 231.6594);
--input: oklch(0.9809 0.0025 228.7836);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.9784 0.0011 197.1387);
--sidebar-foreground: oklch(0.1884 0.0128 248.5103);
--sidebar-primary: oklch(0.6723 0.1606 244.9955);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9392 0.0166 250.8453);
--sidebar-accent-foreground: oklch(0.6723 0.1606 244.9955);
--sidebar-border: oklch(0.9271 0.0101 238.5177);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.15);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--tracking-normal: 0em;
--background: oklch(1 0 0);
--foreground: oklch(0.1884 0.0128 248.5103);
}
.dark {
--header-height: 64px;
--background: oklch(0 0 0);
--foreground: oklch(0.9328 0.0025 228.7857);
--card: oklch(0.2097 0.008 274.5332);
--card-foreground: oklch(0.8853 0 0);
--popover: oklch(0 0 0);
--popover-foreground: oklch(0.9328 0.0025 228.7857);
--primary: oklch(0.6692 0.1607 245.011);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.9622 0.0035 219.5331);
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
--muted: oklch(0.209 0 0);
--muted-foreground: oklch(0.5637 0.0078 247.9662);
--accent: oklch(0.1928 0.0331 242.5459);
--accent-foreground: oklch(0.6692 0.1607 245.011);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.2674 0.0047 248.0045);
--input: oklch(0.302 0.0288 244.8244);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.2097 0.008 274.5332);
--sidebar-foreground: oklch(0.8853 0 0);
--sidebar-primary: oklch(0.6818 0.1584 243.354);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
--sidebar-accent-foreground: oklch(0.6692 0.1607 245.011);
--sidebar-border: oklch(0.3795 0.022 240.5943);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--radius: 1.3rem;
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.25);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}
.no-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.radial-gradient {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.radial-gradient:hover {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 0.5);
}
.radial-gradient-border-button {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.linear-mask {
mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
-webkit-mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
}
.linear-overlay {
background-image: linear-gradient(
-75deg,
rgba(255, 255, 255, 0.1) calc(var(--x) + 20%),
rgba(255, 255, 255, 0.5) calc(var(--x) + 25%),
rgba(255, 255, 255, 0.1) calc(var(--x) + 100%)
);
mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
-webkit-mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: xor;
padding: 1px;
}
@keyframes move-x {
from {
transform: translateX(var(--move-x-from));
}
to {
transform: translateX(var(--move-x-to));
}
}
.cpu-architecture {
offset-anchor: 10px 0px;
animation: animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
}
.cpu-line-1 {
offset-path: path("M 10 20 h 79.5 q 5 0 5 5 v 30");
animation-duration: 5s;
animation-delay: 1s;
}
.cpu-line-2 {
offset-path: path("M 180 10 h -69.7 q -5 0 -5 5 v 40");
animation-delay: 6s;
animation-duration: 2s;
}
.cpu-line-3 {
offset-path: path("M 130 20 v 21.8 q 0 5 -5 5 h -25");
animation-delay: 4s;
animation-duration: 6s;
}
.cpu-line-4 {
offset-path: path("M 170 80 v -21.8 q 0 -5 -5 -5 h -65");
animation-delay: 3s;
animation-duration: 3s;
}
.cpu-line-5 {
offset-path: path(
"M 135 65 h 15 q 5 0 5 5 v 10 q 0 5 -5 5 h -39.8 q -5 0 -5 -5 v -35"
);
animation-delay: 9s;
animation-duration: 4s;
}
.cpu-line-6 {
offset-path: path("M 94.8 95 v -46");
animation-delay: 3s;
animation-duration: 7s;
}
.cpu-line-7 {
offset-path: path(
"M 88 88 v -15 q 0 -5 -5 -5 h -10 q -5 0 -5 -5 v -5 q 0 -5 5 -5 h 28"
);
animation-delay: 4s;
animation-duration: 4s;
}
.cpu-line-8 {
offset-path: path("M 30 30 h 25 q 5 0 5 5 v 6.5 q 0 5 5 5 h 35");
animation-delay: 3s;
animation-duration: 3s;
}
@keyframes animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
.shadcn {
offset-anchor: 10px 0px;
animation: shadcn-animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
animation-duration: 4s;
animation-delay: 1.5s;
}
.shadcn-line-1 {
offset-path: path("M 35 14 h 25 q 5 0 5 5 v 23 q 0 5 5 5 h 25");
}
.shadcn-line-2 {
offset-path: path("M 30 50 h 65");
}
.shadcn-line-3 {
offset-path: path("M 36.3 87 h 24 q 5 0 5 -5 v -24 q 0 -5 5 -5 h 25");
}
.compBox {
animation: shadncn-animation-path-backwards;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.57, 0.02, 0, 1);
offset-path: path("M 167 36 h -17 q -5 0 -5 5 v 4 q 0 5 -5 5 h -32");
animation-duration: 4s;
opacity: 0;
animation-delay: 5s;
}
@keyframes shadncn-animation-path-backwards {
0% {
offset-distance: 100%;
opacity: 0;
}
90% {
opacity: 1;
offset-distance: 0%;
}
100% {
opacity: 0;
}
}
@keyframes shadcn-animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
@keyframes landing-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.animate-landing-marquee {
animation: landing-marquee var(--marquee-duration, 45s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-landing-marquee {
animation: none;
}
}
author/apps/www/package.json
{
"name": "www",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"preinstall": "npx only-allow bun",
"postinstall": "fumadocs-mdx",
"dev": "next dev",
"prebuild": "bun run build:registry",
"build": "next build",
"build:registry": "npx tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache || true",
"shadcn:build": "npx shadcn build registry.json --output ../www/public/r",
"typecheck": "tsc --noEmit",
"start": "next start",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format:fix": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
"check": "bun run lint && bun run typecheck && bun run format:check"
},
"dependencies": {
"@number-flow/react": "^0.5.10",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-three/fiber": "^9.6.1",
"@react-three/postprocessing": "^3.0.4",
"@tabler/icons-react": "^3.35.0",
"@types/react-syntax-highlighter": "^15.5.13",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"front-matter": "^4.0.2",
"fumadocs-core": "^16.2.5",
"fumadocs-mdx": "^13.0.5",
"jotai": "^2.15.1",
"lucide-react": "^0.552.0",
"motion": "^12.23.24",
"next": "15.2.6",
"next-themes": "^0.4.6",
"postprocessing": "^6.39.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-syntax-highlighter": "^16.1.0",
"rehype-pretty-code": "^0.14.1",
"schema-dts": "^1.1.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"three": "^0.185.1",
"ts-morph": "^27.0.2",
"zod": "^4.1.13"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@shikijs/transformers": "^3.20.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^20.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/three": "^0.185.0",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"eslint": "^9.39.2",
"eslint-config-next": "^16.0.10",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"lefthook": "^2.0.4",
"prettier": "^3.6.2",
"rimraf": "^6.1.2",
"shiki": "^3.20.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.47.0"
},
"engines": {
"bun": ">=1.1.43"
},
"prettier": {
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"importOrder": [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^@workspace/(.*)$",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/hooks/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/(.*)$",
"^@/registry/(.*)$",
"^@/styles/(.*)$",
"^@/app/(.*)$",
"^@/www/(.*)$",
"",
"^[./]"
],
"importOrderParserPlugins": [
"typescript",
"jsx",
"decorators-legacy"
],
"plugins": [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
]
}
}
Usage.tsx실행 안내·자료
// Local host for the unchanged exact baseline demonstration.
import OriginalDemo from "./baseline-example.tsx";
export default function Demo() { return <><OriginalDemo /></>; }
runtime/author-mount.tsx실행 안내·자료
/** Local sandbox host. Ready is a mount observation, never a verification result. */
import React, { Component, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
declare global {
interface Window {
__STYLEGALLERY_PREVIEW__: { id: string; status: string; errors: string[] };
}
}
export function mount(Demo: React.ComponentType, id: string) {
const state = window.__STYLEGALLERY_PREVIEW__ = { id, status: 'loading', errors: [] as string[] };
const send = (message: object) => parent.postMessage({ ...message, id }, '*');
const report = (error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
if (!state.errors.includes(message)) state.errors.push(message);
state.status = 'error';
document.body.dataset.previewStatus = 'error';
send({ type: 'sg-preview-error', message });
};
window.addEventListener('error', (event) => report(event.error ?? event.message));
window.addEventListener('unhandledrejection', (event) => report(event.reason));
window.addEventListener('securitypolicyviolation', (event) => report(`Blocked by preview policy: ${event.violatedDirective}`));
const theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.documentElement.classList.toggle('dark', theme === 'dark');
document.documentElement.dataset.theme = theme;
// Keep original source bytes while ensuring imported attribution/navigation remains local text.
const removeDestinations = () => document.querySelectorAll('a[href]').forEach((link) => {
link.removeAttribute('href');
link.removeAttribute('target');
});
document.addEventListener('click', (event) => {
if ((event.target as Element)?.closest?.('a')) event.preventDefault();
}, true);
document.addEventListener('submit', (event) => event.preventDefault());
new MutationObserver(removeDestinations).observe(document.getElementById('root')!, { childList: true, subtree: true });
const observe = () => {
const root = document.getElementById('root')!;
const elements = Array.from(root.querySelectorAll('*'));
const rect = root.getBoundingClientRect();
const diagnostics = {
textLength: (root.textContent ?? '').trim().length,
elementCount: elements.length,
visibleElementCount: elements.filter((element) => {
const bounds = element.getBoundingClientRect();
const style = getComputedStyle(element);
return bounds.width > 0 && bounds.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) !== 0;
}).length,
images: [],
canvases: [],
rootRect: { width: rect.width, height: rect.height },
documentWidth: document.documentElement.scrollWidth,
viewportWidth: innerWidth,
};
send({ type: 'sg-preview-observation', diagnostics });
};
class Boundary extends Component<{ children: React.ReactNode }, { error: string | null }> {
state = { error: null as string | null };
static getDerivedStateFromError(error: Error) { return { error: error.message }; }
componentDidCatch(error: Error) { report(error); }
render() { return this.state.error ? <p role="alert">This preview could not render: {this.state.error}</p> : this.props.children; }
}
function Ready() {
useEffect(() => {
const preference = matchMedia('(prefers-reduced-motion: reduce)');
const syncSVG = () => document.querySelectorAll('svg').forEach((svg) => {
if (preference.matches) svg.pauseAnimations?.();
else svg.unpauseAnimations?.();
});
syncSVG();
preference.addEventListener('change', syncSVG);
let second = 0;
let delayed = 0;
const first = requestAnimationFrame(() => { second = requestAnimationFrame(() => {
if (state.status !== 'error') {
state.status = 'mounted';
document.body.dataset.previewStatus = 'mounted';
send({ type: 'sg-preview-ready' });
observe();
delayed = window.setTimeout(observe, 700);
}
}); });
return () => {
cancelAnimationFrame(first);
cancelAnimationFrame(second);
clearTimeout(delayed);
preference.removeEventListener('change', syncSVG);
};
}, []);
return <Demo />;
}
createRoot(document.getElementById('root')!).render(<Boundary><Ready /></Boundary>);
}
runtime/author-styles.css실행 안내·자료
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--background);
--color-card-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--muted);
--color-secondary-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--muted);
--color-accent-foreground: var(--foreground);
--color-popover: var(--background);
--color-popover-foreground: var(--foreground);
--color-destructive: #dc2626;
--color-destructive-foreground: #ffffff;
--color-input: var(--border);
--color-border: var(--border);
--color-ring: var(--foreground);
--radius-lg: 0.5rem;
--radius-md: 0.375rem;
--radius-sm: 0.25rem;
}
:root {
--background: #ffffff;
--border: #d4d4d8;
--foreground: #18181b;
--muted: #f4f4f5;
--muted-foreground: #71717a;
--primary: #18181b;
--primary-foreground: #fafafa;
background: var(--background);
color: var(--foreground);
color-scheme: light;
font-family: Arial, sans-serif;
}
.dark {
--background: #09090b;
--border: #3f3f46;
--foreground: #fafafa;
--muted: #27272a;
--muted-foreground: #a1a1aa;
--primary: #fafafa;
--primary-foreground: #18181b;
color-scheme: dark;
}
body {
margin: 0;
min-height: 100vh;
}
#root {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: center;
min-height: 100vh;
padding: 24px;
width: 100%;
}
#root > * {
max-width: 100%;
}
noscript {
display: block;
padding: 24px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-play-state: paused !important;
}
}
runtime/applied-theme.css실행 안내·자료
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Open Sans, sans-serif;
--font-mono: Menlo, monospace;
--font-serif: Georgia, serif;
--radius: 1.3rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
--tracking-normal: var(--tracking-normal);
--shadow-2xl: var(--shadow-2xl);
--shadow-xl: var(--shadow-xl);
--shadow-lg: var(--shadow-lg);
--shadow-md: var(--shadow-md);
--shadow: var(--shadow);
--shadow-sm: var(--shadow-sm);
--shadow-xs: var(--shadow-xs);
--shadow-2xs: var(--shadow-2xs);
--spacing: var(--spacing);
--letter-spacing: var(--letter-spacing);
--shadow-offset-y: var(--shadow-offset-y);
--shadow-offset-x: var(--shadow-offset-x);
--shadow-spread: var(--shadow-spread);
--shadow-blur: var(--shadow-blur);
--shadow-opacity: var(--shadow-opacity);
--color-shadow-color: var(--shadow-color);
--color-destructive-foreground: var(--destructive-foreground);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
alternate;
--animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
@keyframes shimmer-slide {
to {
transform: translate(calc(100cqw - 100%), 0);
}
}
@keyframes spin-around {
0% {
transform: translateZ(0) rotate(0);
}
15%,
35% {
transform: translateZ(0) rotate(90deg);
}
65%,
85% {
transform: translateZ(0) rotate(270deg);
}
100% {
transform: translateZ(0) rotate(360deg);
}
}
}
:root {
--header-height: 64px;
--radius: 1.3rem;
--card: oklch(0.9784 0.0011 197.1387);
--card-foreground: oklch(0.1884 0.0128 248.5103);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1884 0.0128 248.5103);
--primary: oklab(67.23% -0.06787 -0.14566);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.1884 0.0128 248.5103);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.9222 0.0013 286.3737);
--muted-foreground: oklch(0.1884 0.0128 248.5103);
--accent: oklch(0.9392 0.0166 250.8453);
--accent-foreground: oklch(0.6723 0.1606 244.9955);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.9317 0.0118 231.6594);
--input: oklch(0.9809 0.0025 228.7836);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.9784 0.0011 197.1387);
--sidebar-foreground: oklch(0.1884 0.0128 248.5103);
--sidebar-primary: oklch(0.6723 0.1606 244.9955);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9392 0.0166 250.8453);
--sidebar-accent-foreground: oklch(0.6723 0.1606 244.9955);
--sidebar-border: oklch(0.9271 0.0101 238.5177);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.15);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--tracking-normal: 0em;
--background: oklch(1 0 0);
--foreground: oklch(0.1884 0.0128 248.5103);
}
.dark {
--header-height: 64px;
--background: oklch(0 0 0);
--foreground: oklch(0.9328 0.0025 228.7857);
--card: oklch(0.2097 0.008 274.5332);
--card-foreground: oklch(0.8853 0 0);
--popover: oklch(0 0 0);
--popover-foreground: oklch(0.9328 0.0025 228.7857);
--primary: oklch(0.6692 0.1607 245.011);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.9622 0.0035 219.5331);
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
--muted: oklch(0.209 0 0);
--muted-foreground: oklch(0.5637 0.0078 247.9662);
--accent: oklch(0.1928 0.0331 242.5459);
--accent-foreground: oklch(0.6692 0.1607 245.011);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.2674 0.0047 248.0045);
--input: oklch(0.302 0.0288 244.8244);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.2097 0.008 274.5332);
--sidebar-foreground: oklch(0.8853 0 0);
--sidebar-primary: oklch(0.6818 0.1584 243.354);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
--sidebar-accent-foreground: oklch(0.6692 0.1607 245.011);
--sidebar-border: oklch(0.3795 0.022 240.5943);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--radius: 1.3rem;
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.25);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}
.no-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.radial-gradient {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.radial-gradient:hover {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 0.5);
}
.radial-gradient-border-button {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.linear-mask {
mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
-webkit-mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
}
.linear-overlay {
background-image: linear-gradient(
-75deg,
rgba(255, 255, 255, 0.1) calc(var(--x) + 20%),
rgba(255, 255, 255, 0.5) calc(var(--x) + 25%),
rgba(255, 255, 255, 0.1) calc(var(--x) + 100%)
);
mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
-webkit-mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: xor;
padding: 1px;
}
@keyframes move-x {
from {
transform: translateX(var(--move-x-from));
}
to {
transform: translateX(var(--move-x-to));
}
}
.cpu-architecture {
offset-anchor: 10px 0px;
animation: animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
}
.cpu-line-1 {
offset-path: path("M 10 20 h 79.5 q 5 0 5 5 v 30");
animation-duration: 5s;
animation-delay: 1s;
}
.cpu-line-2 {
offset-path: path("M 180 10 h -69.7 q -5 0 -5 5 v 40");
animation-delay: 6s;
animation-duration: 2s;
}
.cpu-line-3 {
offset-path: path("M 130 20 v 21.8 q 0 5 -5 5 h -25");
animation-delay: 4s;
animation-duration: 6s;
}
.cpu-line-4 {
offset-path: path("M 170 80 v -21.8 q 0 -5 -5 -5 h -65");
animation-delay: 3s;
animation-duration: 3s;
}
.cpu-line-5 {
offset-path: path(
"M 135 65 h 15 q 5 0 5 5 v 10 q 0 5 -5 5 h -39.8 q -5 0 -5 -5 v -35"
);
animation-delay: 9s;
animation-duration: 4s;
}
.cpu-line-6 {
offset-path: path("M 94.8 95 v -46");
animation-delay: 3s;
animation-duration: 7s;
}
.cpu-line-7 {
offset-path: path(
"M 88 88 v -15 q 0 -5 -5 -5 h -10 q -5 0 -5 -5 v -5 q 0 -5 5 -5 h 28"
);
animation-delay: 4s;
animation-duration: 4s;
}
.cpu-line-8 {
offset-path: path("M 30 30 h 25 q 5 0 5 5 v 6.5 q 0 5 5 5 h 35");
animation-delay: 3s;
animation-duration: 3s;
}
@keyframes animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
.shadcn {
offset-anchor: 10px 0px;
animation: shadcn-animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
animation-duration: 4s;
animation-delay: 1.5s;
}
.shadcn-line-1 {
offset-path: path("M 35 14 h 25 q 5 0 5 5 v 23 q 0 5 5 5 h 25");
}
.shadcn-line-2 {
offset-path: path("M 30 50 h 65");
}
.shadcn-line-3 {
offset-path: path("M 36.3 87 h 24 q 5 0 5 -5 v -24 q 0 -5 5 -5 h 25");
}
.compBox {
animation: shadncn-animation-path-backwards;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.57, 0.02, 0, 1);
offset-path: path("M 167 36 h -17 q -5 0 -5 5 v 4 q 0 5 -5 5 h -32");
animation-duration: 4s;
opacity: 0;
animation-delay: 5s;
}
@keyframes shadncn-animation-path-backwards {
0% {
offset-distance: 100%;
opacity: 0;
}
90% {
opacity: 1;
offset-distance: 0%;
}
100% {
opacity: 0;
}
}
@keyframes shadcn-animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
@keyframes landing-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.animate-landing-marquee {
animation: landing-marquee var(--marquee-duration, 45s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-landing-marquee {
animation: none;
}
}
runtime/process.ts실행 안내·자료
// Browser host for a Node-only import in unused upstream documentation helpers.
// Actual components consume the unchanged cn export; no provider environment is exposed.
export default { env: {} };
THIRD-PARTY-LICENSES.txt실행 안내·자료
react 19.2.3 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
@tabler/icons-react 3.35.0 — LICENSE
MIT License
Copyright (c) 2020-2025 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
clsx 2.1.1 — license
MIT License
Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
========================================================================
tailwind-merge 3.3.1 — LICENSE.md
MIT License
Copyright (c) 2021 Dany Castillo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
scheduler 0.27.0 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
react-dom 19.2.3 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
tailwindcss 4.1.13 — LICENSE
MIT License
Copyright (c) Tailwind Labs, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
tw-animate-css 1.3.8 — LICENSE
MIT License
Copyright (c) 2025 Wombosvideo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
provenance.json실행 안내·자료
{
"id": "21st-37d1455eb7e5",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"demoIdentity": {
"file": "baseline-example.tsx",
"export": "default",
"source": "exact-public-demo"
},
"fidelity": {
"preserved": "21st 공개 discovery의 고유 데모 ID와 미리보기 이미지가 가리키는 원래 데모 바이트 전체",
"dependency_revision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"observed_differences": [
"원래 CDN demo의 레이아웃 및 인수를 유지하고, 별도 component는 조사 시점 고정 작성자 revision에서 연결한다."
],
"mapping": [
"21st 설치 별칭 @/components/ui/activity-dropdown → 작성자 registry/gammaui/activity-dropdown.tsx; 문서 ComponentSource의 이름과 export/props 대조"
]
},
"acquisitionLimitations": [
"CDN 데모는 SHA256으로, 작성자 원본과 의존 파일은 Git 커밋 및 blob SHA로 고정했다.",
"현재 작성자 revision의 의존 파일이 과거 21st 내부 구현과 바이트 단위로 같다는 주장은 하지 않는다.",
"다운로드한 코드를 실행하지 않았으며 브라우저·상호작용 검증은 별도 단계다."
],
"files": [
{
"file": "baseline-example.tsx",
"kind": "implementation",
"sha256": "0f1cf0a1bafdb22be40eaa88bbbacd2e9e9c1f2a49e438fde5427fe9518853d8",
"sourceRevision": "sha256:0f1cf0a1bafdb22be40eaa88bbbacd2e9e9c1f2a49e438fde5427fe9518853d8",
"license": "MIT"
},
{
"file": "LICENSE",
"kind": "license",
"sha256": "185f2cd440066381aa1fb70411e11e6c288a961a91bc5274e92a72ee245f83ed",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "upstream/apps/www/content/docs/components/activity-dropdown.mdx",
"kind": "upstream-implementation",
"sha256": "bd21a5f7c0a600f0e6229a6ac3c8fcb74f3e802e2f41cdc5ac014a331998b045",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "upstream/apps/www/registry/example/activity-dropdown-demo.tsx",
"kind": "upstream-implementation",
"sha256": "8166ff98cba42f1e89705f9f56b939eec14633608afbaab39f2c4fe7679cb122",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/registry/gammaui/activity-dropdown.tsx",
"kind": "implementation-dependency",
"sha256": "629fb26497377add8792c7a9428f40b29c1b8d3150e53896725dba8b26a9ab15",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/lib/utils.ts",
"kind": "implementation-dependency",
"sha256": "498451813b574b01a0eac9a1d5a96dfa261218ad55fff92ffc5afbf9fe785e22",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/app/globals.css",
"kind": "theme-source",
"sha256": "d48b78e343a4a866a9ccc1137ca3ac2418ebf221bf52ab79d70759426f091628",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/package.json",
"kind": "dependency-manifest",
"sha256": "f3307ab08f84659d3850ff0d9aa47e10999581e3536af88acef876b1cd1099fa",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
}
],
"importMap": {
"@/components/ui/activity-dropdown": "author/apps/www/registry/gammaui/activity-dropdown.tsx",
"@/lib/utils": "author/apps/www/lib/utils.ts"
},
"declaredDependencies": {
"@tabler/icons-react": "^3.35.0",
"clsx": "^2.1.1",
"next": "15.2.6",
"process": null,
"react": "^19.2.3",
"tailwind-merge": "^3.3.1"
},
"runtimeDependencies": {
"react": "19.2.3",
"@tabler/icons-react": "3.35.0",
"clsx": "2.1.1",
"tailwind-merge": "3.3.1",
"scheduler": "0.27.0",
"react-dom": "19.2.3",
"tailwindcss": "4.1.13",
"tw-animate-css": "1.3.8"
},
"assets": [],
"assetAdaptations": [],
"adaptations": [
"The exact original closed-first demonstration and activity data are retained.",
"The original author CSS is applied. An empty browser environment shim satisfies an unused Node process import in documentation utilities; component cn code is unchanged."
],
"runtime_verified": false
}
README.md실행 안내·자료
# Activity Dropdown · Gamma UI
The acquired original files are unchanged. Source revision: c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd. Each exact file hash and any demo content revision is recorded in provenance.json. Keep all included license notices.
- The exact original closed-first demonstration and activity data are retained.
- The original author CSS is applied. An empty browser environment shim satisfies an unused Node process import in documentation utilities; component cn code is unchanged.
- Host spacing and fallback tokens come from runtime/author-styles.css. runtime/applied-theme.css contains the selected original author theme and overrides fallback tokens.
Open the standalone HTML to run with all JavaScript and CSS bundled locally. ?theme=light and ?theme=dark select the host theme. The parent gallery must use an opaque allow-scripts sandbox. No external requests are required.
For source reuse, start with Usage.tsx and the unchanged baseline demonstration. Resolve @/ imports to their included matching author files and install the exact package versions in provenance.json. The host mount and styles are supplied for reference.
Build success and the ready message do not prove runtime or accessibility behavior. Browser validation is recorded separately.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
펼침 애니메이션과 펼침 컨트롤의 차이
펼침 상태는 입력 가능한 트리거와 내용의 이용 가능성으로도 표현해야 합니다.
- 이 예제에서는
- ActivityDropdown은 바깥 div의 onClick으로 isOpen을 토글합니다. 버튼, tabIndex, 키보드 처리, aria-expanded, aria-controls는 없습니다. 접힌 목록도 DOM에 남고 grid 0fr·opacity 0·overflow로 감추므로 보이지 않는 내용의 접근성 노출을 따로 확인해야 합니다.
코드와 함께 확인하기
코드에서 찾기
onClick={() => setIsOpen(!isOpen)}activity-dropdown.tsx전체 카드의 클릭이 펼침 상태를 바꿉니다.
isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"activity-dropdown.tsx내용 제거 대신 시각적 축소와 투명도를 사용합니다.
직접 해보기
Tab으로 카드를 찾아 Enter·Space로 열기를 시도하고 접힌 목록의 접근성 트리를 확인합니다.
살펴볼 변화클릭만으로 되는 동작과 키보드로 가능한 동작을 분리해 기록합니다. 펼침 상태와 내용 가용성이 접근성 정보에 드러나는지도 확인해야 합니다.
목록을 열고 활동 설명 영역을 누릅니다.
살펴볼 변화행 클릭도 바깥 카드로 전달되어 접히는지 확인합니다. 각 활동을 별도로 여는 액션이 구현된 것으로 설명하지 않습니다.
펼침 상태에서 계산되는 순차 등장
반복 입력이 오면 최신 상태를 기준으로 표현을 갱신하고 이전 완료 시점에 의미를 맡기지 않습니다.
- 이 예제에서는
- 행의 이동·투명도는 isOpen에서 결정되고 펼칠 때만 index × 75ms 지연을 줍니다. 닫을 때 지연은 0ms이고 전환 클래스는 500ms입니다. 별도 타이머나 완료 콜백은 없으며 이 숫자는 작성자 설정값입니다. 실제 부드러움이나 모션 감소 동작을 증명하는 값은 아닙니다.
코드와 함께 확인하기
코드에서 찾기
transitionDelay: isOpen ? `${index * 75}ms` : "0ms"activity-dropdown.tsx열기와 닫기의 지연 정책이 다릅니다.
"transition-all duration-500 ease-in-out"activity-dropdown.tsx여러 속성에 적용하는 전환 설정입니다.
직접 해보기
행이 순서대로 등장하는 도중 열기·닫기·열기를 빠르게 반복합니다.
살펴볼 변화마지막 isOpen 상태와 전체 행의 최종 가시성이 일치해야 합니다. 중간 포즈의 연속성은 실제 프레임으로 따로 기록합니다.
검증 환경에서 CSS 전환을 끄고 같은 토글을 반복합니다.
살펴볼 변화행의 최종 열림·닫힘이 전환 완료 이벤트 없이도 같은 상태로 정리되어야 합니다.
활동 수와 상대 시간은 고정 예제 데이터
요약 수치와 시간 표시는 어떤 데이터에서 갱신되는지 드러내야 합니다.
- 이 예제에서는
- activities는 다섯 개의 고정 객체이고 헤더의 5 New Activities도 별도 문자열입니다. Just Now 같은 time 값은 시계에서 계산되지 않으며 읽음 상태나 데이터 수신 코드가 없습니다. 목록에 항목을 추가해도 헤더 숫자를 자동으로 계산하는 구조는 아닙니다.
코드와 함께 확인하기
코드에서 찾기
const activities: Activity[] = [activity-dropdown.tsx활동 내용과 시간 문구를 정적 배열로 선언합니다.
5 New Activitiesactivity-dropdown.tsx헤더 수치가 배열 길이에서 파생되지 않습니다.
직접 해보기
원본 미리보기를 충분히 기다렸다 다시 열어 time 문구를 비교합니다.
살펴볼 변화시간 경과에 따라 갱신되는 알림 목록으로 소개하지 않습니다.
원본과 분리된 검증용 복사본에서 활동을 하나 추가하고 헤더와 행 수를 비교합니다.
살펴볼 변화수치가 자동으로 따라오지 않는 지점을 확인합니다. 실제 데이터 연결 시 요약과 목록의 소유자를 함께 정해야 합니다.
