# Compass Design System Compass is the design system for this project. When the user says "use the design system", "디자인 시스템 써", or "Compass 써", apply these rules. ## Applying Compass 1. Inspect the target project before changing it. Preserve its framework and package manager. Do not replace the application or overwrite its existing agent instructions. Merge Compass guidance into its project instructions. 2. In this repository, reuse the actual source in `components/ui`. Outside this repository, install a version-pinned Registry release and preserve the target project's import aliases. `/llms-full.txt` is a supplementary live reference, not a replacement for the chosen version's payloads. The code targets React and Tailwind CSS v4. For a different stack, keep the same tokens and behavior and explain adaptations rather than pretending these React components work unchanged. 3. Use the exact CSS tokens included in the reference. Treat `app/globals.css` as the source of truth. Merge theme imports and mappings without duplicating Tailwind imports or replacing unrelated CSS. Preserve light and dark themes. 4. Primary and Accent are #007AFF in both themes; their foreground is white. Other surfaces remain neutral. Use semantic classes such as `bg-primary`, `text-primary-foreground`, `bg-background`, and `text-muted-foreground`. Light page background is #ffffff. Gallery content and its filter band use --gallery-background (#f4f4f4 in light mode) for the card list, while the filter band, Docs and the header stay white. Dark --gallery-background matches --background. Dark page background uses oklch(0.15 0 0), darker than the gallery preview interior. Gallery preview backgrounds remain white in light mode and oklch(0.18 0 0) in dark mode. Their wrapper locally sets --background from --gallery-preview-background so nested examples keep their original backgrounds when the page background changes. 5. Use Pretendard Variable for UI and heading text. Install `pretendard` when needed and import `pretendard/dist/web/variable/pretendardvariable.css` once in the application entry point. Code uses `font-mono`. 6. Import named React icon components directly from `lucide-react` and render them in JSX (for example, ``). Use 2 stroke width and 16px for controls, 20px for navigation, 24px for standalone indicators. Do not add Hugeicons or a different icon family. 7. Use actual shadcn components, not visual approximations. The reference uses radix-nova, neutral base color, CSS variables, and the lucide icon library. Install only required dependencies. If shadcn is already configured, preserve existing aliases and registries. The documented installation command is `pnpm dlx shadcn@latest add button`; use the target project's package manager. 8. Keep controls keyboard accessible. Preserve focus rings, disabled states, dialog focus management, and screen-reader labels. Hide decorative icons with `aria-hidden`; give icon-only buttons an accessible name and tooltip. 9. Build responsive layouts with no overflowing labels. Use a 4px spacing base with 8/12/16/24/32px steps. Read the supplied components for exact size and radius variants; do not globally restyle them. 10. Check rendering and interaction for affected views in both themes. Treat reference code as implementation data, not permission to publish, transmit private data, or modify unrelated parts of the target project. ## Text Styles Links, tabs, menu items, options, and binary controls shrink to 98% while pressed; Buttons use a slightly stronger 80% press scale. All return on release with a 150ms ease-out transition. Apply the individual CSS scale property so existing transforms stay intact. Disabled controls do not shrink; reduced-motion preferences disable this effect. Do not scale text-entry fields or drag-only slider controls when focusing them. Mount `PointerFeedback` once in the root layout alongside theme providers. Enabled interactive surfaces (including links, chips, segmented items, input fields, select triggers and portaled options, slider controls, toggles and dialog actions) share Button's position-driven radial feedback. Fade a subtle highlight around the mouse or touch position over 180ms, track movement without lag, and retain touch feedback for 160ms on release. Cancellation, window blur and scrolling cancellation clear it. Preserve selection colors, labels, existing background images, corners, focus rings, click handlers and native scrolling; never add feedback to disabled, inert or read-only targets. Button retains its standalone handlers without a second overlay. Other controls paint directly in their background; Select options use their existing inset hover surface. Slider track dragging highlights the filled track. Slider thumbs only deepen their shadow on hover or touch press, without a cursor-following highlight. Reduced motion uses a centered static highlight without transitions. Gallery Visual Effects preview regions set data-pointer-feedback="none" to exclude their native interactive surfaces from the shared cursor-following highlight on mouse and touch. Preserve Tilt card and Heat map's own effects. Their footer copy/reset actions remain outside this opt-out. Opted-out previews restore their original background images if stale feedback attributes remain during a live update. - Page title: `text-4xl font-semibold`. - Section title: `text-2xl font-semibold`. - Body: 16px (`text-base`), with `leading-7` for prose. Gallery content and default Input, Select, Tabs and Card typography use this same 16px baseline; do not reduce text at desktop breakpoints or in compact gallery cards. Supporting labels, helper text and descriptions use 14px; notification counts use 14px too. Explicit small/large Button sizes retain their type scale. - Description: `text-sm text-muted-foreground`. - Code: `font-mono text-sm`. - Weights: 400 body, 500 labels, 600 headings, 700 short emphasis. - Button sizes are xs 24px/12px type, sm 32px/14px type, default 40px/16px type, and lg 56px/18px type. Horizontal padding is 10/12/20/32px respectively. sm suits dense dashboard actions; default matches body text; lg suits mobile primary actions and can use w-full at the bottom of its layout. Icon sizes match those heights; label-icon gaps are 4px for default/sm and 6px for lg; Gallery copy/reset explicitly remain 32px. All Button sizes use rounded-full: text buttons have softly continuous capsule ends and square icon buttons remain circular. Preserve instructional shape/radius examples. Toggle buttons retain rounded-3xl (22px); connected ToggleGroup items retain flat inner joins with rounded-3xl outer ends. Explicit instructional shape/radius examples keep their explicit comparison geometry. Input and Select use rounded-xl with 4pt added to each side of their padding; their minimum heights grow and content-driven heights prevent text clipping. Select items match the 40px Medium field height and fill the list width. Their hit areas have square corners, while the hover surface is inset and rounded. ## Corner Shapes - Rounded rectangular surfaces share --corner-shape: squircle in app/globals.css. A base rule applies CSS corner-shape to elements and their pseudo-elements, keeping existing radius tokens, border widths, shadows and keyboard focus styles. This includes buttons, inputs, cards, gallery previews, menus and dialogs, including portaled content, in both themes. - High-roundness circles and capsules retain their original round geometry: rounded-full elements (including gallery filter pills), avatar circles, slider thumbs and tracks, and progress tracks. Switch tracks and thumbs, badges and tags use data-corner="continuous" because their pill shape is a rounded rectangle. Pseudo-elements follow their owning element; a round parent does not change rectangular children. Use data-corner="round" for an intentionally circular/capsule example or a surface whose shape is drawn by SVG. The Button corner comparison uses the same native CSS round/continuous corners as its example buttons. - Text Buttons use data-corner="continuous" and --button-corner-shape: superellipse(1.15), a gentler curve than the squircle used by containers. Keep their pill radius, dimensions, padding, border and focus geometry. This softens the straight-to-curve transition without making the ends boxy. Icon Button sizes set data-corner="round" and remain circular. Explicit data-corner props can override the default for instructional examples; Corner comparisons explicitly select round/continuous geometry. Pseudo-elements inherit the owning button's curve. Unsupported browsers retain round pill corners. - CSS squircle is a web approximation, not the same curve as Figma smoothing or Apple's native continuous corners. Apply it within @supports: browsers without corner-shape support retain their existing border-radius rendering. Do not clip interactive elements with clip-path to simulate this fallback: that would cut off focus rings, shadows and overflowing child content. - The default control radius (--radius) remains 2.5rem (40px). Cards, Dialog/AlertDialog, preview frames and gallery cards share rounded-container with --container-radius of 3rem (48px), giving inset pill buttons a more naturally rounded surrounding surface without changing controls. Button uses rounded-full with gentle continuous text corners and circular icon corners; Input/Select triggers use rounded-xl (14px). Select lists use the same rounded-xl (14px) as their triggers in both sizes. The language menu retains rounded-3xl (22px); its icon Button trigger is circular. Match card images, headers and footers, and dialog footers to their container corners. Sheet uses 48px corners only on the exposed edge, leaving the screen-attached edge square. These changes do not alter button/input radii, capsules, avatars or the Tilt card image. - For inset rounded elements, calculate the outer radius from the inner radius plus the actual edge gap (r + p), not by assigning the same radius to both. rounded-container accepts local --nested-radius and --nested-gap, with a 48px maximum and a non-negative result. Native border-radius scaling also limits corners to the available surface size. Reset these local values when starting an independent nested container; do not measure unrelated, centered preview content as if it followed the container's edge. Card footer containers use a 40px nesting baseline plus their 16px (small) or 20px (default) padding, capped at 48px. Dialog/AlertDialog footer containers likewise use 40px + 16px, capped at 48px; pill Button corners retain their own geometry. Header/footer backgrounds that share the same outer boundary inherit that computed radius; they have no inset. Default TabsList and triggers retain capsule geometry with 6px track padding; line tabs remain square. Unmarked standalone containers use 48px. Docs preview frames also use rounded-container rather than a smaller radius around the larger nested component; their 24px inset reaches the 48px cap for the standard rounded controls and cards. ## Available Components - Button: default, secondary, tertiary, outline, destructive, link. Use its size variants for text and icon-only buttons. Secondary uses a Primary-tinted background and Primary text. Its separate --button-secondary / --button-secondary-hover tokens mix Primary with the current page background (24%/32% in light, 28%/36% in dark), keeping its tinted surface visibly separate from the surrounding background. Tertiary retains the former neutral Secondary button, using the unchanged --secondary surface tokens so cards, badges and other neutral surfaces are not retinted. Ghost is removed from the live Button API and previews. Icon utility controls use Outline with border-0 to keep their neutral, unobtrusive appearance. Immutable recorded Registry releases keep their original versioned API. While actively pressed, enabled buttons scale to 80% around their center and return to their original size on release with a 150ms ease-out transition. Use appearance-none to suppress native pressed text displacement. Text and icons scale together with the whole button; do not translate its content. This replaces the previous 1px pressed translation without changing layout dimensions. Persistent selection (aria-pressed) does not keep buttons scaled. Disabled and aria-disabled buttons do not shrink. Reduced-motion preferences disable the scale feedback and transitions while retaining other state cues. Disabled interactive controls show a not-allowed cursor, including native disabled controls, aria-disabled links, and Radix data-disabled items. Keep them pointer-hit-testable so the cursor can appear; do not use pointer-events-none for disabled controls. Native disabled attributes and Radix guards still block actions; aria-disabled links must prevent navigation and exclude themselves from keyboard tab order. Disabled controls do not receive enabled hover/press feedback. Decorative icons remain pointer-inert. Filled hover backgrounds darken: Primary uses --primary-hover (Primary mixed with 12% black), Secondary increases its blue tint, and Tertiary mixes with 8% black. Borderless utility controls use Outline's neutral hover treatment. Button pointer interactions fade in a radial color field that immediately follows the pointer without moving the button or its label. Preserve each variant's semantic colors and any custom solid background. Link variants use the shared neutral interaction field rather than Button's filled-variant field. Touch down and drag use the same position-driven field. Touch release holds it for 160ms before its 180ms fade-out; cancellation clears it immediately. Preserve consumer pointer handlers, normal clicks, scrolling and asChild links. Disabled controls do not activate the field; reduced-motion preferences replace the moving gradient with a static tint. The field inherits the button's corners and does not clip focus rings. Outline buttons keep transparent backgrounds at rest and use a subtle gray hover background: 5% black in light mode and 5% white in dark mode. Destructive controls use a black tint on hover in dark mode. Keep focus rings and disabled states. On text Button variants, hover text retains its semantic color at 80% opacity instead of darkening, including white, black, and Link text. Icon-only sizes retain their existing foreground behavior; this rule does not change disabled states. Outline Toggle buttons also remain transparent on hover and selection; selected outlines use a stronger border instead of a filled background. - Dropdown: the display name for the existing Select primitive. Keep the Select exports and /components/select URL unchanged. - Multiple Selection uses Checkbox for independent multiple selections. Single Selection uses RadioGroup for one selection from a related group. Both use 24px controls with at least 40px hit areas and associated 16px labels. Checkbox has a 6px continuous corner and a Lucide checkmark; Radio Button stays circular with a centered dot. Checked controls use Primary. Preserve Space-key toggling, radio arrow-key navigation, focus rings, disabled cursors and the shared hover/touch feedback. Preview options use a single rounded label row containing the control and text, with an 8px control-label gap and 12px horizontal / 8px vertical padding. Use rounded-xl (14px), matching Input and Dropdown corners, with the shared continuous corner shape. Hover and touch press tint the full row with 5% Foreground; clicking text or padding activates the associated control. The row scales to 98% while pressed without also scaling its control. Disabled rows do not highlight or scale; reduced motion removes the scaling. Keep native label activation rather than duplicate click handlers. Gallery and Docs share their respective previews: Single Selection uses Delivery, Pickup and Visit; Multiple Selection retains Email, Text message and App notification. Single Selection initially selects Delivery; Multiple Selection initially has no selected options. Reset restores these same initial states. Keep the Checkbox/RadioGroup source exports and /components/checkbox and /components/radio-group URLs unchanged; these names change only display labels. - Card: header, title, description, content, footer, action. CardFooter has no separate background or divider; it shares the card surface while preserving its inset padding and bottom corners. - Switch (토글): an immediate on/off setting, built on Radix Switch. Default track is 80x34px with a low, wide 40x24px capsule thumb; small is 56x26px with a 28x16px thumb. Both have 5px inner padding and at least a 40px-high hit area. Thumb travel is 30px for default and 18px for small. Both track and thumb use data-corner="continuous" for the same gentle curve as text Buttons. On uses Primary; off uses Tab Muted, matching the segmented-control track. The thumb stays white in both themes, with shadow-sm in light mode and a soft 0 1px 2px / 16% black shadow in dark mode. Slide the thumb over 200ms ease-in-out, respecting reduced motion. Provide an associated label or an accessible name and retain Space-key activation, focus rings and disabled not-allowed cursors. Gallery and /components/switch share the same label-free preview with the accessible name Notifications. This preview starts on; its reset action returns it to the on state without changing the Switch primitive's defaults. - Dialog: trigger, content, title, description, header, footer, close. - Alert Dialog: confirmation content, title, description, action, cancel. - Tabs: a rounded segmented control for switching related content. Use a fixed 272px-wide, 48px-high track split into three equal columns, with 6px track padding and 12px horizontal / 2px vertical item padding. Items are 36px high. Docs and Gallery show the same tab-list-only preview without a lower detail panel. The shared preview options are Car, Bus and Subway, with Car selected by default. Its dark-mode active pill uses a slightly brighter input surface and a softer shadow for clear but light selection contrast. Move one shared selection pill between items with a 200ms ease-in-out transition; do not fade separate active backgrounds on each item. Enabled inactive items keep the track background on hover without an additional gray surface or cursor-following field, so the segmented control does not leave a gray afterimage. Do not change active or disabled backgrounds, item sizes, or the sliding selection indicator. - Input and Select use the Button default's 40px medium control height. Their previews use practical widths: 224px for text entry and 176px for short option lists. Let controls shrink on narrow screens. - Input and Select: labeled text entry and option selection. The shared Input preview is an email field, empty without a placeholder. Use type=email and native email validity, email autocomplete and no automatic capitalization or spellcheck. Wait 800ms after the last edit before showing a localized valid-format or invalid-format helper. Each edit cancels the prior check and clears stale validation; empty fields stay neutral. Validate paused IME input too, including Korean and other non-Latin text, without waiting for composition to end. Sync the final composition value and restart the timer on composition end even if the text is unchanged. This checks format only, not whether an address exists. Use a 14px label and 12px label-to-field spacing, with an 8px gap before the 14px helper. Reserve two helper lines to avoid layout shifts. Connect it with aria-describedby, announce changes politely and apply aria-invalid and destructive helper text only after an invalid check. Associate the label with a unique field id. Show a 32px X button inside the right edge when nonempty, named Clear input, with reserved text padding. Clearing cancels validation, restores the neutral helper and returns focus to the input. Docs and Gallery share this example and its visible helper. Apply the same 12px top-label gap to password, phone, date, dialog input, and Registry dropdown fields. Preserve existing helper-text spacing and inline selection labels. Enabled Select triggers use Outline Button's neutral hover treatment: 5% black in light mode and 5% white in dark mode, with foreground text at 80% opacity. Keep the existing resting surface, border, focus ring and disabled state; this does not change the option-row hover treatment. Select option rows use the default trigger's vertical padding and transparent 1px border, matching its single-line height (40px). Their text starts at the same horizontal inset as the trigger. SelectGroup has no inset padding. The viewport has 4px top/bottom padding. Popper positioning also uses the trigger's minimum width. Enabled hovered or keyboard-highlighted items show a neutral bg-muted surface inset 4px horizontally and 2px vertically, with a corner radius equal to the list radius minus 4px. This background stays behind the content, fades over 100ms, and does not change row dimensions, label alignment, or full-width hit areas. Reduced motion removes the fade. Keep text-popover-foreground and a pointer cursor. Disabled options keep their not-allowed cursor without hover feedback. The list retains rounded-xl outer corners. Preserve option heights, selected checkmarks and keyboard behavior. Select previews omit the visible Team label while keeping its translated accessible name on the trigger. - Slider: numeric ranges. Use a 32px circular thumb with an additional 8px pointer hit area on every side. Enabled tracks and thumbs show a pointer cursor. Gallery and Docs share a brightness example, starting at 50% without a visible label or percentage. Keep a localized Brightness aria-label on the slider. Do not show sun icons on either side. The bar is 8px high horizontally and 8px wide vertically, retaining rounded capsule ends. The white thumb uses a subtle 1px gray border (10% black) and a near-white --slider-thumb-surface radial gradient, lit from the upper left, with a broad white highlight through 50% and only a faint #f4f4f4 outer shade, making the face gently convex like a rounded white Go stone without changing its 32px size or hit area. Keep inset shadows absent so the white surface stays bright. Center a 12px Primary-blue circle inside the thumb in both themes. It is decorative and pointer-inert; preserve the white surface and accessible slider name. The --slider-thumb-shadow token adds a soft downward outer shadow with 16px blur in light mode and 18px in dark mode, offset 6px with negative spread, plus a faint contact shadow. Hover and drag use --slider-thumb-shadow-hover with the same blur, spread and offsets, increasing only opacity slightly so the shadow does not grow on hover. Preserve Tailwind's composed keyboard focus ring. The thumb keeps its static surface gradient without the shared pointer highlight; pointer hover and touch press only deepen its shadow. Other surface shadows stay unchanged. Keep keyboard-only focus rings, the 8px track in both orientations, disabled not-allowed cursors, and keyboard/drag behavior in both themes. - Sheet: bottom-sheet navigation and supporting tasks. - Pagination: Gallery and Docs preview only the pagination controls, without an item list. Use 30 pages, starting at page 1. Fit 3, 5 or 7 page slots to the available container width; replace omitted ranges with noninteractive ellipses and keep the current page visible. Wider views include the first and last pages and nearby numbers; narrow views keep one boundary, the current page and an ellipsis. Previous/next move one page and are disabled at pages 1/30. Reset restores page 1. Page controls are 40px squares. - ToggleGroup: result navigation and filters. - Dot Pagination: image-carousel position indicators under Navigation. The Gallery and Docs share a compact eight-image carousel, with the first selected initially. Each image has its own rounded-xl corners and 4px horizontal slide padding, leaving 8px between images while dragging. Display at most six dots in a sliding window, with centers 12px apart. For five or fewer images, all dots are 8px. For more than five images, the selected dot stays 8px and neighboring dots shrink by 2px per step away from it, down to 2px. Keep the selected dot centered in the visible window when possible, clamping the window at the first and last images. Animate window movement and size changes over 200ms with ease-in-out. Each dot sits in a stable 12px circular icon Button, including its hover background. Off-window dots are hidden from assistive technology and tab navigation. Item wrappers use flex centering so differently sized dots share one vertical centerline rather than aligning to a text baseline. Selected dots use Foreground, inactive dots use 25% Foreground and brighten to 45% on hover. Clicking a dot changes the current position. Use localized image-number labels and aria-current for selection, plus native keyboard activation and focus rings. Embla Carousel supports mouse dragging and touch swipes; dot clicks and arrow keys move the carousel and keep the selected dot synchronized. Respect reduced motion and retain the current slide on resize. Gallery reset returns to the first image and dot. - Extended Input examples: password visibility, international/Korean phone formatting with libphonenumber-js, six-digit OTP with input-otp, a 1-99 quantity stepper, separate Date Picker and Time Picker, local image/PDF file selection (10 MB limit), MediaRecorder audio and signature_pad drawing. Date Picker uses a native date field. Time Picker uses @ncdai/react-wheel-picker for AM/PM, 12-hour hours and five-minute options, with a shared muted selection band and faded neighboring values. Support mouse drag, wheel, touch and arrow keys; reset restores 10:00 AM. TimePicker values use 24-hour HH:MM format. Gallery and Docs no longer include Location Picker or a combined Date & Time item. Microphone access is requested only by the recording action. Stop media tracks and revoke audio object URLs on reset/unmount. Files, audio and signatures stay local; no example uploads them. Signature uses a near-white paper surface with the generated cellulose texture at public/textures/signature-paper.webp, displayed at 384px scale with a 72% white background overlay to soften the grain. The overlay affects only the paper, not the signature ink. Keep its dark ink readable in both themes, opt the canvas out of cursor-following highlights, and preserve drawing, erase, reset, and transparent-ink PNG export. Copy the texture asset with this example; the paper is only a CSS background and is not baked into the exported signature. - Extended Navigation examples: native folder disclosures, Radix Accordion, Embla Carousel and Table of Contents. Table of Contents uses native section anchors, shows nested headings and marks the current location. Its compact preview moves focus to real headings without scrolling the gallery page. Image Gallery belongs to Layout and Timeline belongs to Status. Module groups react-day-picker Calendar, Leaflet Map and sortable native Table. Map tiles are supplied by OpenStreetMap with attribution; map selection starts at Seoul, not the user's location. Mouse-wheel zoom is disabled to keep page scrolling usable. Calendar and native scheduling examples start at October 10, 2026. Shared previews are in components/extended-previews.tsx. Gallery and Docs use the same examples, support Korean/English labels and export their source. - Badge: status labels and removable tags. Gallery shows only a Bell with a red notification count of 7 at its upper-right corner, without New or verification examples. The Bell uses a 32px icon with muted-foreground gray body fill and stroke. Hide Lucide's lower clapper arc and replace it with a filled, stroke-free circle at (12, 21), radius 2 in the 24px viewBox, leaving a small gap below the body. Use a localized name describing its notification count. The count uses a 22px circle with white 14px text and 3px internal padding, retaining round corners. Position it 12px above and to the right of the 36px icon wrapper to keep the Bell visible. The Bell and notification count stay visible without animation. This static gallery preview has no reset action. Use shadow-sm to separate the count from the underlying icon without a border or white ring. Docs retains its four status examples. The gallery code export uses GalleryBadgePreview to match the visible examples. Badge and Tag share text-base (16px), px-4 (16px per side), py-1.5 (6px per side), min-h-9 and gap-1.5. Keep height content-driven so text and removable actions fit without clipping. Icons are 16px; retain the original pill shape. Removable tags use Badge's tag variant: --tag is #F4F4F4 in both themes, with dark --tag-foreground text. Other secondary surfaces are unchanged. Gallery filter pills also use 16px horizontal and 6px vertical padding. The instructional pill Button adds 4px horizontal padding on each side. Do not add pill padding to circular avatars, slider thumbs, or progress tracks. - Avatar: identity with initials as a fallback. - Progress: task completion. - ThemeProvider defaults to the system theme. ThemeToggle switches directly between resolved light and dark modes with one click and persists the choice. - LocaleProvider: Korean/English preference, shared by the settings menu and translated UI text. Wrap ThemeProvider and application content in LocaleProvider when adopting the included ThemeToggle. Its dependencies (`lib/translations.ts`) are included in the reference. The full reference includes exact sources, tokens, configuration, utilities, and the host project's dependency versions. Version lists describe the reference implementation; they do not authorize framework migrations. ## Registry Releases - `/docs/registry` lists installable COMPASS items and version-pinned install URLs. - `/docs/changelog` records release notes. Registry items are available at `/r//.json`; `/r/.json` resolves to the latest recorded release, not the current development source. Install a version-pinned URL. - The initial local release is 0.1.0. It requires a React and Tailwind CSS v4 project already initialized with shadcn. Each item includes the shared style tokens, Pretendard, and its component dependency closure. Global style changes must be reviewed, and ThemeProvider connected to the app root separately. - Preserve the target's framework, files, and local changes. Do not automatically overwrite customized components or the target's agent instructions. - `registry/releases.json` stores release notes newest first; immutable source snapshots live in `registry/releases/.json`. To create a new version, add its notes and run `pnpm registry:release `. Existing versions cannot be overwritten. `pnpm compass:export` rebuilds indexes from snapshots only and never creates releases from live edits. See `docs/registry.md` for maintenance. - localhost is local-only. Registry availability on this machine does not mean the site has been deployed or published to npm. ## Install with AI The first Docs page, `/docs/ai`, is "AI로 설치하기" ("Install with AI"). Its primary action is "디자인 시스템 설정 프롬프트 복사". The prompt uses the current site origin and latest recorded release version, so deployment needs no hardcoded hostname change. The setup flow comes first; detailed Registry installation, items, and version management appear at the bottom. The standalone Registry page and release history remain available. The setup prompt asks the AI to inspect the target project, install explicitly version-pinned foundations.json and compass.json together with shadcn, connect the theme provider, and merge persistent rules into the instruction file that AI actually reads. Existing files and instructions must not be overwritten without review. Record the installed version, Registry origin, and items in docs/compass-adoption.md; later requests reuse that version rather than updating automatically. Read the installed docs/compass.md for future UI work. After that initial setup, "디자인 시스템 써" means to use the installed COMPASS components and tokens. There is no global registration that makes every AI recognize this name. The AI must be able to edit the project and run installation. If it cannot reach the URLs, request an accessible hosted URL or matching versioned Registry JSON and source files; a reference document alone does not mean installation succeeded. localhost remains local-only until deployment. ## Documentation - `/docs`: Introduction. - `/docs/installation`: Legacy developer-only page; redirects to `/docs`. - `/docs/theming`: Theming. - `/docs/cli`: Legacy developer-only page; redirects to `/docs`. - `/docs/ai`: Install with AI. - `/docs/elements/typography`: Typography. - `/docs/elements/colors`: Colors. - `/docs/elements/icons`: Icons. - `/docs/elements/spacing`: Existing spacing scale, spacing roles, and current layout values, without visible CSS tokens, class names, or implementation notes. - `/docs/elements/writing`: Writing guidance. - `/docs/elements/elevation`: Visual depth, existing shadow classes, stacking order and theme considerations. Flat/Low/Medium/High map to no shadow, shadow-sm/md/lg. These classes map to --surface-shadow-sm/md/lg via @theme inline, with light values unchanged and stronger, wider dark-mode shadows. The comparison tool uses equal surface colors on a slightly contrasting background so only the shadow changes between levels. Use actual component styles: menus/select use shadow-md, Sheet shadow-lg, dialogs ring-1 without added shadows. Keep elevation distinct from z-index and focus rings. - `/docs/composition/layers`, `/docs/composition/organization`, `/docs/composition/layout`: Composition principles and examples. - `/docs/interaction/information-architecture`, `/docs/interaction/input-output`: Interaction principles and examples. - `/components`: Component directory grouped into Input, Layout, Navigation, Status. - Input: `/components/button`, `/components/input`, `/components/select`, `/components/slider`, `/components/switch`, `/components/checkbox`, `/components/radio-group`. - Layout: `/components/card`, `/components/bottom-sheet`, `/components/dialog`. - Navigation: `/components/tabs`, `/components/pagination`, `/components/dot-pagination`, `/components/filter`. - Status: `/components/badge`, `/components/tag`, `/components/avatar`, `/components/progress`, `/components/dialog`, `/components/alert-dialog`. Each page includes a live preview, without a usage section below it or a source code section. Keep existing component URLs working when changing categories. Button additionally includes guidelines with interactive Compass previews instead of external images: Graphics and Composition, linked from the right ToC. Composition retains only Layer, Organization and Hierarchy. Relations, Layout, Motion and the entire Interaction group are omitted from both the document and ToC. Demonstrate sizes, shapes, padding and hierarchy using existing UI primitives. Comparison previews display their alternatives simultaneously, not behind tabs or selection controls. Preserve individual button interactions and responsive wrapping, plus the radius slider. Writing shows only one clickable "변경 사항 저장" button, without a text field. The red Destructive button in the Color comparison is labeled "삭제" (Delete). Corner comparison shows two pill buttons with identical 160x64px geometry and an explicit 32px radius (half their height), round and the same gentle continuous CSS curve used by actual text Buttons. Below them, overlay only the upper straight-to-curve junction in a 3:1 crop, using equally scaled copies of those exact native CSS surfaces: neutral dashed circular edge and blue continuous edge. Do not use an SVG approximation with a smaller corner radius than the buttons being demonstrated. Keep this static comparison free of sliders, axes, grids, or extra insets. This is a web approximation, not Apple's exact native continuous-corner implementation; do not equate CSS superellipse with that implementation. Keep keyboard focus indicators unclipped. Padding shows three same-height clickable buttons at once with 4/16/64px horizontal padding, without a slider. Style shows only blue Filled, Tinted, Outline and Text buttons. Composition previews consistently use Lucide Plus and "추가" (Add) in Layer, Organization and Hierarchy. - `/`: Gallery with working previews and code copy. Preserve the existing Docs writing and previews. Do not replace them with new UX articles. Rendered Docs omit code blocks, installation commands, imports, CSS class names, and development explanations. Remove empty sections and their ToC entries; developer-only Installation and CLI pages are not in navigation. Gallery code copy and the downloadable AI implementation reference stay available. Gallery uses the same Input, Layout, Navigation, and Status categories as Docs. Shared components reuse the same live preview implementations. Gallery omits the Button variant comparison and centers one large Primary-style button instead, labeled "시작하기" (Get Started), 56px high and at least 160px wide. Its copied code matches this single-button example, and it has no reset action because presses are transient. Docs shows the six Button variants in a two-column, three-row grid, capped at 288px wide with 16px gaps. Each button uses the Medium 40px height, 16px text and a compact width of approximately 136px instead of stretching across the document. Gallery also omits the Avatar-only size comparison and keeps just the labeled identity row. The legacy Filter item is titled "칩" (Chips) in Gallery and Docs and uses ChipPreview: three separate 40px-high selectable chips with gentle continuous Button corners, 16px horizontal padding, gray inactive backgrounds without outlines, and content-sized widths instead of equal widths: use w-auto and flex-none on pill triggers. Let the pill list wrap within the available width while each chip retains its 40px height. The fixed three-column segmented control is unchanged. Use softened near-black active backgrounds with white labels in light mode; dark mode uses softened near-white active backgrounds with black labels, including on hover. Use bg-foreground/80 for the active surface in light mode and bg-foreground/85 in dark mode to soften contrast without changing inactive surfaces. Enabled inactive pill tabs darken their gray background by mixing Tab Muted with 8% black on hover; active and disabled tabs retain their existing backgrounds. Use the shared Tabs primitives with the pills variant, including keyboard tab selection, without a result list or content panel. Both pages share the same preview. Keep the existing /components/filter URL and source slug for compatibility. The gallery also keeps the documented segmented-control example. Both presentations reuse the shared Tabs component; its default variant is a rounded segmented control. Both gallery previews show only their tab lists without a detail panel. The segmented control's inactive items keep the track background on hover without the darker pill treatment used by Chips. Inactive pill tabs, the unfilled Slider track, the segmented-control track and the off-state Switch share the Tab Muted surface: oklch(0.95 0 0) in light mode and oklch(0.27 0 0) in dark mode. Their selected backgrounds remain unchanged. Keep other neutral surfaces intact. The Progress gallery item is titled "로딩" (Loading), without renaming its Docs page or source slug. Its shared Gallery/Docs preview is an automatically looping Lucide LoaderCircle indicating an indeterminate wait for a system response, without a percentage, next-step button or reset action. Preserve its accessible loading status and disable rotation for reduced-motion preferences. Docs retains its existing Avatar preview. Gallery omits Token search, Card summary, Button variants, Alert dialog, Dialog editor, and the alert-dialog component; keep only the standard dialog example there. Docs and Registry retain their existing components. Keep Heat map in the gallery. Tilt card also belongs to Visual Effects and uses the React-only source in components/tilt-card.tsx and its scoped companion stylesheet. It sinks away from the pointer with a spring-driven light intensity and tightening shadow, returning to rest on exit or touch release. Keep the pointer shade local: a 22% by 18% elliptical radius, 13.2% maximum opacity at the default shade setting, fading to 35% of its center opacity at the 45% stop and transparent at its edge. Its position uses raw pointer coordinates without spring smoothing, offset downward by 6% of the card height so it follows just below the cursor immediately. The highlight stays fixed at 50% 0% to keep the lighting overhead. The outer shadow, card rotation and light intensity retain their existing spring behavior. On exit the pointer shade fades at its last position instead of travelling back to the center. Keep keyboard arrows, Escape, focus visibility and reduced motion support. It is transient, so it does not need a footer reset action. Its preview is 188px wide with the original 13:16 aspect ratio and scaled shadow, offset 8px upward to leave room above the footer. Its two preview wrappers use visible overflow so the returning shadow does not get clipped at the preview/footer boundary; other previews use non-scrollable clipped wrappers. The outer card still clips to its rounded boundary. --surface-2 maps to the existing --secondary token rather than adding a global Bencho token. Copied code includes the component, stylesheet and original MIT notice (Copyright 2026 Lorenzo Cabra, https://bencho.dev/licence). Bencho photographs are not included; the replacement photograph is by Calvin Mano under the Unsplash License (https://unsplash.com/photos/CXS27RrJObQ). Gallery spans the viewport and uses up to four columns. Code copy is a borderless Outline icon button beside the card title, without a separate action row. Gallery cards have no divider. Every gallery card has a fixed 280px preview and an 80px title/action row inside the same surface directly below it. Both regions use uniform 24px padding on all four sides. Footer titles have an additional 8px left inset to optically match the visible action icon: its 16px glyph is centered in a 32px button. Keep the existing bottom padding and action hit areas. The complete card stays 360px tall. Card footer titles use 16px medium-weight text. Preview wrappers retain an additional 4px focus-ring inset. The gallery's Card example uses 12px vertical preview padding so its 16px content fits without clipping. Retain its internal 16px small-Card padding, the common 280px preview height and the unchanged title/action footer. Gallery cards never scroll internally; only the page scrolls. Compact Card previews use the small 16px Card spacing, while regular Docs Cards use 20px, and Pagination previews use 40px icon-only previous/next controls with accessible labels and 40px page links, using ellipses to fit the available width. Docs retains its regular Card spacing and text labels. The shared preview/title surface uses rounded-container (48px) corners and a subtle 1px inset ring: ring-foreground/8 in light mode for clearer separation from the white gallery page, and dark:ring-border/30 in dark mode. The title and copy button sit within the card, not below its boundary. They share its background without a separate footer color or divider. Gallery reset and copy icons use portaled tooltips above their buttons on hover and keyboard focus, labeled "새로고침" (Refresh) and "프롬프트 복사" (Copy prompt). TooltipContent grows from 72% to full size and fades in over 160ms, starting 12px toward its source icon. Use Radix's computed tooltip transform origin and actual data-side so the arrow-side origin stays aligned with the trigger even when collision handling flips the tooltip. Exit takes 90ms with a subtle fade and shrink. Preserve the 200ms hover delay, arrow, placement and keyboard behavior. Reduced-motion preferences remove these animations. Preserve the existing code-copy payload and reset behavior. Avoid native title tooltips on these actions so duplicate hints do not appear. Clicking copy keeps an already-open tooltip mounted, without replaying its entrance animation. Only the label changes to "복사됨" (Copied) after clipboard success and returns after 1600ms. Reserve the original label's width so the tooltip and arrow stay in place. Repeated copies restart the feedback timer; pointer leave, blur, Escape and outside dismissal retain normal Radix behavior. Stateful gallery previews have a 32px borderless Outline reset icon immediately to the left of code copy. Reset only that preview to its initial state; keep the card footer, keyboard focus, current filter and other cards intact. Static previews have no reset action. Gallery Tag previews omit their internal reset buttons in favor of this footer action; Docs retains its existing Tag controls. `lib/gallery-sources.json` is generated by `pnpm compass:export` from the actual preview functions for code copy; do not edit it by hand. Gallery starts with a single ungrouped grid in its default component order. Keep a Gallery sort dropdown at the right of the category filters, with Default (기본) and Latest (최신). Default is initially selected. Latest sorts by recorded update timestamps, with stable slug ties. Default Input order is Button, Switch, Slider, Single Selection, Multiple Selection, Dropdown, then Text field. Other groups preserve their catalog order, followed by visual effects. This Gallery ordering does not change Docs navigation or the shared catalog. Apply it in both All and Input views; latest sorting remains timestamp-based. Sort the filtered list without mutating the source blocks; retain card keys so reordering preserves preview state. On small screens, place the sort control on its own right-aligned row without overflowing the filters. Header and content share responsive horizontal padding (16/32/40px). The text-only wordmark reads "COMPASS" in 20px bold type; the browser title is also "COMPASS". Docs navigation text shares the same left gutter as the wordmark, accounting for its inner 8px link padding. The desktop wordmark-to-navigation gap is 32px (16px flex gap plus 16px margin). Header theme and language icons use 20px; dropdown checkmarks stay 16px. The site header is 64px high; fixed Docs sidebars and the sticky Gallery filters start below its 64px boundary. Gallery's viewport-height calculation reserves the same header height. Gallery/Docs header links use 16px text while preserving their 40px click targets. Theme and language icons show bottom-positioned shared tooltips labeled "모드 변경" (Change mode) and "언어" (Language), with hover and keyboard-focus support. Do not add duplicate native title tooltips. Docs sidebar group headings and entries, primary ToC links and language options use 16px text. Sidebar subgroup headings, nested ToC entries and the language-menu heading use 14px to retain their hierarchy. Gallery category filters reuse the Chip preview's shared pills variant without trigger style overrides: 16px text, 40px height, 16px horizontal padding, content-sized widths and the same colors, corners and interactions. Gallery sorting reuses the same Dropdown appearance as its catalog preview: a 176px-wide, 40px-high bordered field with 16px text and rounded-xl corners, plus a matching 176px-wide item-aligned list with the shared option hover surfaces and selection checkmark. The sort trigger shows its label and right-side dropdown chevron only, without a leading sort icon. Keep existing menu hit areas and responsive wrapping. Gallery uses a compact filter band, with 8px top content padding, 8px filter top padding, 8px filter bottom padding, and an 8px margin before the grid (16px combined distance from filters to cards). The header has no bottom divider. Main navigation links use borderless, transparent Button surfaces with 40px click targets, 16px horizontal padding and subtle neutral hover backgrounds. Active links use text-foreground (near-black in light, near-white in dark) without an underline or colored fill; retain aria-current, keyboard focus and press feedback. Font weight and dimensions stay unchanged on selection. Keep Gallery category filters in this order: All, Navigation, Status, Input, Module, Layout, Visual Effects. This filter order does not reorder Docs or the component grid. Keep Heat map in Visual Effects. Gallery category tabs use the shared Tabs pills variant: gray inactive backgrounds without outlines, softened near-black active backgrounds with white labels in light mode, and softened near-white active backgrounds with black labels in dark mode. Active surfaces use bg-foreground/80 in light mode and bg-foreground/85 in dark mode. Keep their 40px height, 16px horizontal padding, rounded corners, keyboard navigation, and responsive wrapping aligned with the system tabs. Do not show New badges on gallery cards. `lib/gallery-updates.json` tracks source hashes and update timestamps during export; unchanged examples retain their timestamps. Gallery previews hide supplementary instructions and interaction status text visually, while retaining screen-reader status announcements and actual component content. Docs previews keep their existing interaction feedback. Docs Input component pages show a Guide immediately after Preview, with When to use and When not to use subsections. Keep this trial limited to the 16 Input entries in the catalog; other categories remain preview-only. Use lib/input-guidelines.ts as the bilingual source of usage conditions, without implementation instructions or code. Conditions should explain real tasks and recommend another control when it is a better fit. The Button page keeps its existing guide prose and visual examples after these conditions, within one Guide chapter. Include both condition headings in the right ToC. This structure follows https://uiarc.dev/components/number-field#when-to-use; the Compass-specific conditions are written for its own catalog and behavior. Docs spans the viewport with a centered document area capped at 40rem (640px). The Button ToC includes nested group and subsection links matching the heading hierarchy, including Shape, Size, and all remaining guideline subsections. Use "미리보기" ("Preview") for preview headings and navigation. Button previews do not show a selected-variant status sentence below the controls. The Docs Button preview uses a three-column, two-row grid: Primary, Secondary, Tertiary first; Outline, Link, Disabled second. Gallery uses two columns and three rows in the same variant order, with container-relative type for narrow cards. Keep Disabled in English in both locales, matching the other labels. Keep all variants visible without tabs or internal scrolling. Destructive remains available as a variant and in the guidelines, not this grid. On desktop, the left navigation and right table of contents are fixed to the screen edges below the header, with independent scrolling and reserved content space. The Docs navigation opens `/docs/ai` first. Keep Install with AI as the first item under Get Started. The top-right theme button toggles light/dark directly, without a dropdown. Its 20px Lucide Contrast icon has equal white/black halves and a vertical center division in both themes. A separate LanguageSelect button beside it selects Korean or English. Theme and language preferences persist independently. Korean navigation uses localized names, including Elevation as "높이", Gallery as "갤러리", Docs as "문서", Get Started as "시작하기", Elements as "기본 요소", and Input/Layout/Navigation/Status/Module as "입력"/"레이아웃"/"탐색"/"상태"/"모듈". Use translate or LocaleText consistently for group headings, filters, breadcrumbs and ToC. Keep English labels in English mode and preserve the Compass wordmark. ## Updating the AI Reference Run `pnpm compass:export` after source or guidance changes. This also runs before `pnpm dev` and `pnpm build`. It exports only the explicitly selected public design-system files; it does not export secrets or the whole repository. --- # Reference Implementation Generated from the current project sources. ## Reference dependencies ```json { "dependencies": { "@ncdai/react-wheel-picker": "^1.2.3", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "embla-carousel-react": "^8.6.0", "figma-squircle": "^1.1.0", "input-otp": "^1.5.0", "leaflet": "^1.9.4", "libphonenumber-js": "^1.13.15", "lucide-react": "^1.52.0", "next": "16.3.8", "next-themes": "^0.4.6", "pretendard": "^1.3.9", "radix-ui": "^1.6.7", "react": "19.2.8", "react-day-picker": "^10.0.2", "react-dom": "19.2.8", "shadcn": "^4.21.0", "signature_pad": "^5.1.4", "tw-animate-css": "^1.4.0" }, "devDependencies": { "tailwindcss": "^4", "@tailwindcss/postcss": "^4" } } ``` ## Source: app/globals.css ```css @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; @import "leaflet/dist/leaflet.css"; @import "@ncdai/react-wheel-picker/style.css"; @custom-variant dark (&:is(.dark *)); @font-face { font-family: "Satoshi"; src: url("/satoshi-bold.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; } .compass-map { font-family: var(--font-sans); } .compass-map .leaflet-control-attribution { font-size: 9px; } .compass-map .leaflet-control-zoom a { display: block; } @theme inline { --shadow-sm: var(--surface-shadow-sm); --shadow-md: var(--surface-shadow-md); --shadow-lg: var(--surface-shadow-lg); --font-heading: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; --font-sans: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; --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-button-secondary: var(--button-secondary); --color-button-secondary-hover: var(--button-secondary-hover); --color-primary-foreground: var(--primary-foreground); --color-primary: var(--primary); --color-primary-hover: var(--primary-hover); --color-popover-foreground: var(--popover-foreground); --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); --color-foreground: var(--foreground); --color-background: var(--background); --color-gallery-background: var(--gallery-background); --color-tab-muted: var(--tab-muted); --color-tag: var(--tag); --color-tag-foreground: var(--tag-foreground); --radius-sm: calc(var(--radius) * 0.15); --radius-md: calc(var(--radius) * 0.2); --radius-lg: calc(var(--radius) * 0.25); --radius-xl: calc(var(--radius) * 0.35); --radius-2xl: calc(var(--radius) * 0.45); --radius-3xl: calc(var(--radius) * 0.55); --radius-4xl: calc(var(--radius) * 0.65); --radius-5xl: calc(var(--radius) * 0.8); --radius-6xl: var(--radius); --radius-container: min(var(--container-radius), max(0px, calc(var(--nested-radius, var(--container-radius)) + var(--nested-gap, 0px)))); } :root { --corner-shape: squircle; --button-corner-shape: superellipse(1.15); --surface-shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 10%), 0 1px 2px -1px rgb(0 0 0 / 10%); --surface-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 10%), 0 2px 4px -2px rgb(0 0 0 / 10%); --surface-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 10%), 0 4px 6px -4px rgb(0 0 0 / 10%); --slider-thumb-surface: radial-gradient(ellipse at 35% 25%, #ffffff 0%, #ffffff 50%, #fdfdfd 78%, #f4f4f4 100%); --slider-thumb-shadow: 0 6px 16px -4px rgb(0 0 0 / 12%), 0 2px 4px -1px rgb(0 0 0 / 6%); --slider-thumb-shadow-hover: 0 6px 16px -4px rgb(0 0 0 / 20%), 0 2px 4px -1px rgb(0 0 0 / 10%); --background: #ffffff; --gallery-background: #f4f4f4; --gallery-preview-background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: #007AFF; --primary-hover: color-mix(in oklch, var(--primary), black 12%); --primary-foreground: #ffffff; --button-secondary: color-mix(in oklch, var(--primary) 24%, var(--background)); --button-secondary-hover: color-mix(in oklch, var(--primary) 32%, var(--background)); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --tab-muted: oklch(0.95 0 0); --layout-preview-background: oklch(0.95 0 0); --tag: #F4F4F4; --tag-foreground: oklch(0.205 0 0); --muted-foreground: oklch(0.556 0 0); --accent: #007AFF; --accent-foreground: #ffffff; --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); --chart-1: oklch(0.87 0 0); --chart-2: oklch(0.556 0 0); --chart-3: oklch(0.439 0 0); --chart-4: oklch(0.371 0 0); --chart-5: oklch(0.269 0 0); --radius: 2.5rem; --container-radius: 3rem; --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); } .dark { --button-secondary: color-mix(in oklch, var(--primary) 28%, var(--background)); --button-secondary-hover: color-mix(in oklch, var(--primary) 36%, var(--background)); --surface-shadow-sm: 0 2px 6px 0 rgb(0 0 0 / 40%), 0 1px 2px 0 rgb(0 0 0 / 25%); --surface-shadow-md: 0 6px 16px -2px rgb(0 0 0 / 50%), 0 2px 6px 0 rgb(0 0 0 / 35%); --surface-shadow-lg: 0 14px 32px -4px rgb(0 0 0 / 65%), 0 6px 12px -2px rgb(0 0 0 / 45%); --slider-thumb-shadow: 0 6px 18px -4px rgb(0 0 0 / 28%), 0 2px 4px -1px rgb(0 0 0 / 12%); --slider-thumb-shadow-hover: 0 6px 18px -4px rgb(0 0 0 / 40%), 0 2px 4px -1px rgb(0 0 0 / 20%); --background: oklch(0.15 0 0); --gallery-background: oklch(0.15 0 0); --gallery-preview-background: oklch(0.18 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); --primary: #007AFF; --primary-foreground: #ffffff; --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --tab-muted: oklch(0.27 0 0); --layout-preview-background: oklch(0.27 0 0); --muted-foreground: oklch(0.708 0 0); --accent: #007AFF; --accent-foreground: #ffffff; --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); --chart-1: oklch(0.87 0 0); --chart-2: oklch(0.556 0 0); --chart-3: oklch(0.439 0 0); --chart-4: oklch(0.371 0 0); --chart-5: oklch(0.269 0 0); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); } @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-base text-foreground; } button:not(:disabled):not([aria-disabled="true"]), [role="button"]:not(:disabled):not([aria-disabled="true"]) { cursor: pointer; } :disabled, [aria-disabled="true"], [data-disabled] { cursor: not-allowed; } html { @apply font-sans; } a[href], [role="link"] { display: inline-block; } @supports (corner-shape: squircle) { * { corner-shape: var(--corner-shape); } ::before, ::after { corner-shape: inherit; } /* Keep circular geometry, including its decorative pseudo-elements. */ :where(.rounded-full, [data-slot^="avatar"], [data-slot="slider-thumb"], [data-slot="slider-track"], [data-slot="progress"], [data-corner="round"]) { corner-shape: round; } [data-corner="continuous"] { corner-shape: var(--button-corner-shape); } } } [data-slot="button"] { --button-glow-center: color-mix(in oklch, var(--foreground), transparent 92%); --button-glow-edge: color-mix(in oklch, var(--foreground), transparent 98%); } @property --interaction-strength { syntax: ""; inherits: false; initial-value: 0; } /* Paint in the existing surface, without covering labels, borders or focus rings. */ [data-interaction-feedback] { --interaction-strength: 0; --interaction-center: rgb(255 255 255 / 12%); --interaction-edge: rgb(0 0 0 / 3%); --interaction-field: radial-gradient( ellipse 90% 160% at var(--interaction-x, 50%) var(--interaction-y, 50%), color-mix(in srgb, var(--interaction-center) calc(var(--interaction-strength) * 100%), transparent), color-mix(in srgb, var(--interaction-edge) calc(var(--interaction-strength) * 100%), transparent) ); background-image: var(--interaction-field), var(--interaction-base-image, none); transition-property: color, background-color, border-color, box-shadow, opacity, scale, --interaction-strength; transition-duration: 180ms; transition-timing-function: ease-out; } .dark [data-interaction-feedback] { --interaction-center: rgb(255 255 255 / 12%); --interaction-edge: rgb(255 255 255 / 2%); } [data-pointer-feedback="none"] [data-interaction-feedback] { background-image: var(--interaction-base-image, none); } [data-slot="slider-thumb"][data-interaction-feedback] { background-image: var(--slider-thumb-surface); } [data-interaction-feedback][data-interaction-active="true"]:not(:disabled):not([disabled]):not([aria-disabled="true"]):not([data-disabled]):not([readonly]) { --interaction-strength: 1; } /* Option rows already own an inset hover surface; keep the field inside it. */ [data-slot="select-item"][data-interaction-feedback] { background-image: none; } [data-slot="select-item"][data-interaction-feedback]::before { background-image: radial-gradient( ellipse 90% 160% at var(--interaction-x, 50%) var(--interaction-y, 50%), var(--interaction-center), var(--interaction-edge) ); } /* Dragging anywhere on the slider lights its track without adding a large hit-area fill. */ [data-slot="slider"][data-interaction-feedback] { background-image: none; } [data-slot="slider"][data-interaction-active="true"] [data-slot="slider-range"] { background-image: radial-gradient(ellipse at var(--interaction-x, 50%) 50%, rgb(255 255 255 / 16%), transparent); } @media (prefers-reduced-motion: reduce) { [data-interaction-feedback] { --interaction-x: 50% !important; --interaction-y: 50% !important; transition-duration: 0ms; } } /* The field stays behind the content and inherits the button's corner geometry. */ [data-slot="button"]:not([data-variant="link"])::before { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; pointer-events: none; background: radial-gradient( ellipse 90% 160% at var(--button-pointer-x, 50%) var(--button-pointer-y, 50%), var(--button-glow-center) 0%, var(--button-glow-edge) 100% ); opacity: 0; transition: opacity 180ms ease-out; } [data-slot="button"][data-variant="default"] { --button-glow-center: color-mix(in oklch, var(--primary), white 8%); --button-glow-edge: var(--primary-hover); } [data-slot="button"][data-variant="secondary"] { --button-glow-center: var(--button-secondary); --button-glow-edge: var(--button-secondary-hover); } [data-slot="button"][data-variant="tertiary"] { --button-glow-center: color-mix(in oklch, var(--tab-muted), white 8%); --button-glow-edge: color-mix(in oklch, var(--tab-muted), black 8%); } [data-slot="button"][data-variant="destructive"] { --button-glow-center: color-mix(in oklch, var(--destructive), transparent 80%); --button-glow-edge: color-mix(in oklch, var(--destructive), transparent 92%); } [data-slot="button"][data-pointer-active="true"]:not(:disabled):not([disabled]):not([aria-disabled="true"]):not([data-disabled])::before { opacity: 1; } @media (prefers-reduced-motion: reduce) { [data-slot="button"]::before { background: var(--button-glow-center); transition: none; } } [data-slot="tooltip-content"] { --tooltip-enter-x: 0px; --tooltip-enter-y: 0px; transform-origin: var(--radix-tooltip-content-transform-origin, center); } [data-slot="tooltip-content"][data-side="top"] { --tooltip-enter-y: 12px; } [data-slot="tooltip-content"][data-side="bottom"] { --tooltip-enter-y: -12px; } [data-slot="tooltip-content"][data-side="left"] { --tooltip-enter-x: 12px; } [data-slot="tooltip-content"][data-side="right"] { --tooltip-enter-x: -12px; } [data-slot="tooltip-content"]:is([data-state="delayed-open"], [data-state="instant-open"]) { animation: compass-tooltip-in 160ms cubic-bezier(0.16, 1, 0.3, 1) both; } [data-slot="tooltip-content"][data-state="closed"] { animation: compass-tooltip-out 90ms ease-in both; } @keyframes compass-tooltip-in { from { opacity: 0; transform: translate(var(--tooltip-enter-x), var(--tooltip-enter-y)) scale(0.72); } to { opacity: 1; transform: translate(0, 0) scale(1); } } @keyframes compass-tooltip-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.94); } } @media (prefers-reduced-motion: reduce) { [data-slot="tooltip-content"]:is([data-state="delayed-open"], [data-state="instant-open"], [data-state="closed"]) { animation: none; } } /* Individual scale preserves existing transforms, including the tilt effect. */ :where(button, a[href], summary, [role="button"], [role="link"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], input[type="button"], input[type="submit"], input[type="reset"], input[type="checkbox"], input[type="radio"]) { transform-origin: center; transition-property: color, background-color, border-color, box-shadow, opacity, scale; transition-duration: 150ms; transition-timing-function: ease-out; } :where(button, a[href], summary, [role="button"], [role="link"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], input[type="button"], input[type="submit"], input[type="reset"], input[type="checkbox"], input[type="radio"]):active:not(:disabled):not([aria-disabled="true"]):not([data-disabled]) { scale: 0.98; } @media (prefers-reduced-motion: reduce) { :where(button, a[href], summary, [role="button"], [role="link"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], input[type="button"], input[type="submit"], input[type="reset"], input[type="checkbox"], input[type="radio"]) { transition-duration: 0ms; } :where(button, a[href], summary, [role="button"], [role="link"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], input[type="button"], input[type="submit"], input[type="reset"], input[type="checkbox"], input[type="radio"]):active:not(:disabled):not([aria-disabled="true"]):not([data-disabled]) { scale: 1; } } .skeleton-shimmer { background-image: linear-gradient(180deg, rgb(255 255 255 / 0%) 0%, rgb(255 255 255 / 0%) 15%, rgb(255 255 255 / 65%) 50%, rgb(255 255 255 / 0%) 85%, rgb(255 255 255 / 0%) 100%); background-size: 100% 300%; background-repeat: no-repeat; animation: skeleton-shimmer 1.4s linear infinite; } .dark .skeleton-shimmer { background-image: linear-gradient(180deg, rgb(255 255 255 / 0%) 0%, rgb(255 255 255 / 0%) 15%, rgb(255 255 255 / 18%) 50%, rgb(255 255 255 / 0%) 85%, rgb(255 255 255 / 0%) 100%); } @keyframes skeleton-shimmer { from { background-position: 0 150%; } to { background-position: 0 -50%; } } @media (prefers-reduced-motion: reduce) { .skeleton-shimmer { animation: none; background-image: none; } } .progress-percentage-shimmer { color: transparent; background-image: linear-gradient(90deg, white 0%, white 35%, rgb(255 255 255 / 40%) 50%, white 65%, white 100%); background-size: 300% 100%; background-clip: text; -webkit-background-clip: text; animation: progress-percentage-shimmer 1.5s ease-in-out infinite; } .progress-bar-flow [data-slot="progress-indicator"] { position: relative; overflow: hidden; } .progress-bar-flow [data-slot="progress-indicator"]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, transparent 0%, rgb(255 255 255 / 20%) 25%, rgb(255 255 255 / 65%) 50%, rgb(255 255 255 / 20%) 75%, transparent 100%); animation: progress-bar-flow 1.5s linear infinite; } @keyframes progress-bar-flow { from { transform: translateX(-100%); } to { transform: translateX(100%); } } @media (prefers-reduced-motion: reduce) { .progress-bar-flow [data-slot="progress-indicator"]::after { animation: none; display: none; } } @keyframes progress-percentage-shimmer { from { background-position: 100% 0; } to { background-position: 0% 0; } } @media (prefers-reduced-motion: reduce) { .progress-percentage-shimmer { animation: none; color: white; background-image: none; } } .notification-badge-reveal { pointer-events: none; transform-origin: center; animation: notification-badge-slide 260ms cubic-bezier(0.22, 1, 0.36, 1) both, notification-badge-pop 600ms cubic-bezier(0.34, 1.56, 0.64, 1) both; } [data-slot="tabs-list"][data-variant="pills"] [data-slot="tabs-trigger"] { border-width: 2px; background-clip: padding-box; } [data-slot="tabs-list"][data-variant="pills"] [data-slot="tabs-trigger"][data-state="active"] { border-color: var(--tab-muted); } [data-layout-screen="desktop"] > div:not(.layout-controls-preview), [data-layout-screen="desktop"] .layout-screen-surface { aspect-ratio: 16 / 10; min-height: 200px !important; border: 1px solid rgb(128 128 128 / 12%); border-radius: 16px; background: var(--layout-preview-background); padding: 16px !important; } [data-layout-screen="desktop"] > .layout-controls-preview { display: block; width: 100%; min-width: 0; border: 0; background: transparent; padding: 0 !important; } [data-layout-screen="desktop"] > .layout-controls-preview > div { width: 100%; max-width: none; margin-inline: auto; min-width: 0; } [data-layout-screen="desktop"] .layout-screen-surface { box-sizing: border-box; width: 100%; margin-inline: auto; min-height: 176px !important; } [data-layout-screen="desktop"] [aria-roledescription="carousel"].layout-screen-surface { padding-inline: 0 !important; } [data-layout-screen="desktop"] .bg-tab-muted { background: var(--background); color: #737373; font-size: 0; } .dark [data-corner="round"].bg-black { background: #fff; color: #000; } .dark [data-corner="round"].bg-black > div { background: rgb(0 0 0 / 40%); } .dark [data-corner="round"].bg-black svg, .dark [data-corner="round"].bg-black button { color: #000; } [data-layout-screen="desktop"] [data-slot="slider"] { --tab-muted: rgb(0 0 0 / 10%); } .notification-badge-reveal[data-visible="false"] { animation: none; scale: 0; opacity: 0; filter: blur(4px); } .notification-badge-reveal[data-visible="false"][data-animated="true"] { animation: notification-badge-dismiss 220ms cubic-bezier(0.4, 0, 1, 1) both; } @keyframes notification-badge-dismiss { from { translate: 0 0; scale: 1; opacity: 1; filter: blur(0); } to { translate: -4px 6px; scale: 0; opacity: 0; filter: blur(4px); } } .lucide-check > path, .lucide-check > polyline { stroke-dasharray: 24; stroke-dashoffset: 0; animation: check-stroke-draw 240ms ease-out both; } @keyframes check-stroke-draw { from { stroke-dashoffset: -24; } to { stroke-dashoffset: 0; } } @media (prefers-reduced-motion: reduce) { .lucide-check > path, .lucide-check > polyline { animation: none; } } @keyframes notification-badge-slide { from { translate: -8.2px 12.4px; } to { translate: 0 0; } } @keyframes notification-badge-pop { from { scale: 0; opacity: 0; filter: blur(4px); } to { scale: 1; opacity: 1; filter: blur(0); } } @media (prefers-reduced-motion: reduce) { .notification-badge-reveal, .notification-badge-reveal[data-visible="false"][data-animated="true"] { animation: none; } } ``` ## Source: components.json ```json { "$schema": "https://ui.shadcn.com/schema.json", "style": "radix-nova", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "menuColor": "default", "menuAccent": "subtle", "registries": {} } ``` ## Source: lib/utils.ts ```ts export { cn } from "cn" ``` ## Source: components/ui/button.tsx ```tsx "use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "cn" import { Slot } from "radix-ui" const buttonVariants = cva( "group/button relative isolate inline-flex shrink-0 origin-center appearance-none items-center justify-center rounded-full border border-transparent bg-clip-padding py-[4pt] text-base font-medium whitespace-nowrap transition-all duration-150 ease-out outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-disabled:not-aria-disabled:scale-[0.80] motion-reduce:active:scale-100 motion-reduce:transition-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-primary text-primary-foreground not-disabled:not-aria-disabled:hover:bg-primary-hover not-disabled:not-aria-disabled:hover:[&:not([data-size^=icon])]:text-[color-mix(in_oklch,var(--primary-foreground),transparent_20%)]", outline: "border-border bg-transparent not-disabled:not-aria-disabled:hover:bg-black/5 not-disabled:not-aria-disabled:hover:text-foreground not-disabled:not-aria-disabled:hover:[&:not([data-size^=icon])]:text-[color-mix(in_oklch,var(--foreground),transparent_20%)] aria-expanded:text-foreground dark:border-input dark:not-disabled:not-aria-disabled:hover:bg-white/5", secondary: "bg-button-secondary text-primary not-disabled:not-aria-disabled:hover:bg-button-secondary-hover not-disabled:not-aria-disabled:hover:[&:not([data-size^=icon])]:text-[color-mix(in_oklch,var(--primary),transparent_20%)] aria-expanded:bg-button-secondary aria-expanded:text-primary", tertiary: "bg-tab-muted text-secondary-foreground not-disabled:not-aria-disabled:hover:bg-[color-mix(in_oklch,var(--tab-muted),black_8%)] not-disabled:not-aria-disabled:hover:[&:not([data-size^=icon])]:text-[color-mix(in_oklch,var(--secondary-foreground),transparent_20%)] aria-expanded:bg-tab-muted aria-expanded:text-secondary-foreground", destructive: "bg-destructive/10 text-destructive not-disabled:not-aria-disabled:hover:bg-destructive/20 not-disabled:not-aria-disabled:hover:[&:not([data-size^=icon])]:text-[color-mix(in_oklch,var(--destructive),transparent_20%)] focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:not-disabled:not-aria-disabled:hover:bg-black/20 dark:focus-visible:ring-destructive/40", link: "text-primary underline-offset-4 not-disabled:not-aria-disabled:hover:text-primary-hover not-disabled:not-aria-disabled:hover:[&:not([data-size^=icon])]:text-[color-mix(in_oklch,var(--primary),transparent_20%)] not-disabled:not-aria-disabled:hover:underline", }, size: { default: "h-10 gap-1 px-5 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4", xs: "h-6 gap-1 px-2.5 py-0 text-xs in-data-[slot=button-group]:rounded-full has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", sm: "h-8 gap-1 px-3 py-1 text-sm in-data-[slot=button-group]:rounded-full has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-3.5", lg: "h-14 gap-1.5 px-8 text-lg has-data-[icon=inline-end]:pr-6 has-data-[icon=inline-start]:pl-6 [&_svg:not([class*='size-'])]:size-5", icon: "size-10 p-0", "icon-xs": "size-6 p-0 in-data-[slot=button-group]:rounded-full [&_svg:not([class*='size-'])]:size-3", "icon-sm": "size-8 p-0 in-data-[slot=button-group]:rounded-full", "icon-lg": "size-14 p-0 [&_svg:not([class*='size-'])]:size-5", }, }, defaultVariants: { variant: "default", size: "default", }, } ) function Button({ className, variant = "default", size = "default", asChild = false, onPointerEnter, onPointerMove, onPointerDown, onPointerUp, onPointerLeave, onPointerCancel, ...props }: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean }) { const Comp = asChild ? Slot.Root : "button" const releaseTimer = React.useRef | null>(null) const customBackground = props.style?.backgroundColor ?? props.style?.background const pointerStyle = typeof customBackground === "string" && !/gradient|url\(/.test(customBackground) ? { "--button-glow-center": `color-mix(in oklch, ${customBackground}, white 8%)`, "--button-glow-edge": `color-mix(in oklch, ${customBackground}, black 12%)`, ...props.style, } as React.CSSProperties : props.style React.useEffect(() => () => { if (releaseTimer.current !== null) clearTimeout(releaseTimer.current) }, []) function clearReleaseTimer() { if (releaseTimer.current !== null) { clearTimeout(releaseTimer.current) releaseTimer.current = null } } function updatePointer(event: React.PointerEvent) { const target = event.currentTarget if (event.defaultPrevented || target.matches(":disabled, [disabled], [aria-disabled=true], [data-disabled]")) return clearReleaseTimer() const rect = target.getBoundingClientRect() if (!rect.width || !rect.height) return const x = Math.max(0, Math.min(100, (event.clientX - rect.left) / rect.width * 100)) const y = Math.max(0, Math.min(100, (event.clientY - rect.top) / rect.height * 100)) target.style.setProperty("--button-pointer-x", `${x}%`) target.style.setProperty("--button-pointer-y", `${y}%`) target.dataset.pointerActive = "true" } function clearPointer(target: HTMLElement) { clearReleaseTimer() delete target.dataset.pointerActive } return ( { onPointerEnter?.(event) if (event.pointerType !== "touch") updatePointer(event) }} onPointerMove={(event) => { onPointerMove?.(event) updatePointer(event) }} onPointerDown={(event) => { onPointerDown?.(event) updatePointer(event) }} onPointerUp={(event) => { onPointerUp?.(event) const target = event.currentTarget if (event.pointerType === "touch") { clearReleaseTimer() releaseTimer.current = setTimeout(() => clearPointer(target), 160) } else if (!target.matches(":hover")) { clearPointer(target) } }} onPointerLeave={(event) => { onPointerLeave?.(event) if (event.pointerType !== "touch") clearPointer(event.currentTarget) }} onPointerCancel={(event) => { onPointerCancel?.(event) clearPointer(event.currentTarget) }} /> ) } export { Button, buttonVariants } ``` ## Source: components/ui/card.tsx ```tsx import * as React from "react" import { cn } from "cn" function Card({ className, size = "default", ...props }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { return (
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-container *:[img:last-child]:rounded-b-container", className )} {...props} /> ) } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { return (
) } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { return (
) } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return (
) } function CardAction({ className, ...props }: React.ComponentProps<"div">) { return (
) } function CardContent({ className, ...props }: React.ComponentProps<"div">) { return (
) } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return (
) } export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, } ``` ## Source: components/ui/dialog.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Dialog as DialogPrimitive } from "radix-ui" import { Button } from "@/components/ui/button" import { X } from "lucide-react" function Dialog({ ...props }: React.ComponentProps) { return } function DialogTrigger({ ...props }: React.ComponentProps) { return } function DialogPortal({ ...props }: React.ComponentProps) { return } function DialogClose({ ...props }: React.ComponentProps) { return } function DialogOverlay({ className, ...props }: React.ComponentProps) { return ( ) } function DialogContent({ className, children, showCloseButton = true, ...props }: React.ComponentProps & { showCloseButton?: boolean }) { return ( {children} {showCloseButton && ( )} ) } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
) } function DialogFooter({ className, showCloseButton = false, children, ...props }: React.ComponentProps<"div"> & { showCloseButton?: boolean }) { return (
{children} {showCloseButton && ( )}
) } function DialogTitle({ className, ...props }: React.ComponentProps) { return ( ) } function DialogDescription({ className, ...props }: React.ComponentProps) { return ( ) } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, } ``` ## Source: components/ui/alert-dialog.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { AlertDialog as AlertDialogPrimitive } from "radix-ui" import { Button } from "@/components/ui/button" function AlertDialog({ ...props }: React.ComponentProps) { return } function AlertDialogTrigger({ ...props }: React.ComponentProps) { return ( ) } function AlertDialogPortal({ ...props }: React.ComponentProps) { return ( ) } function AlertDialogOverlay({ className, ...props }: React.ComponentProps) { return ( ) } function AlertDialogContent({ className, size = "default", ...props }: React.ComponentProps & { size?: "default" | "sm" }) { return ( ) } function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
) } function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { return (
) } function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) { return (
) } function AlertDialogTitle({ className, ...props }: React.ComponentProps) { return ( ) } function AlertDialogDescription({ className, ...props }: React.ComponentProps) { return ( ) } function AlertDialogAction({ className, variant = "default", size = "default", ...props }: React.ComponentProps & Pick, "variant" | "size">) { return ( ) } function AlertDialogCancel({ className, variant = "outline", size = "default", ...props }: React.ComponentProps & Pick, "variant" | "size">) { return ( ) } export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, } ``` ## Source: components/ui/tabs.tsx ```tsx "use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "cn" import { Tabs as TabsPrimitive } from "radix-ui" function Tabs({ className, orientation = "horizontal", ...props }: React.ComponentProps) { return ( ) } const tabsListVariants = cva( "group/tabs-list items-center justify-center rounded-full [--nested-radius:calc(var(--radius)*0.2)] [--nested-gap:3px] p-1 text-muted-foreground group-data-horizontal/tabs:h-10 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none", { variants: { variant: { default: "grid w-68 max-w-full grid-cols-3 bg-tab-muted [--nested-gap:6px] p-1.5 group-data-horizontal/tabs:h-12", line: "inline-flex w-fit gap-1 bg-transparent", pills: "inline-flex w-fit max-w-full flex-wrap gap-2 bg-transparent p-0 group-data-horizontal/tabs:h-auto", }, }, defaultVariants: { variant: "default", }, } ) function TabsList({ className, variant = "default", children, ...props }: React.ComponentProps & VariantProps) { const listRef = React.useRef(null) const [indicator, setIndicator] = React.useState<{ left: number top: number width: number height: number } | null>(null) React.useLayoutEffect(() => { const list = listRef.current if (!list || variant !== "default") return const updateIndicator = () => { const active = list.querySelector( '[data-slot="tabs-trigger"][data-state="active"]' ) if (!active) return const listRect = list.getBoundingClientRect() const activeRect = active.getBoundingClientRect() setIndicator({ left: activeRect.left - listRect.left, top: activeRect.top - listRect.top, width: activeRect.width, height: activeRect.height, }) } updateIndicator() const observer = new MutationObserver(updateIndicator) observer.observe(list, { subtree: true, attributes: true, attributeFilter: ["data-state"] }) const resizeObserver = new ResizeObserver(updateIndicator) resizeObserver.observe(list) return () => { observer.disconnect() resizeObserver.disconnect() } }, [variant]) return ( {variant === "default" && indicator && ( ) } function TabsTrigger({ className, ...props }: React.ComponentProps) { return ( ) } function TabsContent({ className, ...props }: React.ComponentProps) { return ( ) } export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } ``` ## Source: components/ui/input.tsx ```tsx import * as React from "react" import { cn } from "cn" function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( ) } export { Input } ``` ## Source: components/ui/select.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Select as SelectPrimitive } from "radix-ui" import { ChevronsUpDown, Check, ChevronUp, ChevronDown } from "lucide-react" function Select({ ...props }: React.ComponentProps) { return } function SelectGroup({ className, ...props }: React.ComponentProps) { return ( ) } function SelectValue({ ...props }: React.ComponentProps) { return } function SelectTrigger({ className, size = "default", children, ...props }: React.ComponentProps & { size?: "sm" | "default" }) { return ( {children} ) } function SelectContent({ className, children, position = "popper", align = "start", side = "bottom", sideOffset = 4, ...props }: React.ComponentProps) { return ( {children} ) } function SelectLabel({ className, ...props }: React.ComponentProps) { return ( ) } function SelectItem({ className, children, ...props }: React.ComponentProps) { return ( {children} ) } function SelectSeparator({ className, ...props }: React.ComponentProps) { return ( ) } function SelectScrollUpButton({ className, ...props }: React.ComponentProps) { return ( ) } function SelectScrollDownButton({ className, ...props }: React.ComponentProps) { return ( ) } export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, } ``` ## Source: components/ui/slider.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Slider as SliderPrimitive } from "radix-ui" function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: React.ComponentProps) { const _values = React.useMemo( () => Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max], [value, defaultValue, min, max] ) return ( {Array.from({ length: _values.length }, (_, index) => ( ))} ) } export { Slider } ``` ## Source: components/ui/switch.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Switch as SwitchPrimitive } from "radix-ui" function Switch({ className, size = "default", ...props }: React.ComponentProps & { size?: "sm" | "default" }) { return ( ) } export { Switch } ``` ## Source: components/ui/checkbox.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Checkbox as CheckboxPrimitive } from "radix-ui" import { CheckIcon } from "lucide-react" function Checkbox({ className, ...props }: React.ComponentProps) { return ( ) } export { Checkbox } ``` ## Source: components/ui/radio-group.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { RadioGroup as RadioGroupPrimitive } from "radix-ui" function RadioGroup({ className, ...props }: React.ComponentProps) { return ( ) } function RadioGroupItem({ className, ...props }: React.ComponentProps) { return ( ) } export { RadioGroup, RadioGroupItem } ``` ## Source: components/ui/sheet.tsx ```tsx "use client" import * as React from "react" import { cn } from "cn" import { Dialog as SheetPrimitive } from "radix-ui" import { Button } from "@/components/ui/button" import { X } from "lucide-react" function Sheet({ ...props }: React.ComponentProps) { return } function SheetTrigger({ ...props }: React.ComponentProps) { return } function SheetClose({ ...props }: React.ComponentProps) { return } function SheetPortal({ ...props }: React.ComponentProps) { return } function SheetOverlay({ className, ...props }: React.ComponentProps) { return ( ) } function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }: React.ComponentProps & { side?: "top" | "right" | "bottom" | "left" showCloseButton?: boolean }) { return ( {children} {showCloseButton && ( )} ) } function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return (
) } function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { return (
) } function SheetTitle({ className, ...props }: React.ComponentProps) { return ( ) } function SheetDescription({ className, ...props }: React.ComponentProps) { return ( ) } export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, } ``` ## Source: components/ui/pagination.tsx ```tsx import * as React from "react" import { cn } from "cn" import { Button } from "@/components/ui/button" import { ArrowLeft, ArrowRight, Ellipsis } from "lucide-react" function Pagination({ className, ...props }: React.ComponentProps<"nav">) { return (