feat: 初始化工程

master
Lxy 1 month ago
commit 1dea8319d5

@ -0,0 +1,345 @@
# Design System Inspired by Binance.US
## 1. Visual Theme & Atmosphere
Binance.US radiates the polished urgency of a digital trading floor — a space where money moves and decisions happen in seconds. The design is a two-tone composition that alternates between stark white trading surfaces and deep near-black panels (`#222126`), creating a visual rhythm that mirrors the bull-and-bear duality of crypto markets. Binance Yellow (`#F0B90B`) cuts through this monochrome foundation like a gold ingot on a steel desk — unmistakable, confident, and engineered to guide every eye toward the next action.
The interface speaks the language of fintech trust. Custom BinancePlex typography gives every headline and data point a proprietary gravitas, while generous whitespace and restrained decoration keep the focus on numbers, charts, and call-to-action buttons. The design avoids visual complexity in favor of operational clarity — every element exists to either inform or convert. Product screenshots of the mobile trading app dominate the middle sections, presented on floating device mockups against golden gradients, reinforcing that this is a platform you carry with you.
What makes Binance.US distinctive is the tension between warmth and precision. The golden yellow brand color — warm, optimistic, almost celebratory — lives inside a system of cold, clinical grey text and razor-sharp borders. This isn't a playful fintech like Robinhood or a corporate fortress like Fidelity — it's a crypto-native platform that wraps cutting-edge trading technology in the visual language of established finance.
**Key Characteristics:**
- Two-tone light/dark section alternation — white surfaces for trust, dark panels for depth
- Binance Yellow (`#F0B90B`) as the singular accent color driving all primary actions
- BinancePlex custom typeface providing proprietary brand identity at every text level
- Pill-shaped CTA buttons (50px radius) that demand attention
- Floating device mockups on golden gradients for product showcasing
- Crypto price tickers with real-time data prominently displayed
- Shadow-light elevation with subtle 5% opacity card shadows
## 2. Color Palette & Roles
### Primary
- **Binance Yellow** (`#F0B90B`): The signature — primary CTA backgrounds, brand accent, active states, link color. The single most important color in the system
- **Binance Gold** (`#FFD000`): Lighter gold variant used for pill button borders, secondary CTA fills, and golden gradient highlights
- **Light Gold** (`#F8D12F`): Soft gold for gradient endpoints and hover-adjacent states
### Secondary & Accent
- **Active Yellow** (`#D0980B`): Darkened yellow for active/pressed button states — the "clicked" gold
- **Focus Blue** (`#1EAEDB`): Accessibility focus state — appears on hover and focus for all interactive elements
### Surface & Background
- **Pure White** (`#FFFFFF`): Primary page canvas, card surfaces, light section backgrounds
- **Snow** (`#F5F5F5`): Subtle surface differentiation, input backgrounds, alternating row fills
- **Binance Dark** (`#222126`): Dark section backgrounds, footer canvas, "Trusted by millions" panel — a near-black with a faint purple undertone
- **Dark Card** (`#2B2F36`): Card surfaces within dark sections, elevated dark containers
- **Ink** (`#1E2026`): Button text on yellow backgrounds, deepest text color on light surfaces
### Neutrals & Text
- **Primary Text** (`#1E2026`): Main body text, headings on light backgrounds — near-black with slight warmth
- **Secondary Text** (`#32313A`): Navigation links, descriptive copy on light surfaces
- **Slate** (`#848E9C`): Tertiary text, metadata, timestamps, footer links — the workhorse grey
- **Steel** (`#686A6C`): Disabled-adjacent text, subtle labels
- **Muted** (`#777E90`): Secondary navigation links, less prominent footer text
- **Hover Dark** (`#1A1A1A`): Universal link hover color — text darkens on hover
### Semantic & Accent
- **Crypto Green** (`#0ECB81`): Positive price movement, success states, "up" indicators
- **Crypto Red** (`#F6465D`): Negative price movement, error states, "down" indicators
- **Border Light** (`#E6E8EA`): Standard card and section borders on light backgrounds
- **Border Gold** (`#FFD000`): Active/selected state borders, pill button outlines
### Gradient System
- **Golden Glow**: Radial gradient from `#F0B90B` center to `#F8D12F` edge — used behind product mockup screenshots
- **Dark Fade**: Linear gradient from `#222126` to transparent — used for dark section transitions
- **Hero Shimmer**: Subtle animated gold gradient on hero section accents
## 3. Typography Rules
### Font Family
**Primary:** BinancePlex (custom proprietary typeface designed by Binance)
- Fallbacks: Arial, sans-serif
- Replaced DIN Next to solve multi-language spacing issues
- Available in weights: 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold)
**System:** system-ui stack for cookie banners and third-party UI
- Fallbacks: Segoe UI, Roboto, Helvetica, Arial
### Hierarchy
| Role | Size | Weight | Line Height | Letter Spacing | Notes |
|------|------|--------|-------------|----------------|-------|
| Display Hero | 60px | 700 | 1.08 | — | Hero headlines, maximum impact |
| Display Secondary | 34px | 700 | 1.00 | — | Section titles on dark backgrounds |
| Heading 1 | 28px | 500 | 1.00 | — | Major section headings |
| Heading 2 | 24px | 700 | 1.00 | — | Feature headings, card titles |
| Heading 3 | 24px | 600 | 1.00 | — | Subsection headings |
| Heading 4 | 20px | 600 | 1.25 | — | Card headings, feature labels |
| Body Large | 20px | 500 | 1.50 | — | Hero subtitle, lead paragraphs |
| Body | 16px | 500 | 1.50 | — | Standard body text |
| Body SemiBold | 16px | 600 | 1.30 | — | Emphasized body, nav links |
| Body Bold | 16px | 700 | 1.50 | — | Strong emphasis text |
| Button | 16px | 600 | 1.25 | 0.16px | Primary button text |
| Button Small | 14.4px | 600 | 1.60 | 0.72px | Secondary buttons, wider tracking |
| Caption | 14px | 500 | 1.43 | — | Metadata, labels, prices |
| Caption SemiBold | 14px | 600 | 1.50 | — | Emphasized captions |
| Small | 12px | 600 | 1.00 | — | Tags, badges, fine print |
| Tiny | 11px | 500 | 1.00 | — | Micro-labels, chart annotations |
### Principles
BinancePlex is engineered for data-dense interfaces where numbers and text must coexist at multiple scales. The typeface has tabular numerals by default — critical for price columns and portfolio values that need perfect vertical alignment. Weights lean toward the heavier end (500-700), giving the interface a sense of authority and confidence that's essential for a financial platform. The tight line-heights (1.00-1.25) on headings create a stacked, compressed feel that mirrors the density of trading dashboards, while body text opens up to 1.50 for comfortable reading of educational and marketing content.
## 4. Component Stylings
### Buttons
**Primary (Yellow Fill)**
- Background: Binance Yellow (`#F0B90B`)
- Text: Ink (`#1E2026`), 16px/600, BinancePlex
- Border: none
- Border radius: slightly rounded (6px)
- Padding: 6px 32px
- Hover: shifts to Focus Blue (`#1EAEDB`) with white text
- Active: darkens to Active Yellow (`#D0980B`)
- Focus: Focus Blue (`#1EAEDB`) bg, 1px black border, 2px black outline, opacity 0.9
- Transition: background 200ms ease
**Primary Pill (Gold)**
- Background: Binance Gold (`#FFD000`)
- Text: White (`#FFFFFF`)
- Border: 1px solid `#FFD000`
- Border radius: full pill (50px)
- Padding: 10px horizontal
- Shadow: `rgb(153,153,153) 0px 2px 10px -3px`
- Hover: shifts to Focus Blue (`#1EAEDB`) with white text
**Secondary (White Outlined)**
- Background: White (`#FFFFFF`)
- Text: Binance Yellow (`#F0B90B`)
- Border: 1px solid `#F0B90B`
- Border radius: full pill (50px)
- Padding: 10px horizontal
- Shadow: `rgb(153,153,153) 0px 2px 10px -3px`
- Hover: shifts to Focus Blue bg, white text
**Disabled**
- Background: `#E6E8EA`
- Text: `#848E9C`
- Cursor: not-allowed
### Cards & Containers
- Background: White (`#FFFFFF`) on light sections, Dark Card (`#2B2F36`) on dark sections
- Border: 1px solid `#E6E8EA` on light cards
- Border radius: medium rounded (12px) for content cards, tight (8px) for data cards
- Shadow: `rgba(32, 32, 37, 0.05) 0px 3px 5px 0px` — barely visible, trust-building
- Hover: shadow intensifies to `rgba(8, 8, 8, 0.05) 0px 3px 5px 5px`
- Transition: box-shadow 200ms ease
### Inputs & Forms
- Background: White (`#FFFFFF`) or Snow (`#F5F5F5`)
- Text: Ink (`#1E2026`)
- Border: 1px solid `#E6E8EA`
- Border radius: 8px
- Padding: 0px 12px (compact for trading context)
- Focus: border shifts to black (`#000000`), 1px outline
- Placeholder: Slate (`#848E9C`)
- Transition: border-color 200ms ease
### Navigation
- Background: White (`#FFFFFF`), sticky
- Height: ~64px
- Left: Binance logo (SVG, yellow mark + dark wordmark)
- Center/Right: navigation links in 14px/600 BinancePlex, color `#32313A`
- CTA: Yellow pill button "Get Started" in nav right
- Hover: links darken to `#1A1A1A`
- Mobile: hamburger menu, full-height overlay
- Top: optional promotional banner bar
### Image Treatment
- Product mockups: device frames on golden gradient backgrounds, floating with subtle shadow
- Hero images: full-width contained within card-like areas with rounded corners (24px)
- Video sections: 24px radius with embedded player controls
- App screenshots: dark-themed trading UI shown within phone/tablet bezels
- Crypto icons: 48px circular with brand colors
### Trust Indicators
- Real-time crypto price ticker (BTC, BNB, SOL with green/red price change)
- "Trusted by millions" section with statistics on dark background
- Security badges and regulatory compliance mentions
- QR code for direct app download in footer
## 5. Layout Principles
### Spacing System
Base unit: 8px
| Token | Value | Use |
|-------|-------|-----|
| space-1 | 4px | Tight inline gaps, icon padding |
| space-2 | 8px | Base unit, button icon gaps, tight margins |
| space-3 | 12px | Card internal padding, input padding |
| space-4 | 16px | Standard padding, section margins |
| space-5 | 20px | Card gaps, medium padding |
| space-6 | 24px | Section internal padding |
| space-7 | 32px | Section breaks, large padding |
| space-8 | 48px | Major section padding |
| space-9 | 64px | Hero section padding |
| space-10 | 80px | Large section spacing |
### Grid & Container
- Max container width: 1200px (centered)
- Hero area: single column with side-by-side text + image above 1024px
- Feature grid: 3-column on desktop, single column on mobile
- Product showcase: 2-column (text + device mockup)
- Horizontal padding: 32px desktop, 16px mobile
- Grid gap: 24px between feature cards
### Whitespace Philosophy
Binance.US uses whitespace as a trust signal. Generous padding around the hero section and between content blocks creates a sense of spaciousness that counters the information density typically associated with crypto exchanges. The light sections breathe — wide margins around headlines and ample spacing between cards — while dark sections compress, packing features into tighter grids to convey capability and depth. The overall rhythm alternates between "inviting entry" (light, spacious) and "deep functionality" (dark, dense).
### Border Radius Scale
| Value | Context |
|-------|---------|
| 1px | Subtle edge softening, fine UI elements |
| 2px | Close buttons, micro-interactive elements |
| 6px | Primary buttons (non-pill), small cards |
| 8px | Form inputs, data cards, image containers |
| 10px | Navigation pills, tag containers |
| 12px | Content cards, feature containers |
| 24px | Video containers, hero imagery, large cards |
| 50px | Pill buttons (CTA), search inputs, full-round elements |
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat | No shadow, solid background | Default for inline elements |
| Subtle | `rgba(32, 32, 37, 0.05) 0px 3px 5px` | Content cards, resting state |
| Medium | `rgba(8, 8, 8, 0.05) 0px 3px 5px 5px` | Hovered cards, elevated containers |
| Pill Shadow | `rgb(153,153,153) 0px 2px 10px -3px` | Pill CTA buttons, floating actions |
| Heavy | `rgba(0,0,0) 0px 32px 37px` | Modal overlays, dropdown menus |
Binance.US uses a whisper-light shadow system. Card shadows are barely perceptible at 5% opacity — they exist not for dramatic depth but as subtle ground cues that keep cards from feeling pasted onto the surface. The pill button shadow is the exception: slightly more visible to give CTAs a "floating" quality that invites clicks. The philosophy is pragmatic — in a financial context, heavy shadows feel frivolous, while no shadows at all feel flat and untrustworthy. The 5% sweet spot communicates professionalism.
### Decorative Depth
- **Golden gradient backgrounds**: Behind device mockup sections, radial golden glow centered on the product
- **Dark-to-light section transitions**: Hard cut (no gradient blend) between white and `#222126` sections
- **Price ticker strip**: Flat, borderless, reads as a data bar rather than a decorative element
## 7. Do's and Don'ts
### Do
- Use Binance Yellow (`#F0B90B`) exclusively for primary CTAs and brand accents — it's the single point of color
- Keep light and dark sections strictly alternating for visual rhythm
- Use BinancePlex at weight 500+ for all interactive elements — this is a confidence-forward design
- Apply 50px radius to all primary CTA pill buttons — the signature interactive shape
- Maintain 12px radius on content cards for a polished but not overly rounded feel
- Show real-time data prominently (prices, percentages, stats) — numbers build trust
- Use Slate (`#848E9C`) for all secondary/metadata text — the universal quiet voice
- Keep shadows at 5% opacity or less — barely there but present
### Don't
- Don't introduce additional brand colors — Binance Yellow is the only accent; all other color is data-driven (green up, red down)
- Don't use rounded corners above 12px on content cards — only CTAs and video containers go higher
- Don't add heavy shadows or hover lift effects — this is a restrained financial platform
- Don't use BinancePlex below weight 500 for headings — lighter weights undermine authority
- Don't place yellow text on yellow backgrounds — always ensure high contrast pairing
- Don't mix pill (50px) and square (6px) button styles in the same row
- Don't soften the dark sections — `#222126` should feel authoritative, not grey
- Don't use decorative illustrations — imagery should be product screenshots or data visualizations
- Don't add animation beyond subtle transitions (200ms ease) — financial platforms need stability
- Don't use colored backgrounds for semantic states in cards — keep cards white or dark, use text color for semantic meaning
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <425px | Single column, stacked hero, hamburger nav, 16px padding |
| Small Mobile | 425-599px | Wider mobile layout, price ticker wraps |
| Tablet Small | 600-768px | 2-column feature grid begins |
| Tablet | 769-896px | Hero side-by-side layout begins |
| Desktop Small | 897-1024px | Full nav expands, 3-column features |
| Desktop | 1024-1280px | Full layout, max content width |
| Large Desktop | 1280-1440px | Increased margins, centered container |
| XL Desktop | >1440px | Max-width container (1200px) with expanded margins |
### Touch Targets
- Minimum touch target: 44x44px (WCAG AAA)
- Pill CTA buttons: 48px height minimum
- Nav links: 44px touch area
- Crypto ticker items: full-width tappable rows on mobile
- App download buttons: large tap zones (50px+)
### Collapsing Strategy
- **Navigation**: Full horizontal links → hamburger menu below 897px; logo and "Get Started" CTA remain visible
- **Hero section**: Side-by-side (text left, image right) → stacked (text top, image below) at 768px
- **Feature grid**: 3-col → 2-col at 768px → 1-col at 600px
- **Price ticker**: Horizontal row → wrapping or scrollable at 600px
- **Section padding**: 64px → 48px → 32px → 16px as viewport narrows
- **Device mockups**: Scale down proportionally, maintain centered positioning
- **Footer**: Multi-column → stacked accordion sections on mobile
### Image Behavior
- Device mockups: CSS-scaled with max-width constraints, maintain aspect ratio
- Hero imagery: contained within rounded containers (24px), scale proportionally
- App screenshots: responsive width with fixed aspect ratio
- QR code: fixed 120px square, hidden on mobile (replaced with direct app store links)
## 9. Agent Prompt Guide
### Quick Color Reference
- Primary CTA: Binance Yellow (`#F0B90B`)
- Secondary CTA: Binance Gold (`#FFD000`)
- Background Light: Pure White (`#FFFFFF`)
- Background Dark: Binance Dark (`#222126`)
- Heading text: Ink (`#1E2026`)
- Body text: Slate (`#848E9C`)
- Border: Border Light (`#E6E8EA`)
- Positive: Crypto Green (`#0ECB81`)
- Negative: Crypto Red (`#F6465D`)
### Example Component Prompts
- "Create a hero section with white background, a 60px/700 bold headline in Ink (#1E2026), a 20px/500 subtitle in Slate (#848E9C), and a Binance Yellow (#F0B90B) pill button (50px radius) with dark text (#1E2026)"
- "Design a crypto price ticker strip showing BTC, BNB, SOL prices in 14px/600 Ink (#1E2026) with green (#0ECB81) or red (#F6465D) percentage changes, on a white background with #E6E8EA bottom border"
- "Build a feature card grid (3-column, 24px gap) with 12px radius white cards, subtle shadow (rgba(32,32,37,0.05) 0px 3px 5px), each containing a yellow (#F0B90B) icon, 20px/600 heading, and 14px/500 #848E9C description"
- "Create a dark section (#222126) with a 34px/700 white headline centered, and a 3-column feature grid using dark cards (#2B2F36) with 12px radius and yellow (#F0B90B) accent icons"
- "Design a sticky navigation bar with white background, Binance logo left, 14px/600 #32313A nav links center, and a yellow (#F0B90B) pill button (50px radius, 6px padding 32px) labeled 'Get Started' right"
### Iteration Guide
When refining existing screens generated with this design system:
1. Focus on ONE component at a time
2. Reference specific color names and hex codes from this document
3. Remember: Binance Yellow (#F0B90B) is the ONLY accent color — everything else is grey/dark/white
4. Use the dark/light section alternation for visual pacing
5. Numbers and data should be prominent — this is a financial platform
6. Pill buttons (50px radius) for CTAs, regular buttons (6px radius) for form actions
7. Keep shadows almost invisible (5% opacity) — trust comes from clarity, not depth
8. BinancePlex at 600+ weight for any text that needs to feel authoritative

24
app/.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "postcss.config.js",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>期权希腊字母定价引擎</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

@ -0,0 +1,31 @@
Using Node.js 20, Tailwind CSS v3.4.19, and Vite v7.2.4
Tailwind CSS has been set up with the shadcn theme
Setup complete: /mnt/agents/output/app
Components (40+):
accordion, alert-dialog, alert, aspect-ratio, avatar, badge, breadcrumb,
button-group, button, calendar, card, carousel, chart, checkbox, collapsible,
command, context-menu, dialog, drawer, dropdown-menu, empty, field, form,
hover-card, input-group, input-otp, input, item, kbd, label, menubar,
navigation-menu, pagination, popover, progress, radio-group, resizable,
scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner,
spinner, switch, table, tabs, textarea, toggle-group, toggle, tooltip
Usage:
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
Structure:
src/sections/ Page sections
src/hooks/ Custom hooks
src/types/ Type definitions
src/App.css Styles specific to the Webapp
src/App.tsx Root React component
src/index.css Global styles
src/main.tsx Entry point for rendering the Webapp
index.html Entry point for the Webapp
tailwind.config.js Configures Tailwind's theme, plugins, etc.
vite.config.ts Main build and dev server settings for Vite
postcss.config.js Config file for CSS post-processing tools

8274
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,80 @@
{
"name": "my-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.562.0",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.70.0",
"react-resizable-panels": "^4.2.2",
"react-router": "^7.6.1",
"recharts": "^2.15.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"vaul": "^1.1.2",
"zod": "^4.3.5"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"kimi-plugin-inspect-react": "^1.0.3",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -0,0 +1 @@
/* App-specific styles */

@ -0,0 +1,81 @@
import { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Calculator, ArrowLeftRight, TrendingUp, BarChart3, BookOpen, Clock } from 'lucide-react';
import Header from '@/sections/Header';
import CalculatorSection from '@/sections/CalculatorSection';
import ReverseSolver from '@/sections/ReverseSolver';
import GreeksAnalysis from '@/sections/GreeksAnalysis';
import Visualization from '@/sections/Visualization';
import GuideSection from '@/sections/GuideSection';
import TimeDecayResearch from '@/sections/TimeDecayResearch';
function App() {
const [activeTab, setActiveTab] = useState('calculator');
return (
<div className="min-h-screen bg-white text-[#1E2026]">
<Header />
<main className="container mx-auto px-8 py-8 max-w-[1200px]">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-6 bg-white border border-[#E6E8EA] rounded-[6px]">
<TabsTrigger value="calculator" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]">
<Calculator className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="reverse" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]">
<ArrowLeftRight className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="sensitivity" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]">
<TrendingUp className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="timedecay" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]">
<Clock className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="visualization" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]">
<BarChart3 className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="guide" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]">
<BookOpen className="w-4 h-4 mr-2" />
</TabsTrigger>
</TabsList>
<TabsContent value="calculator" className="mt-8">
<CalculatorSection />
</TabsContent>
<TabsContent value="reverse" className="mt-8">
<ReverseSolver />
</TabsContent>
<TabsContent value="sensitivity" className="mt-8">
<GreeksAnalysis />
</TabsContent>
<TabsContent value="timedecay" className="mt-8">
<TimeDecayResearch />
</TabsContent>
<TabsContent value="visualization" className="mt-8">
<Visualization />
</TabsContent>
<TabsContent value="guide" className="mt-8">
<GuideSection />
</TabsContent>
</Tabs>
</main>
<footer className="border-t border-[#E6E8EA] mt-16 py-8 text-center text-[#848E9C] text-sm">
<p> Black-Scholes-Merton (1973) | 使</p>
</footer>
</div>
);
}
export default App;

@ -0,0 +1,64 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

@ -0,0 +1,155 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

@ -0,0 +1,66 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

@ -0,0 +1,11 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
}
export { AspectRatio }

@ -0,0 +1,51 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

@ -0,0 +1,83 @@
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "div"
return (
<Comp
className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}

@ -0,0 +1,62 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

@ -0,0 +1,220 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

@ -0,0 +1,239 @@
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

@ -0,0 +1,357 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

@ -0,0 +1,31 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

@ -0,0 +1,182 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

@ -0,0 +1,252 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

@ -0,0 +1,141 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

@ -0,0 +1,255 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

@ -0,0 +1,104 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}

@ -0,0 +1,246 @@
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import type * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }

@ -0,0 +1,170 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}

@ -0,0 +1,75 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
)
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
)
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
)
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
)
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

@ -0,0 +1,193 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
)
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

@ -0,0 +1,28 @@
import { cn } from "@/lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

@ -0,0 +1,274 @@
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
)
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}

@ -0,0 +1,168 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants, type Button } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

@ -0,0 +1,29 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

@ -0,0 +1,54 @@
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.Group>) {
return (
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.Separator>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

@ -0,0 +1,188 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

@ -0,0 +1,137 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

@ -0,0 +1,726 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

@ -0,0 +1,63 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
}
export { Slider }

@ -0,0 +1,38 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

@ -0,0 +1,16 @@
import { Loader2Icon } from "lucide-react"
import { cn } from "@/lib/utils"
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
}
export { Spinner }

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

@ -0,0 +1,81 @@
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
spacing?: number
}
>({
size: "default",
variant: "default",
spacing: 0,
})
function ToggleGroup({
className,
variant,
size,
spacing = 0,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & {
spacing?: number
}) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }

@ -0,0 +1,45 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

@ -0,0 +1,95 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0f172a;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #475569;
}
/* Number input styling */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
opacity: 0.5;
}

@ -0,0 +1,794 @@
/**
* Black-Scholes-Merton
* r=0, q=00
*
*
* Delta, Gamma, Theta, Vega, Rho
* Vanna, Vomma, Charm
*/
function erf(x: number): number {
const sign = x >= 0 ? 1 : -1;
x = Math.abs(x);
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const t = 1 / (1 + p * x);
const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return sign * y;
}
export function normCDF(x: number): number {
return 0.5 * (1 + erf(x / Math.SQRT2));
}
export function normPDF(x: number): number {
return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
}
// Inverse CDF (quantile function)
export function normInverse(p: number): number {
if (p <= 0) return -6;
if (p >= 1) return 6;
if (p === 0.5) return 0;
const a1 = -3.969683028665376e+01;
const a2 = 2.209460984245205e+02;
const a3 = -2.759285104469687e+02;
const a4 = 1.383577518672690e+02;
const a5 = -3.066479806614716e+01;
const a6 = 2.506628277459239e+00;
const b1 = -5.447609879822406e+01;
const b2 = 1.615858368580409e+02;
const b3 = -1.556989798598866e+02;
const b4 = 6.680131188771972e+01;
const b5 = -1.328068155288572e+01;
const c1 = -7.784894002430293e-03;
const c2 = -3.223964580411365e-01;
const c3 = -2.400758277161838e+00;
const c4 = -2.549732539343734e+00;
const c5 = 4.374664141464968e+00;
const c6 = 2.938163982698783e+00;
const d1 = 7.784695709041462e-03;
const d2 = 3.224671290700398e-01;
const d3 = 2.445134137142996e+00;
const d4 = 3.754408661907416e+00;
const p_low = 0.02425;
const p_high = 1 - p_low;
let qv: number, r: number, x: number;
if (p < p_low) {
qv = Math.sqrt(-2 * Math.log(p));
x = (((((c1 * qv + c2) * qv + c3) * qv + c4) * qv + c5) * qv + c6) /
((((d1 * qv + d2) * qv + d3) * qv + d4) * qv + 1);
} else if (p <= p_high) {
qv = p - 0.5;
r = qv * qv;
x = (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * qv /
(((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1);
} else {
qv = Math.sqrt(-2 * Math.log(1 - p));
x = -(((((c1 * qv + c2) * qv + c3) * qv + c4) * qv + c5) * qv + c6) /
((((d1 * qv + d2) * qv + d3) * qv + d4) * qv + 1);
}
const e = normCDF(x) - p;
const u = e * Math.sqrt(2 * Math.PI) * Math.exp(x * x / 2);
x = x - u / (1 + x * u / 2);
return x;
}
export interface BSParams {
S: number;
K: number;
T: number;
sigma: number;
}
export interface BSGreeks {
price: number;
delta: number;
gamma: number;
theta: number;
thetaDaily: number;
vega: number;
rho: number;
vanna: number;
vomma: number;
charm: number;
speed: number;
zomma: number;
color: number;
itmProb: number;
intrinsic: number;
timeValue: number;
d1: number;
d2: number;
}
function calcD1D2(S: number, K: number, T: number, sigma: number): { d1: number; d2: number } {
if (T <= 0 || sigma <= 0 || S <= 0 || K <= 0) return { d1: 0, d2: 0 };
const lnSK = Math.log(S / K);
const d1 = (lnSK + 0.5 * sigma * sigma * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
return { d1, d2 };
}
export function calculateBSM(p: BSParams, optionType: 'call' | 'put'): BSGreeks {
const { S, K, T, sigma } = p;
if (T <= 0 || sigma <= 0) {
const intrinsic = optionType === 'call' ? Math.max(S - K, 0) : Math.max(K - S, 0);
return {
price: intrinsic, delta: optionType === 'call' ? (S > K ? 1 : 0) : (S < K ? -1 : 0),
gamma: 0, theta: 0, thetaDaily: 0, vega: 0, rho: 0,
vanna: 0, vomma: 0, charm: 0, speed: 0, zomma: 0, color: 0,
itmProb: optionType === 'call' ? (S > K ? 1 : 0) : (S < K ? 1 : 0),
intrinsic, timeValue: 0, d1: 0, d2: 0,
};
}
const { d1, d2 } = calcD1D2(S, K, T, sigma);
const nd1 = normPDF(d1);
const Nd1 = normCDF(d1);
const Nd2 = normCDF(d2);
const Nmd1 = normCDF(-d1);
const Nmd2 = normCDF(-d2);
let price: number, intrinsic: number;
if (optionType === 'call') {
price = S * Nd1 - K * Nd2;
intrinsic = Math.max(S - K, 0);
} else {
price = K * Nmd2 - S * Nmd1;
intrinsic = Math.max(K - S, 0);
}
const timeValue = Math.max(price - intrinsic, 0);
const delta = optionType === 'call' ? Nd1 : Nd1 - 1;
const gamma = nd1 / (S * sigma * Math.sqrt(T));
const volTerm = -(S * nd1 * sigma) / (2 * Math.sqrt(T));
const theta = optionType === 'call' ? volTerm : volTerm;
const vega = S * nd1 * Math.sqrt(T) * 0.01;
const rho = optionType === 'call' ? K * T * Nd2 * 0.01 : -K * T * Nmd2 * 0.01;
const vanna = -(d2 / sigma) * nd1;
const vomma = vega * (d1 * d2) / sigma;
const charm = optionType === 'call' ? -nd1 * (2 * T - d2 * sigma * Math.sqrt(T)) / (2 * T * sigma * Math.sqrt(T)) : nd1 / (T * sigma * Math.sqrt(T)) - nd1 * (2 * T - d2 * sigma * Math.sqrt(T)) / (2 * T * sigma * Math.sqrt(T));
const speed = -gamma / S * (d1 / (sigma * Math.sqrt(T)) + 1);
const zomma = gamma * (d1 * d2 - 1) / sigma;
const color = gamma * ((2 * T - d2 * sigma * Math.sqrt(T)) / (2 * T * sigma * Math.sqrt(T)) + (1 - d1 * d2) / T);
const itmProb = optionType === 'call' ? Nd2 : Nmd2;
return {
price, delta, gamma, theta, thetaDaily: theta / 365, vega, rho,
vanna, vomma, charm, speed, zomma, color, itmProb, intrinsic, timeValue, d1, d2,
};
}
// ============================================================
// INVERSE SOLVERS
// ============================================================
export function solveImpliedVolatility(
targetPrice: number, S: number, K: number, T: number, optionType: 'call' | 'put',
maxIter: number = 100, tolerance: number = 1e-8
): number | null {
if (T <= 0 || targetPrice <= 0) return null;
let sigma = Math.sqrt(2 * Math.PI / T) * (targetPrice / S);
if (!isFinite(sigma) || sigma <= 0) sigma = 0.2;
for (let i = 0; i < maxIter; i++) {
const greeks = calculateBSM({ S, K, T, sigma }, optionType);
const diff = greeks.price - targetPrice;
if (Math.abs(diff) < tolerance) return sigma;
const vegaRaw = S * normPDF(greeks.d1) * Math.sqrt(T);
if (Math.abs(vegaRaw) < 1e-10) return null;
sigma = sigma - diff / vegaRaw;
if (sigma <= 0) sigma = 0.001;
if (sigma > 5) sigma = 5;
}
return null;
}
export function solveSpotPrice(
targetPrice: number, K: number, T: number, sigma: number, optionType: 'call' | 'put',
maxIter: number = 100, tolerance: number = 1e-8
): number | null {
if (T <= 0 || sigma <= 0 || targetPrice < 0) return null;
let low = 0.01, high = optionType === 'call' ? Math.max(K * 3, K + targetPrice * 2) : K * 2;
for (let i = 0; i < maxIter; i++) {
const mid = (low + high) / 2;
const greeks = calculateBSM({ S: mid, K, T, sigma }, optionType);
const diff = greeks.price - targetPrice;
if (Math.abs(diff) < tolerance) return mid;
if (optionType === 'call') { if (diff > 0) high = mid; else low = mid; }
else { if (diff > 0) low = mid; else high = mid; }
if (high - low < tolerance) return mid;
}
return (low + high) / 2;
}
export function solveSigmaFromDelta(
S: number, K: number, T: number, targetDelta: number, optionType: 'call' | 'put',
maxIter: number = 100, tolerance: number = 1e-8
): number | null {
if (T <= 0 || S <= 0 || K <= 0) return null;
const adjDelta = optionType === 'call' ? targetDelta : targetDelta + 1;
if (adjDelta <= 0 || adjDelta >= 1) return null;
const targetD1 = normInverse(adjDelta);
let sigma = 0.2;
for (let i = 0; i < maxIter; i++) {
const sqrtT = Math.sqrt(T);
const lnSK = Math.log(S / K);
const drift = 0.5 * sigma * sigma * T;
const d1 = (lnSK + drift) / (sigma * sqrtT);
const diff = d1 - targetD1;
if (Math.abs(diff) < tolerance) return sigma;
const dd1_dsigma = (sigma * sigma * T - (lnSK + drift)) / (sigma * sigma * sqrtT);
if (Math.abs(dd1_dsigma) < 1e-10) return null;
sigma = sigma - diff / dd1_dsigma;
if (sigma <= 0) sigma = 0.001;
if (sigma > 5) sigma = 5;
}
return null;
}
export function solveSigmaFromGamma(
S: number, K: number, T: number, targetGamma: number,
maxIter: number = 100, tolerance: number = 1e-8
): number | null {
if (T <= 0 || S <= 0 || targetGamma <= 0) return null;
let sigma = 0.2;
for (let i = 0; i < maxIter; i++) {
const greeks = calculateBSM({ S, K, T, sigma }, 'call');
const diff = greeks.gamma - targetGamma;
if (Math.abs(diff) < tolerance) return sigma;
const ds = 1e-6;
const gUp = calculateBSM({ S, K, T, sigma: sigma + ds }, 'call');
const dGamma = (gUp.gamma - greeks.gamma) / ds;
if (Math.abs(dGamma) < 1e-10) return null;
sigma = sigma - diff / dGamma;
if (sigma <= 0) sigma = 0.001;
if (sigma > 5) sigma = 5;
}
return null;
}
export function solveTFromDelta(
S: number, K: number, sigma: number, targetDelta: number, optionType: 'call' | 'put',
maxIter: number = 200, tolerance: number = 1e-8
): number | null {
if (S <= 0 || K <= 0 || sigma <= 0) return null;
const adjDelta = optionType === 'call' ? targetDelta : targetDelta + 1;
if (adjDelta <= 0 || adjDelta >= 1) return null;
const targetD1 = normInverse(Math.max(0.0001, Math.min(0.9999, adjDelta)));
let low = 1 / 365, high = 3.0;
const lnSK = Math.log(S / K);
const driftRate = 0.5 * sigma * sigma;
for (let i = 0; i < maxIter; i++) {
const T = (low + high) / 2;
const fT = (lnSK + driftRate * T) / (sigma * Math.sqrt(T)) - targetD1;
if (Math.abs(fT) < tolerance) return T;
if (fT > 0) { if (lnSK > 0) high = T; else low = T; }
else { if (lnSK > 0) low = T; else high = T; }
if (high - low < tolerance) return T;
}
return (low + high) / 2;
}
export function solveTFromPrice(
targetPrice: number, S: number, K: number, sigma: number, optionType: 'call' | 'put',
maxIter: number = 100, tolerance: number = 1e-8
): number | null {
if (S <= 0 || K <= 0 || sigma <= 0 || targetPrice <= 0) return null;
let low = 1 / 365, high = 5.0;
for (let i = 0; i < maxIter; i++) {
const T = (low + high) / 2;
const greeks = calculateBSM({ S, K, T, sigma }, optionType);
const diff = greeks.price - targetPrice;
if (Math.abs(diff) < tolerance) return T;
if (diff > 0) high = T; else low = T;
if (high - low < tolerance) return T;
}
return (low + high) / 2;
}
// ============================================================
// MULTI-CONSTRAINT SOLVERS
// ============================================================
export function solvePriceFromGreeksAndS(
S: number, K: number, contractT: number, optionType: 'call' | 'put',
targets: { delta?: number; gamma?: number; theta?: number; vega?: number; rho?: number; },
thetaUnit: 'annual' | 'daily' = 'daily'
): {
sigma: number; T: number; DTE: number; price: number;
greeks: BSGreeks;
residuals: { delta?: number; gamma?: number; theta?: number; vega?: number; rho?: number };
T_from_theta: boolean;
} | null {
if (S <= 0 || K <= 0) return null;
const targetThetaAnnual = targets.theta !== undefined
? (thetaUnit === 'daily' ? targets.theta * 365 : targets.theta)
: undefined;
const hasThetaConstraint = targetThetaAnnual !== undefined;
if (!hasThetaConstraint) {
if (contractT <= 0) return null;
const estimates: number[] = [];
const weights: number[] = [];
if (targets.delta !== undefined) {
const s = solveSigmaFromDelta(S, K, contractT, targets.delta, optionType);
if (s !== null) { estimates.push(s); weights.push(2.0); }
}
if (targets.gamma !== undefined) {
const s = solveSigmaFromGamma(S, K, contractT, targets.gamma);
if (s !== null) { estimates.push(s); weights.push(1.5); }
}
if (estimates.length === 0) return null;
let sigma = estimates.reduce((sum, s, i) => sum + s * weights[i], 0) / weights.reduce((a, b) => a + b, 0);
for (let iter = 0; iter < 500; iter++) {
const greeks = calculateBSM({ S, K, T: contractT, sigma }, optionType);
let grad = 0;
if (targets.delta !== undefined) {
const dDiff = greeks.delta - targets.delta;
grad += 2.0 * dDiff * ((greeks.vega / 0.01) / (S * sigma));
}
if (targets.gamma !== undefined) {
const gDiff = greeks.gamma - targets.gamma;
const ds = 1e-6;
const gUp = calculateBSM({ S, K, T: contractT, sigma: sigma + ds }, optionType);
grad += 1.5 * gDiff * ((gUp.gamma - greeks.gamma) / ds);
}
if (targets.vega !== undefined) {
const vDiff = greeks.vega - targets.vega;
grad += 1.0 * vDiff * greeks.vomma;
}
const stepSize = 0.01 / (1 + iter * 0.001);
sigma = sigma - grad * stepSize;
if (sigma <= 0.001) sigma = 0.001;
if (sigma > 5) sigma = 5;
const loss = Math.sqrt(
(targets.delta !== undefined ? Math.pow(greeks.delta - targets.delta, 2) : 0) +
(targets.gamma !== undefined ? Math.pow(greeks.gamma - targets.gamma, 2) : 0) +
(targets.vega !== undefined ? Math.pow(greeks.vega - targets.vega, 2) : 0)
);
if (loss < 1e-6) break;
}
const finalGreeks = calculateBSM({ S, K, T: contractT, sigma }, optionType);
return {
sigma, T: contractT, DTE: Math.round(contractT * 365),
price: finalGreeks.price, greeks: finalGreeks,
residuals: {
delta: targets.delta !== undefined ? finalGreeks.delta - targets.delta : undefined,
gamma: targets.gamma !== undefined ? finalGreeks.gamma - targets.gamma : undefined,
theta: undefined, vega: targets.vega !== undefined ? finalGreeks.vega - targets.vega : undefined,
rho: targets.rho !== undefined ? finalGreeks.rho - targets.rho : undefined,
},
T_from_theta: false,
};
}
// Joint optimization of (sigma, T) when Theta is provided
let bestSigma = 0.2, bestT = contractT, bestLoss = Infinity;
const T_candidates: number[] = [];
for (let d = 1; d <= 30; d++) T_candidates.push(d / 365);
for (let d = 35; d <= 90; d += 5) T_candidates.push(d / 365);
for (let d = 100; d <= 365; d += 15) T_candidates.push(d / 365);
for (let y = 1.1; y <= 3.0; y += 0.2) T_candidates.push(y);
if (!T_candidates.includes(contractT)) T_candidates.push(contractT);
T_candidates.sort((a, b) => a - b);
for (const Tc of T_candidates) {
let sigma_c: number | null = null;
if (targets.delta !== undefined) sigma_c = solveSigmaFromDelta(S, K, Tc, targets.delta, optionType);
if (sigma_c === null && targets.gamma !== undefined) sigma_c = solveSigmaFromGamma(S, K, Tc, targets.gamma);
if (sigma_c === null) {
for (let s = 0.05; s <= 1.0; s += 0.05) {
const g = calculateBSM({ S, K, T: Tc, sigma: s }, optionType);
let loss = Math.pow(g.theta - targetThetaAnnual, 2);
if (targets.delta !== undefined) loss += Math.pow(g.delta - targets.delta, 2) * 2.0;
if (targets.gamma !== undefined) loss += Math.pow(g.gamma - targets.gamma, 2) * 1.5;
if (targets.vega !== undefined) loss += Math.pow(g.vega - targets.vega, 2);
if (loss < bestLoss) { bestLoss = loss; bestSigma = s; bestT = Tc; }
}
continue;
}
const g = calculateBSM({ S, K, T: Tc, sigma: sigma_c }, optionType);
let loss = Math.pow(g.theta - targetThetaAnnual, 2);
if (targets.delta !== undefined) loss += Math.pow(g.delta - targets.delta, 2) * 2.0;
if (targets.gamma !== undefined) loss += Math.pow(g.gamma - targets.gamma, 2) * 1.5;
if (targets.vega !== undefined) loss += Math.pow(g.vega - targets.vega, 2);
if (targets.rho !== undefined) loss += Math.pow(g.rho - targets.rho, 2) * 0.5;
if (loss < bestLoss) { bestLoss = loss; bestSigma = sigma_c; bestT = Tc; }
}
// Refinement
for (let refine = 0; refine < 3; refine++) {
const T_range = Math.max(bestT * 0.2, 5 / 365);
const T_min = Math.max(1 / 365, bestT - T_range);
const T_max = Math.min(3.0, bestT + T_range);
const T_step = (T_max - T_min) / 20;
for (let i = 0; i <= 20; i++) {
const T_local = T_min + i * T_step;
let sigma_local: number | null = null;
if (targets.delta !== undefined) sigma_local = solveSigmaFromDelta(S, K, T_local, targets.delta, optionType);
if (sigma_local === null && targets.gamma !== undefined) sigma_local = solveSigmaFromGamma(S, K, T_local, targets.gamma);
if (sigma_local === null) continue;
const g = calculateBSM({ S, K, T: T_local, sigma: sigma_local }, optionType);
let loss = Math.pow(g.theta - targetThetaAnnual, 2);
if (targets.delta !== undefined) loss += Math.pow(g.delta - targets.delta, 2) * 2.0;
if (targets.gamma !== undefined) loss += Math.pow(g.gamma - targets.gamma, 2) * 1.5;
if (targets.vega !== undefined) loss += Math.pow(g.vega - targets.vega, 2);
if (targets.rho !== undefined) loss += Math.pow(g.rho - targets.rho, 2) * 0.5;
if (loss < bestLoss) { bestLoss = loss; bestSigma = sigma_local; bestT = T_local; }
}
}
// Gradient descent on both
for (let iter = 0; iter < 200; iter++) {
const greeks = calculateBSM({ S, K, T: bestT, sigma: bestSigma }, optionType);
const ds = 1e-6, dT = 1e-5;
const gSu = calculateBSM({ S, K, T: bestT, sigma: bestSigma + ds }, optionType);
const gSd = calculateBSM({ S, K, T: bestT, sigma: Math.max(bestSigma - ds, 0.001) }, optionType);
const gTu = calculateBSM({ S, K, T: Math.min(bestT + dT, 3.0), sigma: bestSigma }, optionType);
const gTd = calculateBSM({ S, K, T: Math.max(bestT - dT, 1/365), sigma: bestSigma }, optionType);
let loss = 0, gradSigma = 0, gradT = 0;
if (targets.delta !== undefined) {
const dDiff = greeks.delta - targets.delta;
loss += 2.0 * dDiff * dDiff;
gradSigma += 4.0 * dDiff * ((gSu.delta - gSd.delta) / (2 * ds));
gradT += 4.0 * dDiff * ((gTu.delta - gTd.delta) / (2 * dT));
}
if (targetThetaAnnual !== undefined) {
const tDiff = greeks.theta - targetThetaAnnual;
loss += 1.0 * tDiff * tDiff;
gradSigma += 2.0 * tDiff * ((gSu.theta - gSd.theta) / (2 * ds));
gradT += 2.0 * tDiff * ((gTu.theta - gTd.theta) / (2 * dT));
}
if (targets.gamma !== undefined) {
const gDiff = greeks.gamma - targets.gamma;
loss += 1.5 * gDiff * gDiff;
gradSigma += 3.0 * gDiff * ((gSu.gamma - gSd.gamma) / (2 * ds));
gradT += 3.0 * gDiff * ((gTu.gamma - gTd.gamma) / (2 * dT));
}
if (targets.vega !== undefined) {
const vDiff = greeks.vega - targets.vega;
loss += 1.0 * vDiff * vDiff;
gradSigma += 2.0 * vDiff * ((gSu.vega - gSd.vega) / (2 * ds));
gradT += 2.0 * vDiff * ((gTu.vega - gTd.vega) / (2 * dT));
}
if (loss < 1e-10) break;
const step = 0.0005 / (1 + iter * 0.005);
bestSigma = Math.max(0.001, Math.min(5, bestSigma - gradSigma * step));
bestT = Math.max(1/365, Math.min(3.0, bestT - gradT * step));
}
const finalGreeks = calculateBSM({ S, K, T: bestT, sigma: bestSigma }, optionType);
const DTE = Math.round(bestT * 365);
return {
sigma: bestSigma, T: bestT, DTE,
price: finalGreeks.price, greeks: finalGreeks,
residuals: {
delta: targets.delta !== undefined ? finalGreeks.delta - targets.delta : undefined,
gamma: targets.gamma !== undefined ? finalGreeks.gamma - targets.gamma : undefined,
theta: targets.theta !== undefined ? finalGreeks.thetaDaily - (thetaUnit === 'daily' ? targets.theta : targets.theta / 365) : undefined,
vega: targets.vega !== undefined ? finalGreeks.vega - targets.vega : undefined,
rho: targets.rho !== undefined ? finalGreeks.rho - targets.rho : undefined,
},
T_from_theta: true,
};
}
export function solveFromPriceAndGreeks(
targetPrice: number, K: number, contractT: number, optionType: 'call' | 'put',
targets: { delta?: number; gamma?: number; theta?: number; vega?: number; rho?: number; },
thetaUnit: 'annual' | 'daily' = 'daily'
): {
S: number; sigma: number; T: number; DTE: number;
greeks: BSGreeks;
residuals: { price: number; delta?: number; gamma?: number; theta?: number; vega?: number; rho?: number };
T_from_theta: boolean;
} | null {
if (contractT <= 0 || targetPrice < 0 || K <= 0) return null;
const targetThetaAnnual = targets.theta !== undefined
? (thetaUnit === 'daily' ? targets.theta * 365 : targets.theta)
: undefined;
const hasThetaConstraint = targetThetaAnnual !== undefined;
const T_candidates: number[] = hasThetaConstraint
? [1/365, 3/365, 7/365, 14/365, 21/365, 30/365, 45/365, 60/365, 90/365, 120/365, 180/365, 1.0, 2.0, contractT]
: [contractT];
let bestS = K, bestSigma = 0.2, bestT = contractT, bestLoss = Infinity;
for (const T_test of T_candidates) {
if (T_test <= 0) continue;
let S_min: number, S_max: number;
if (targets.delta !== undefined) {
const adjDelta = optionType === 'call' ? targets.delta : targets.delta + 1;
if (adjDelta > 0 && adjDelta < 1) {
const d1_est = normInverse(Math.max(0.001, Math.min(0.999, adjDelta)));
const S_est = K * Math.exp(d1_est * 0.3 * Math.sqrt(T_test) - 0.5 * 0.09 * T_test);
S_min = Math.max(S_est * 0.3, 0.01);
S_max = S_est * 3;
} else { S_min = K * 0.3; S_max = K * 3; }
} else { S_min = K * 0.3; S_max = K * 3; }
const steps = hasThetaConstraint ? 40 : 80;
const S_range = S_max - S_min;
for (let i = 0; i <= steps; i++) {
const S = S_min + (i / steps) * S_range;
let sigma: number | null = null;
if (targets.delta !== undefined) sigma = solveSigmaFromDelta(S, K, T_test, targets.delta, optionType);
if (sigma === null && targets.gamma !== undefined) sigma = solveSigmaFromGamma(S, K, T_test, targets.gamma);
if (sigma === null) sigma = solveImpliedVolatility(targetPrice, S, K, T_test, optionType);
if (sigma === null) continue;
const greeks = calculateBSM({ S, K, T: T_test, sigma }, optionType);
let loss = Math.pow(greeks.price - targetPrice, 2) * 3.0;
if (targets.delta !== undefined) loss += Math.pow(greeks.delta - targets.delta, 2) * 2.0;
if (targets.gamma !== undefined) loss += Math.pow(greeks.gamma - targets.gamma, 2) * 1.5;
if (targetThetaAnnual !== undefined) loss += Math.pow(greeks.theta - targetThetaAnnual, 2) * 0.5;
if (targets.vega !== undefined) loss += Math.pow(greeks.vega - targets.vega, 2) * 1.0;
if (targets.rho !== undefined) loss += Math.pow(greeks.rho - targets.rho, 2) * 0.3;
if (loss < bestLoss) { bestLoss = loss; bestS = S; bestSigma = sigma; bestT = T_test; }
}
}
// Refinement
for (let refine = 0; refine < 3; refine++) {
const localSMin = Math.max(bestS * 0.9, 0.01);
const localSMax = bestS * 1.1;
const localTMin = Math.max(bestT * 0.8, 1/365);
const localTMax = Math.min(bestT * 1.2, 3.0);
const localSRange = localSMax - localSMin;
for (let i = 0; i <= 20; i++) {
const S = localSMin + (i / 20) * localSRange;
if (hasThetaConstraint) {
const localTRange = localTMax - localTMin;
for (let j = 0; j <= 10; j++) {
const T_local = localTMin + (j / 10) * localTRange;
let sigma: number | null = null;
if (targets.delta !== undefined) sigma = solveSigmaFromDelta(S, K, T_local, targets.delta, optionType);
if (sigma === null && targets.gamma !== undefined) sigma = solveSigmaFromGamma(S, K, T_local, targets.gamma);
if (sigma === null) sigma = solveImpliedVolatility(targetPrice, S, K, T_local, optionType);
if (sigma === null) continue;
const greeks = calculateBSM({ S, K, T: T_local, sigma }, optionType);
let loss = Math.pow(greeks.price - targetPrice, 2) * 3.0;
if (targets.delta !== undefined) loss += Math.pow(greeks.delta - targets.delta, 2) * 2.0;
if (targets.gamma !== undefined) loss += Math.pow(greeks.gamma - targets.gamma, 2) * 1.5;
if (targetThetaAnnual !== undefined) loss += Math.pow(greeks.theta - targetThetaAnnual, 2) * 0.5;
if (targets.vega !== undefined) loss += Math.pow(greeks.vega - targets.vega, 2) * 1.0;
if (targets.rho !== undefined) loss += Math.pow(greeks.rho - targets.rho, 2) * 0.3;
if (loss < bestLoss) { bestLoss = loss; bestS = S; bestSigma = sigma; bestT = T_local; }
}
} else {
let sigma: number | null = null;
if (targets.delta !== undefined) sigma = solveSigmaFromDelta(S, K, bestT, targets.delta, optionType);
if (sigma === null && targets.gamma !== undefined) sigma = solveSigmaFromGamma(S, K, bestT, targets.gamma);
if (sigma === null) sigma = solveImpliedVolatility(targetPrice, S, K, bestT, optionType);
if (sigma === null) continue;
const greeks = calculateBSM({ S, K, T: bestT, sigma }, optionType);
let loss = Math.pow(greeks.price - targetPrice, 2) * 3.0;
if (targets.delta !== undefined) loss += Math.pow(greeks.delta - targets.delta, 2) * 2.0;
if (targets.gamma !== undefined) loss += Math.pow(greeks.gamma - targets.gamma, 2) * 1.5;
if (targetThetaAnnual !== undefined) loss += Math.pow(greeks.theta - targetThetaAnnual, 2) * 0.5;
if (targets.vega !== undefined) loss += Math.pow(greeks.vega - targets.vega, 2) * 1.0;
if (targets.rho !== undefined) loss += Math.pow(greeks.rho - targets.rho, 2) * 0.3;
if (loss < bestLoss) { bestLoss = loss; bestS = S; bestSigma = sigma; }
}
}
}
const finalGreeks = calculateBSM({ S: bestS, K, T: bestT, sigma: bestSigma }, optionType);
return {
S: bestS, sigma: bestSigma, T: bestT, DTE: Math.round(bestT * 365),
greeks: finalGreeks,
residuals: {
price: finalGreeks.price - targetPrice,
delta: targets.delta !== undefined ? finalGreeks.delta - targets.delta : undefined,
gamma: targets.gamma !== undefined ? finalGreeks.gamma - targets.gamma : undefined,
theta: targets.theta !== undefined ? finalGreeks.thetaDaily - (thetaUnit === 'daily' ? targets.theta : targets.theta / 365) : undefined,
vega: targets.vega !== undefined ? finalGreeks.vega - targets.vega : undefined,
rho: targets.rho !== undefined ? finalGreeks.rho - targets.rho : undefined,
},
T_from_theta: hasThetaConstraint,
};
}
export function solveWithTUnknown(
S: number, K: number, sigma: number, optionType: 'call' | 'put',
targets: { price?: number; delta?: number; gamma?: number; theta?: number; vega?: number; },
thetaUnit: 'annual' | 'daily' = 'daily'
): {
T: number; DTE: number; price: number;
greeks: BSGreeks;
residuals: { price?: number; delta?: number; gamma?: number; theta?: number; vega?: number };
} | null {
if (S <= 0 || K <= 0 || sigma <= 0) return null;
const targetThetaAnnual = targets.theta !== undefined
? (thetaUnit === 'daily' ? targets.theta * 365 : targets.theta)
: undefined;
let T: number | null = null;
if (targets.delta !== undefined) T = solveTFromDelta(S, K, sigma, targets.delta, optionType);
if (T === null && targets.price !== undefined) T = solveTFromPrice(targets.price, S, K, sigma, optionType);
if (T === null) return null;
// Refinement with gradient descent
for (let iter = 0; iter < 100; iter++) {
const dT = 0.0001;
const greeks = calculateBSM({ S, K, T, sigma }, optionType);
const gUp = calculateBSM({ S, K, T: Math.min(T + dT, 3.0), sigma }, optionType);
const gDown = calculateBSM({ S, K, T: Math.max(T - dT, 0.001), sigma }, optionType);
let loss = 0, dLoss_dT = 0;
if (targets.price !== undefined) {
const diff = greeks.price - targets.price;
loss += 3.0 * diff * diff;
dLoss_dT += 6.0 * diff * ((gUp.price - gDown.price) / (2 * dT));
}
if (targets.delta !== undefined) {
const diff = greeks.delta - targets.delta;
loss += 2.0 * diff * diff;
dLoss_dT += 4.0 * diff * ((gUp.delta - gDown.delta) / (2 * dT));
}
if (targets.gamma !== undefined) {
const diff = greeks.gamma - targets.gamma;
loss += 1.0 * diff * diff;
dLoss_dT += 2.0 * diff * ((gUp.gamma - gDown.gamma) / (2 * dT));
}
if (targetThetaAnnual !== undefined) {
const diff = greeks.theta - targetThetaAnnual;
loss += 0.5 * diff * diff;
dLoss_dT += 1.0 * diff * ((gUp.theta - gDown.theta) / (2 * dT));
}
if (targets.vega !== undefined) {
const diff = greeks.vega - targets.vega;
loss += 1.0 * diff * diff;
dLoss_dT += 2.0 * diff * ((gUp.vega - gDown.vega) / (2 * dT));
}
if (loss < 1e-8) break;
if (Math.abs(dLoss_dT) < 1e-12) break;
const step = 0.001 / (1 + iter * 0.01);
const newT = T - Math.sign(dLoss_dT) * step;
T = Math.max(1/365, Math.min(5, newT));
if (Math.abs(T - newT) < 1e-10) break;
}
const finalGreeks = calculateBSM({ S, K, T, sigma }, optionType);
return {
T, DTE: Math.round(T * 365), price: finalGreeks.price, greeks: finalGreeks,
residuals: {
price: targets.price !== undefined ? finalGreeks.price - targets.price : undefined,
delta: targets.delta !== undefined ? finalGreeks.delta - targets.delta : undefined,
gamma: targets.gamma !== undefined ? finalGreeks.gamma - targets.gamma : undefined,
theta: targets.theta !== undefined ? finalGreeks.thetaDaily - (thetaUnit === 'daily' ? targets.theta : targets.theta / 365) : undefined,
vega: targets.vega !== undefined ? finalGreeks.vega - targets.vega : undefined,
},
};
}
// ============================================================
// TIME DECAY UTILITIES
// ============================================================
export interface TimeDecayPoint {
DTE: number; T: number; price: number; timeValue: number;
thetaDaily: number; thetaPct: number; cumDecay: number;
delta: number; gamma: number; vega: number;
}
export function generateTimeDecayCurve(
S: number, K: number, sigma: number, optionType: 'call' | 'put', maxDTE: number = 180
): TimeDecayPoint[] {
const DTE_list: number[] = [];
for (let d = 1; d <= maxDTE; d += Math.max(1, Math.floor(d / 20))) DTE_list.push(d);
if (DTE_list[DTE_list.length - 1] !== maxDTE) DTE_list.push(maxDTE);
const tvAtMax = calculateBSM({ S, K, T: maxDTE / 365, sigma }, optionType).timeValue;
return DTE_list.map(dte => {
const T = dte / 365;
const g = calculateBSM({ S, K, T, sigma }, optionType);
const dailyPct = g.timeValue > 0.001 ? Math.abs(g.thetaDaily) / g.timeValue * 100 : 0;
const cumDecay = tvAtMax > 0 ? (tvAtMax - g.timeValue) / tvAtMax * 100 : 0;
return {
DTE: dte, T, price: parseFloat(g.price.toFixed(4)),
timeValue: parseFloat(g.timeValue.toFixed(4)),
thetaDaily: parseFloat(g.thetaDaily.toFixed(4)),
thetaPct: parseFloat(dailyPct.toFixed(2)),
cumDecay: parseFloat(cumDecay.toFixed(1)),
delta: parseFloat(g.delta.toFixed(4)),
gamma: parseFloat(g.gamma.toFixed(4)),
vega: parseFloat(g.vega.toFixed(4)),
};
});
}
export function getDecayPhase(DTE: number): { name: string; color: string; description: string } {
if (DTE >= 60) return { name: '平坦区', color: 'text-slate-400', description: '缓慢、可预测的衰减' };
if (DTE >= 30) return { name: '甜点区', color: 'text-emerald-400', description: 'Theta/Gamma比率最佳' };
if (DTE >= 14) return { name: '加速区', color: 'text-amber-400', description: '每日P&L明显改善' };
if (DTE >= 7) return { name: '高速区', color: 'text-orange-400', description: 'Gamma风险急剧放大' };
return { name: '二元区', color: 'text-rose-400', description: '极端衰减Pin Risk极高' };
}
// ============================================================
// CURVE GENERATION
// ============================================================
export function generateGreeksCurve(
K: number, T: number, sigma: number, optionType: 'call' | 'put',
points: number = 100, minS: number = 50, maxS: number = 150
): { S: number; price: number; delta: number; gamma: number; theta: number; vega: number; rho: number; intrinsic: number; timeValue: number }[] {
const data = [];
const step = (maxS - minS) / (points - 1);
for (let i = 0; i < points; i++) {
const S = minS + i * step;
const greeks = calculateBSM({ S, K, T, sigma }, optionType);
data.push({
S: parseFloat(S.toFixed(2)), price: parseFloat(greeks.price.toFixed(4)),
delta: parseFloat(greeks.delta.toFixed(4)), gamma: parseFloat(greeks.gamma.toFixed(4)),
theta: parseFloat(greeks.thetaDaily.toFixed(4)), vega: parseFloat(greeks.vega.toFixed(4)),
rho: parseFloat(greeks.rho.toFixed(4)), intrinsic: parseFloat(greeks.intrinsic.toFixed(4)),
timeValue: parseFloat(greeks.timeValue.toFixed(4)),
});
}
return data;
}
export function generateExpiryComparison(
S: number, K: number, sigma: number, optionType: 'call' | 'put',
expiries: number[] = [0.083, 0.25, 0.5, 1.0, 2.0]
): { expiry: string; price: number; delta: number; gamma: number; theta: number; vega: number }[] {
return expiries.map(T => {
const g = calculateBSM({ S, K, T, sigma }, optionType);
return {
expiry: `${(T * 12).toFixed(1)}`, price: parseFloat(g.price.toFixed(4)),
delta: parseFloat(g.delta.toFixed(4)), gamma: parseFloat(g.gamma.toFixed(4)),
theta: parseFloat(g.thetaDaily.toFixed(4)), vega: parseFloat(g.vega.toFixed(4)),
};
});
}
// ============================================================
// TAYLOR APPROXIMATION
// ============================================================
export function taylorPriceApproximation(
basePrice: number, baseGreeks: BSGreeks,
dS: number, dt: number, dsigma: number, dr: number
): {
firstOrder: number; secondOrder: number; fullApprox: number;
deltaPnL: number; gammaPnL: number; thetaPnL: number;
vegaPnL: number; rhoPnL: number; vannaPnL: number; vommaPnL: number;
} {
const g = baseGreeks;
const deltaPnL = g.delta * dS;
const gammaPnL = 0.5 * g.gamma * dS * dS;
const thetaPnL = g.theta * dt;
const vegaPnL = g.vega * dsigma * 100;
const rhoPnL = g.rho * dr * 100;
const vannaPnL = g.vanna * dS * dsigma * 100;
const vommaPnL = 0.5 * g.vomma * dsigma * dsigma * 10000;
return {
firstOrder: basePrice + deltaPnL + thetaPnL + vegaPnL + rhoPnL,
secondOrder: basePrice + deltaPnL + thetaPnL + vegaPnL + rhoPnL + gammaPnL,
fullApprox: basePrice + deltaPnL + thetaPnL + vegaPnL + rhoPnL + gammaPnL + vannaPnL + vommaPnL,
deltaPnL, gammaPnL, thetaPnL, vegaPnL, rhoPnL, vannaPnL, vommaPnL,
};
}

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)

@ -0,0 +1,20 @@
import { useState } from 'react'
import '../App.css'
export default function Home() {
const [count, setCount] = useState(0)
return (
<>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
</>
)
}

@ -0,0 +1,218 @@
import { useState, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { calculateBSM, generateExpiryComparison, type BSGreeks } from '@/lib/bsm';
import { RefreshCw, TrendingUp, TrendingDown, Clock, Zap, Percent, DollarSign, Calendar } from 'lucide-react';
export default function CalculatorSection() {
const [params, setParams] = useState({ S: 100, K: 100, T_days: 91, sigma: 0.20 });
const [tUnit, setTUnit] = useState<'days' | 'years'>('days');
const [optionType, setOptionType] = useState<'call' | 'put'>('call');
const [showDetails, setShowDetails] = useState(false);
const T = tUnit === 'days' ? params.T_days / 365 : params.T_days;
const greeks: BSGreeks | null = useMemo(() => {
try { return calculateBSM({ S: params.S, K: params.K, T, sigma: params.sigma }, optionType); }
catch { return null; }
}, [params, T, optionType]);
const comparison = useMemo(() => {
return generateExpiryComparison(params.S, params.K, params.sigma, optionType);
}, [params.S, params.K, params.sigma, optionType]);
const update = (key: string, val: string) => {
const n = parseFloat(val);
if (!isNaN(n)) setParams(p => ({ ...p, [key]: n }));
};
const preset = (type: string) => {
switch (type) {
case 'atm_call': setParams({ S: 100, K: 100, T_days: 91, sigma: 0.20 }); setOptionType('call'); break;
case 'itm_call': setParams({ S: 110, K: 100, T_days: 91, sigma: 0.20 }); setOptionType('call'); break;
case 'otm_call': setParams({ S: 90, K: 100, T_days: 91, sigma: 0.20 }); setOptionType('call'); break;
case 'atm_put': setParams({ S: 100, K: 100, T_days: 91, sigma: 0.20 }); setOptionType('put'); break;
case 'itm_put': setParams({ S: 90, K: 100, T_days: 91, sigma: 0.20 }); setOptionType('put'); break;
}
};
if (!greeks) return null;
const isITM = optionType === 'call' ? params.S > params.K : params.S < params.K;
const isOTM = optionType === 'call' ? params.S < params.K : params.S > params.K;
return (
<div className="space-y-8">
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg text-[#1E2026]"></CardTitle>
<div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('atm_call')}>ATM Call</Button>
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('itm_call')}>ITM Call</Button>
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('otm_call')}>OTM Call</Button>
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('atm_put')}>ATM Put</Button>
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('itm_put')}>ITM Put</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"> S</Label>
<Input type="number" value={params.S} onChange={e => update('S', e.target.value)} className="border-[#E6E8EA] text-[#1E2026]" />
</div>
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"> K</Label>
<Input type="number" value={params.K} onChange={e => update('K', e.target.value)} className="border-[#E6E8EA] text-[#1E2026]" />
</div>
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex">
<Input type="number" value={params.T_days} onChange={e => update('T_days', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] rounded-r-none" />
<div className="flex">
<Button size="sm" className={`rounded-none ${tUnit === 'days' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2`} onClick={() => setTUnit('days')}>
<Calendar className="w-3 h-3 mr-1" />
</Button>
<Button size="sm" className={`rounded-l-none ${tUnit === 'years' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2`} onClick={() => setTUnit('years')}>
</Button>
</div>
</div>
<p className="text-[10px] text-[#848E9C]">{tUnit === 'days' ? `${params.T_days}天 ≈ ${(params.T_days/365).toFixed(3)}` : `${params.T_days}年 ≈ ${Math.round(params.T_days*365)}`}</p>
</div>
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"> σ</Label>
<Input type="number" step="0.01" value={params.sigma} onChange={e => update('sigma', e.target.value)} className="border-[#E6E8EA] text-[#1E2026]" />
</div>
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex gap-2">
<Button size="sm" className={`flex-1 ${optionType === 'call' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('call')}>
<TrendingUp className="w-3 h-3 mr-1" /> Call
</Button>
<Button size="sm" className={`flex-1 ${optionType === 'put' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('put')}>
<TrendingDown className="w-3 h-3 mr-1" /> Put
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200 md:col-span-1">
<CardContent className="p-6">
<div className="text-center space-y-4">
<Badge variant="outline" className={`text-sm px-3 py-1 ${isITM ? 'border-[#0ECB81] text-[#0ECB81]' : isOTM ? 'border-[#F0B90B] text-[#F0B90B]' : 'border-[#848E9C] text-[#848E9C]'}`}>
{isITM ? '价内 (ITM)' : isOTM ? '价外 (OTM)' : '平价 (ATM)'}
</Badge>
<div>
<p className="text-sm text-[#848E9C] mb-1"> ()</p>
<p className="text-4xl font-bold text-[#1E2026]">{greeks.price.toFixed(4)}</p>
</div>
<Separator className="bg-[#E6E8EA]" />
<div className="grid grid-cols-2 gap-4 text-sm">
<div><p className="text-[#848E9C]"></p><p className="font-medium text-[#1E2026]">{greeks.intrinsic.toFixed(4)}</p></div>
<div><p className="text-[#848E9C]"></p><p className="font-medium text-[#1E2026]">{greeks.timeValue.toFixed(4)}</p></div>
</div>
<div className="text-xs text-[#848E9C]">
d = {greeks.d1.toFixed(4)} · d = {greeks.d2.toFixed(4)}<br/>
ITM = {(greeks.itmProb * 100).toFixed(2)}%
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200 md:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-lg text-[#1E2026] flex items-center gap-2">
<Zap className="w-5 h-5 text-[#F0B90B]" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<GreekCard label="Delta" value={greeks.delta} color="text-[#0ECB81]" icon={<TrendingUp className="w-4 h-4" />} desc="方向敏感度" />
<GreekCard label="Gamma" value={greeks.gamma} color="text-[#1EAEDB]" icon={<Zap className="w-4 h-4" />} desc="凸性风险" />
<GreekCard label="Theta" value={greeks.thetaDaily} color="text-[#F6465D]" icon={<Clock className="w-4 h-4" />} desc="每日时间衰减" />
<GreekCard label="Vega" value={greeks.vega} color="text-[#F0B90B]" icon={<Percent className="w-4 h-4" />} desc="每1%波动率" />
<GreekCard label="Rho" value={greeks.rho} color="text-[#1EAEDB]" icon={<DollarSign className="w-4 h-4" />} desc="每1%利率" />
</div>
<Separator className="my-4 bg-[#E6E8EA]" />
<div className="flex justify-between items-center">
<p className="text-sm text-[#848E9C]"></p>
<Button variant="outline" size="sm" className="border-[#E6E8EA] text-[#848E9C]" onClick={() => setShowDetails(!showDetails)}>
<RefreshCw className="w-3 h-3 mr-1" />{showDetails ? '收起' : '展开'}
</Button>
</div>
{showDetails && (
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-3">
<Detail label="Vanna" val={greeks.vanna.toFixed(6)} desc="Delta-Vol交叉" />
<Detail label="Vomma" val={greeks.vomma.toFixed(4)} desc="Vega凸性" />
<Detail label="Charm" val={greeks.charm.toFixed(6)} desc="Delta时间衰减" />
<Detail label="Speed" val={greeks.speed.toFixed(6)} desc="Gamma变化率" />
<Detail label="Zomma" val={greeks.zomma.toFixed(6)} desc="Gamma-Vol交叉" />
<Detail label="Color" val={greeks.color.toFixed(6)} desc="Gamma时间衰减" />
<Detail label="年化Theta" val={greeks.theta.toFixed(4)} desc="时间衰减(年化)" />
<Detail label="Price×Delta" val={(greeks.price * greeks.delta).toFixed(4)} desc="Delta加权价格" />
</div>
)}
</CardContent>
</Card>
</div>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader>
<CardTitle className="text-lg text-[#1E2026]"></CardTitle>
<p className="text-sm text-[#848E9C]"> S={params.S}, K={params.K}, σ={params.sigma} </p>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="border-b border-[#E6E8EA]">
<th className="text-left py-2 px-3 text-[#848E9C]"></th>
<th className="text-right py-2 px-3 text-[#848E9C]"></th>
<th className="text-right py-2 px-3 text-[#848E9C]">Delta</th>
<th className="text-right py-2 px-3 text-[#848E9C]">Gamma</th>
<th className="text-right py-2 px-3 text-[#848E9C]">Theta()</th>
<th className="text-right py-2 px-3 text-[#848E9C]">Vega</th>
</tr></thead>
<tbody>{comparison.map((row, i) => (
<tr key={i} className="border-b border-[#E6E8EA] hover:bg-[#F5F5F5]">
<td className="py-2 px-3 text-[#1E2026] font-medium">{row.expiry}</td>
<td className="py-2 px-3 text-right text-[#0ECB81]">{row.price.toFixed(4)}</td>
<td className="py-2 px-3 text-right text-[#1E2026]">{row.delta.toFixed(4)}</td>
<td className="py-2 px-3 text-right text-[#1EAEDB]">{row.gamma.toFixed(4)}</td>
<td className="py-2 px-3 text-right text-[#F6465D]">{row.theta.toFixed(4)}</td>
<td className="py-2 px-3 text-right text-[#F0B90B]">{row.vega.toFixed(4)}</td>
</tr>
))}</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
function GreekCard({ label, value, color, icon, desc }: { label: string; value: number; color: string; icon: React.ReactNode; desc: string }) {
return (
<div className="bg-[#F5F5F5] rounded-lg p-3 border border-[#E6E8EA]">
<div className="flex items-center gap-2 mb-1"><span className={color}>{icon}</span><span className="text-xs text-[#848E9C]">{label}</span></div>
<p className={`text-xl font-bold ${color}`}>{value.toFixed(4)}</p>
<p className="text-[10px] text-[#848E9C] mt-1">{desc}</p>
</div>
);
}
function Detail({ label, val, desc }: { label: string; val: string; desc: string }) {
return (
<div className="bg-[#F5F5F5] rounded p-2 border border-[#E6E8EA]">
<p className="text-[10px] text-[#848E9C]">{label} <span className="text-[#848E9C]">· {desc}</span></p>
<p className="text-sm font-mono text-[#1E2026]">{val}</p>
</div>
);
}

@ -0,0 +1,158 @@
import { useState, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { calculateBSM, taylorPriceApproximation } from '@/lib/bsm';
import { TrendingUp, ArrowRight, BarChart3, Calendar } from 'lucide-react';
export default function GreeksAnalysis() {
const [baseParams, setBaseParams] = useState({ S: 100, K: 100, T_days: 91, sigma: 0.20 });
const [tUnit, setTUnit] = useState<'days' | 'years'>('days');
const [optionType, setOptionType] = useState<'call' | 'put'>('call');
const [shock, setShock] = useState({ dS: 5, dt_days: 1, dsigma: 0, dr: 0 });
const T = tUnit === 'days' ? baseParams.T_days / 365 : baseParams.T_days;
const dt = shock.dt_days / 365;
const updateBase = (key: string, value: string) => { const n = parseFloat(value); if (!isNaN(n)) setBaseParams(p => ({ ...p, [key]: n })); };
const updateShock = (key: string, value: string) => { const n = parseFloat(value); if (!isNaN(n)) setShock(p => ({ ...p, [key]: n })); };
const base = useMemo(() => calculateBSM({ S: baseParams.S, K: baseParams.K, T, sigma: baseParams.sigma }, optionType), [baseParams, T, optionType]);
const exactNew = useMemo(() => calculateBSM({
S: baseParams.S + shock.dS, K: baseParams.K,
T: Math.max(T + dt, 0.0001), sigma: Math.max(baseParams.sigma + shock.dsigma, 0.001),
}, optionType), [baseParams, T, dt, shock, optionType]);
const approx = useMemo(() => taylorPriceApproximation(base.price, base, shock.dS, dt, shock.dsigma, shock.dr), [base, shock, dt]);
return (
<div className="space-y-8">
<div className="bg-[#F5F5F5] border border-[#E6E8EA] rounded-lg p-6">
<h3 className="text-lg font-semibold text-[#1E2026] mb-3"> · Taylor </h3>
<p className="text-sm text-[#848E9C]">
Taylor
DeltaDelta+Gamma
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader className="pb-3">
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2"><TrendingUp className="w-4 h-4 text-[#F0B90B]" /></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> S</Label><Input type="number" value={baseParams.S} onChange={e => updateBase('S', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> K</Label><Input type="number" value={baseParams.K} onChange={e => updateBase('K', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex">
<Input type="number" value={baseParams.T_days} onChange={e => updateBase('T_days', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9 rounded-r-none" />
<Button size="sm" className={`rounded-none ${tUnit === 'days' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2 h-9`} onClick={() => setTUnit('days')}><Calendar className="w-3 h-3 mr-1" /></Button>
<Button size="sm" className={`rounded-l-none ${tUnit === 'years' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2 h-9`} onClick={() => setTUnit('years')}></Button>
</div>
</div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> σ</Label><Input type="number" step="0.01" value={baseParams.sigma} onChange={e => updateBase('sigma', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex gap-2">
<Button size="sm" className={`flex-1 ${optionType === 'call' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('call')}>Call</Button>
<Button size="sm" className={`flex-1 ${optionType === 'put' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('put')}>Put</Button>
</div>
</div>
</div>
<Separator className="bg-[#E6E8EA]" />
<div className="grid grid-cols-3 gap-3">
<Mini label="价格" val={base.price} color="text-[#1E2026]" />
<Mini label="Delta" val={base.delta} color="text-[#0ECB81]" />
<Mini label="Gamma" val={base.gamma} color="text-[#1EAEDB]" />
<Mini label="Theta(日)" val={base.thetaDaily} color="text-[#F6465D]" />
<Mini label="Vega" val={base.vega} color="text-[#F0B90B]" />
<Mini label="Rho" val={base.rho} color="text-[#1EAEDB]" />
</div>
</CardContent>
</Card>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader className="pb-3">
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2"><ArrowRight className="w-4 h-4 text-[#F0B90B]" /></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> ΔS</Label><Input type="number" value={shock.dS} onChange={e => updateShock('dS', e.target.value)} step="0.1" className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"></Label><Input type="number" value={shock.dt_days} onChange={e => updateShock('dt_days', e.target.value)} step="1" className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> Δσ</Label><Input type="number" value={shock.dsigma} onChange={e => updateShock('dsigma', e.target.value)} step="0.001" className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> Δr</Label><Input type="number" value={shock.dr} onChange={e => updateShock('dr', e.target.value)} step="0.0001" className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
</div>
<div className="text-xs text-[#848E9C] bg-[#F5F5F5] p-3 rounded space-y-1">
<p><strong className="text-[#1E2026]">Taylor :</strong></p>
<p className="font-mono">dV Δ·dS + ½Γ·(dS)² + Θ·dt + ν·dσ + ρ·dr + Vanna·dS·dσ + ½Vomma·(dσ)²</p>
</div>
</CardContent>
</Card>
</div>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader>
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2"><BarChart3 className="w-4 h-4 text-[#F0B90B]" /></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<ApxCard title="一阶近似 (Delta Only)" val={approx.firstOrder} exact={exactNew.price} formula="V₀ + Δ·dS + Θ·dt + ν·dσ + ρ·dr" color="border-[#0ECB81]/50" />
<ApxCard title="二阶近似 (Delta+Gamma)" val={approx.secondOrder} exact={exactNew.price} formula="一阶 + ½Γ·(dS)²" color="border-[#1EAEDB]/50" />
<ApxCard title="完整近似 (含交叉项)" val={approx.fullApprox} exact={exactNew.price} formula="二阶 + Vanna·dS·dσ + ½Vomma·(dσ)²" color="border-[#F0B90B]/50" />
</div>
<div className="bg-[#F5F5F5] rounded-lg p-4 border border-[#E6E8EA]">
<h4 className="text-sm font-medium text-[#1E2026] mb-3">P&L </h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<PnL label="Delta 贡献" val={approx.deltaPnL} desc={`${shock.dS > 0 ? '上涨' : '下跌'} ${Math.abs(shock.dS)}`} />
<PnL label="Gamma 贡献" val={approx.gammaPnL} desc="凸性收益" />
<PnL label="Theta 贡献" val={approx.thetaPnL} desc={`流逝 ${shock.dt_days}`} />
<PnL label="Vega 贡献" val={approx.vegaPnL} desc={`波动率 ${shock.dsigma > 0 ? '+' : ''}${(shock.dsigma * 100).toFixed(2)}%`} />
<PnL label="Rho 贡献" val={approx.rhoPnL} desc={`利率 ${shock.dr > 0 ? '+' : ''}${(shock.dr * 100).toFixed(2)}%`} />
<PnL label="Vanna 贡献" val={approx.vannaPnL} desc="价格-波动率交叉" />
<PnL label="Vomma 贡献" val={approx.vommaPnL} desc="波动率凸性" />
<PnL label="精确 P&L" val={exactNew.price - base.price} desc="BS 重新定价" hl />
</div>
</div>
</CardContent>
</Card>
</div>
);
}
function Mini({ label, val, color }: { label: string; val: number; color: string }) {
return (
<div className="bg-[#F5F5F5] rounded p-2 border border-[#E6E8EA]">
<p className="text-[10px] text-[#848E9C]">{label}</p>
<p className={`text-sm font-bold ${color}`}>{val.toFixed(4)}</p>
</div>
);
}
function ApxCard({ title, val, exact, formula, color }: { title: string; val: number; exact: number; formula: string; color: string }) {
const err = Math.abs(val - exact);
const errPct = exact !== 0 ? (err / Math.abs(exact)) * 100 : 0;
return (
<div className={`bg-[#F5F5F5] rounded-lg p-4 border ${color}`}>
<p className="text-xs text-[#848E9C] mb-2">{title}</p>
<p className="text-2xl font-bold text-[#1E2026]">{val.toFixed(4)}</p>
<div className="mt-2 text-xs space-y-1">
<p className="text-[#848E9C] font-mono text-[10px]">{formula}</p>
<p className="text-[#848E9C]">: {exact.toFixed(4)}</p>
<p className={`${errPct < 1 ? 'text-[#0ECB81]' : errPct < 5 ? 'text-[#F0B90B]' : 'text-[#F6465D]'}`}>: {err.toFixed(4)} ({errPct.toFixed(2)}%)</p>
</div>
</div>
);
}
function PnL({ label, val, desc, hl = false }: { label: string; val: number; desc: string; hl?: boolean }) {
return (
<div className={`rounded p-3 border ${hl ? 'bg-[#0ECB81]/10 border-[#0ECB81]/50' : 'bg-[#F5F5F5] border-[#E6E8EA]'}`}>
<p className="text-[10px] text-[#848E9C]">{label} <span className="text-[#848E9C]">· {desc}</span></p>
<p className={`text-lg font-bold ${val > 0 ? 'text-[#0ECB81]' : val < 0 ? 'text-[#F6465D]' : 'text-[#848E9C]'}`}>{val > 0 ? '+' : ''}{val.toFixed(4)}</p>
</div>
);
}

@ -0,0 +1,623 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { BookOpen, TrendingUp, Clock, Zap, Percent, DollarSign, CheckCircle, Calculator, ArrowLeftRight, TrendingDown, Lightbulb, Target, BarChart3, BookMarked, FastForward } from 'lucide-react';
export default function GuideSection() {
return (
<div className="space-y-8">
<Tabs defaultValue="theory" className="w-full">
<TabsList className="bg-white border border-[#E6E8EA] grid grid-cols-4 w-full rounded-[6px]">
<TabsTrigger value="theory" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]"></TabsTrigger>
<TabsTrigger value="calculator" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]"></TabsTrigger>
<TabsTrigger value="reverse" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]"></TabsTrigger>
<TabsTrigger value="examples" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026]"></TabsTrigger>
</TabsList>
<TabsContent value="theory" className="space-y-8 mt-8">
<div className="bg-[#F5F5F5] border border-[#E6E8EA] rounded-lg p-6">
<h3 className="text-lg font-semibold text-[#1E2026] mb-3"></h3>
<p className="text-sm text-[#848E9C]">
Black-Scholes r=0, q=0 DeltaGammaThetaVegaRho
</p>
</div>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader>
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2">
<BookOpen className="w-5 h-5 text-[#F0B90B]" />
Black-Scholes-Merton r=0, q=0
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-[#848E9C] space-y-4">
<p>
BSM 1973 Fischer Black Myron Scholes Robert Merton 1997
r=0 q=0
</p>
<div className="bg-[#F5F5F5] p-4 rounded-lg font-mono text-xs text-[#1E2026] space-y-2">
<p>: C = S·N(d) - K·N(d)</p>
<p>: P = K·N(-d) - S·N(-d)</p>
<p className="text-[#848E9C] mt-2"> d = [ln(S/K) + 0.5·σ²·T] / (σT), d = d - σT</p>
</div>
<p>
S K T σ σ (IV)
</p>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<GreekGuideCard
title="Delta (Δ)"
symbol="∂V/∂S"
color="text-emerald-400"
borderColor="border-emerald-700/50"
icon={<TrendingUp className="w-5 h-5" />}
description="方向敏感度:标的价格每变动 1 单位,期权价格变动多少"
callFormula="Δ_call = N(d₁)"
putFormula="Δ_put = N(d₁) - 1 = -N(-d₁)"
range="Call: [0, 1], Put: [-1, 0]"
keyInsight="双重身份:对冲比率 + 风险中性 ITM 概率近似。Delta 0.30 ≈ 30% 到期实值概率。"
behavior="OTM→0, ATM→0.5, ITM→1 (Call)。高波动率/长期限使曲线更平缓。"
/>
<GreekGuideCard
title="Gamma (Γ)"
symbol="∂²V/∂S²"
color="text-blue-400"
borderColor="border-blue-700/50"
icon={<Zap className="w-5 h-5" />}
description="凸性风险Delta 对价格变动的敏感度,衡量对冲误差"
callFormula="Γ = N'(d₁) / (S·σ·√T)"
putFormula="Γ_put = Γ_call (完全相同)"
range="恒为正"
keyInsight="Gamma 是概率密度函数 (PDF) 的形态。ATM 处最大,深度 ITM/OTM 趋零。"
behavior="临近到期时 ATM Gamma 按 1/√T 发散增长Gamma 爆炸。OTM/ITM 的 Gamma 反而熄灭。"
/>
<GreekGuideCard
title="Theta (Θ)"
symbol="∂V/∂t"
color="text-rose-400"
borderColor="border-rose-700/50"
icon={<Clock className="w-5 h-5" />}
description="时间衰减:每过一天,期权价值因时间流逝损失多少"
callFormula="Θ = -S·N'(d₁)·σ / (2√T)"
putFormula="Θ_put = Θ_call (相同)"
range="恒为负(本简化模型)"
keyInsight="时间价值 ∝ σ√T。最后 30 天贡献约 2/3 的总时间衰减。每日 Theta = 年化 Theta / 365。"
behavior="30-45 DTE 是卖方「甜点区」Theta/Gamma 比率最佳。0-7 DTE 进入二元区Pin Risk 极高。"
/>
<GreekGuideCard
title="Vega (ν)"
symbol="∂V/∂σ"
color="text-purple-400"
borderColor="border-purple-700/50"
icon={<Percent className="w-5 h-5" />}
description="波动率敏感度:隐含波动率每变化 1 个百分点,期权价格变动多少"
callFormula="ν = S·√T·N'(d₁)·0.01"
putFormula="ν_put = ν_call (完全相同)"
range="恒为正"
keyInsight="ATM 处最大,与 √T 成正比。2 年期 LEAPS 的 Vega 是 3 个月期的 √8 ≈ 2.83 倍。"
behavior="近月高 Gamma 低 Vega → 博弈方向选近月;远月低 Gamma 高 Vega → 博弈波动率选远月。"
/>
</div>
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<CardTitle className="text-white text-base flex items-center gap-2">
<DollarSign className="w-5 h-5 text-cyan-400" />
Rho (ρ)
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-xs text-slate-500 mb-1">Call Rho</p>
<p className="font-mono text-cyan-400">ρ_call = K·T·N(d)·0.01</p>
<p className="text-xs text-slate-500 mt-1"> r=0Rho T </p>
</div>
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-xs text-slate-500 mb-1">Put Rho</p>
<p className="font-mono text-cyan-400">ρ_put = -K·T·N(-d)·0.01</p>
<p className="text-xs text-slate-500 mt-1">Put Rho </p>
</div>
</div>
<p>
Rho T {'<'}90 Rho
r=0 Rho 使
</p>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base"></CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<div className="space-y-3">
<RelationshipItem
title="Gamma-Theta 权衡"
formula="Θ = -½ σ² S² Γ"
description="正 Gamma凸性收益的代价是负 Theta时间衰减。这是期权策略设计的最根本约束。"
/>
<RelationshipItem
title="Put-Call Parity"
formula="C - P = S - K"
description="简化版r=0欧式期权的无套利定价基础。Delta_call - Delta_put = 1Gamma、Vega 对 Call/Put 完全相同。"
/>
<RelationshipItem
title="Taylor P&L 展开"
formula="dV ≈ Δ·dS + ½Γ·(dS)² + Θ·dt + ν·dσ + Vanna·dS·dσ + ½Vomma·(dσ)²"
description="小幅变动一阶主导;中幅变动 Gamma 修正;大幅+波动率剧变时 Vanna 交叉项可能主导。"
/>
<RelationshipItem
title="ATM 风暴眼"
formula="Gamma↑ Theta↑ Vega↑ 同时极值"
description="ATM 区域是希腊字母最活跃的「风暴眼」,拥有最大不确定性,时间溢价最高。深度 ITM/OTM 的希腊字母行为趋于钝化。"
/>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-800">
<th className="text-left py-2 px-3 text-slate-400 font-medium"></th>
<th className="text-center py-2 px-3 text-slate-400 font-medium">ITM ()</th>
<th className="text-center py-2 px-3 text-slate-400 font-medium">ATM ()</th>
<th className="text-center py-2 px-3 text-slate-400 font-medium">OTM ()</th>
</tr>
</thead>
<tbody className="text-slate-300">
<tr className="border-b border-slate-800/50"><td className="py-2 px-3 text-slate-400">Call </td><td className="py-2 px-3 text-center">S &gt; K</td><td className="py-2 px-3 text-center">S K</td><td className="py-2 px-3 text-center">S &lt; K</td></tr>
<tr className="border-b border-slate-800/50"><td className="py-2 px-3 text-slate-400"></td><td className="py-2 px-3 text-center text-emerald-400"></td><td className="py-2 px-3 text-center text-amber-400">/</td><td className="py-2 px-3 text-center text-rose-400"></td></tr>
<tr className="border-b border-slate-800/50"><td className="py-2 px-3 text-slate-400">Delta (Call)</td><td className="py-2 px-3 text-center">0.60 - 1.00</td><td className="py-2 px-3 text-center font-bold text-amber-400"> 0.50</td><td className="py-2 px-3 text-center">0.00 - 0.40</td></tr>
<tr className="border-b border-slate-800/50"><td className="py-2 px-3 text-slate-400">Gamma</td><td className="py-2 px-3 text-center text-slate-500"> 0</td><td className="py-2 px-3 text-center font-bold text-blue-400"></td><td className="py-2 px-3 text-center text-slate-500"> 0</td></tr>
<tr className="border-b border-slate-800/50"><td className="py-2 px-3 text-slate-400">Theta ()</td><td className="py-2 px-3 text-center text-slate-500"></td><td className="py-2 px-3 text-center font-bold text-rose-400"></td><td className="py-2 px-3 text-center text-slate-500"></td></tr>
<tr className="border-b border-slate-800/50"><td className="py-2 px-3 text-slate-400">Vega</td><td className="py-2 px-3 text-center text-slate-500"></td><td className="py-2 px-3 text-center font-bold text-purple-400"></td><td className="py-2 px-3 text-center text-slate-500"></td></tr>
<tr><td className="py-2 px-3 text-slate-400"></td><td className="py-2 px-3 text-center"></td><td className="py-2 px-3 text-center">/</td><td className="py-2 px-3 text-center"></td></tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="calculator" className="space-y-6 mt-6">
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2">
<Calculator className="w-5 h-5 text-emerald-400" />
使
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<p></p>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium"></h4>
<div className="grid grid-cols-2 gap-3 text-xs">
<div><span className="text-emerald-400">S ()</span></div>
<div><span className="text-emerald-400">K ()</span></div>
<div><span className="text-emerald-400">T ()</span>/</div>
<div><span className="text-emerald-400">σ ()</span></div>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium"></h4>
<div className="grid grid-cols-2 gap-3 text-xs">
<div><span className="text-blue-400">ATM Call</span>S=K=100</div>
<div><span className="text-blue-400">ITM Call</span>S=110 &gt; K=100</div>
<div><span className="text-blue-400">OTM Call</span>S=90 &lt; K=100</div>
<div><span className="text-blue-400">ATM Put</span></div>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium">T </h4>
<p className="text-xs">"天/年"</p>
<ul className="text-xs space-y-1 list-disc list-inside">
<li>"天" 91 T = 91/365 = 0.2493 </li>
<li>"年" 0.25 T = 0.25 = 91.25 </li>
</ul>
</div>
<div className="bg-amber-900/20 p-4 rounded-lg border border-amber-700/50">
<h4 className="text-amber-400 font-medium text-xs mb-2 flex items-center gap-2">
<Lightbulb className="w-4 h-4" />
</h4>
<p className="text-xs">"所见即所得"</p>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-blue-400" />
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<p> Taylor </p>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium"></h4>
<div className="space-y-2 text-xs">
<div className="flex items-start gap-2">
<span className="text-emerald-400 font-mono"></span>
<span> Delta线</span>
</div>
<div className="flex items-start gap-2">
<span className="text-blue-400 font-mono"></span>
<span> Gamma ±3% </span>
</div>
<div className="flex items-start gap-2">
<span className="text-purple-400 font-mono"></span>
<span> Vanna + Vomma </span>
</div>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium">P&L </h4>
<p className="text-xs"> Greek </p>
<ul className="text-xs space-y-1 list-disc list-inside">
<li><span className="text-emerald-400">Delta </span></li>
<li><span className="text-blue-400">Gamma </span></li>
<li><span className="text-rose-400">Theta </span></li>
<li><span className="text-purple-400">Vega </span></li>
<li><span className="text-cyan-400">Vanna/Vomma</span></li>
</ul>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2">
<TrendingDown className="w-5 h-5 text-rose-400" />
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<p> Theta 线</p>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium"></h4>
<p className="text-xs"> σ·S·T·N'(d) T 线</p>
<div className="bg-amber-900/20 p-3 rounded text-xs">
<p className="text-amber-400">💡 </p>
<p> DTE 120 30 75%50%75%</p>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium">Gamma-Theta r=0 </h4>
<p className="text-xs font-mono">Θ = -½σ²S²Γ</p>
<p className="text-xs"> Gamma Theta Gamma 1/T Theta </p>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg space-y-3">
<h4 className="text-slate-200 font-medium"></h4>
<div className="grid grid-cols-5 gap-2 text-xs">
<div className="text-center p-2 bg-slate-700/50 rounded">
<p className="text-emerald-400">90+ DTE</p>
<p className="text-slate-500"></p>
</div>
<div className="text-center p-2 bg-slate-700/50 rounded">
<p className="text-emerald-400">45-90 DTE</p>
<p className="text-slate-500"></p>
</div>
<div className="text-center p-2 bg-amber-900/30 rounded border border-amber-600/50">
<p className="text-amber-400">30-45 DTE</p>
<p className="text-amber-400"></p>
</div>
<div className="text-center p-2 bg-slate-700/50 rounded">
<p className="text-rose-400">7-30 DTE</p>
<p className="text-slate-500"></p>
</div>
<div className="text-center p-2 bg-rose-900/30 rounded border border-rose-600/50">
<p className="text-rose-400">0-7 DTE</p>
<p className="text-rose-400"></p>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reverse" className="space-y-6 mt-6">
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2">
<ArrowLeftRight className="w-5 h-5 text-purple-400" />
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<p></p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-slate-800/50 p-4 rounded-lg border border-emerald-700/50">
<h4 className="text-emerald-400 font-medium text-sm mb-2">S + </h4>
<p className="text-xs mb-2"> S Greek </p>
<div className="text-xs text-slate-500">
<p> ThetaT 使 σ</p>
<p> Theta σ T</p>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg border border-blue-700/50">
<h4 className="text-blue-400 font-medium text-sm mb-2"> + S</h4>
<p className="text-xs mb-2"></p>
<div className="text-xs text-slate-500">
<p> SσT </p>
<p></p>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg border border-purple-700/50">
<h4 className="text-purple-400 font-medium text-sm mb-2"> + S σ</h4>
<p className="text-xs mb-2"> IV</p>
<div className="text-xs text-slate-500">
<p>使 Newton-Raphson </p>
<p> 3-5 </p>
</div>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg border border-rose-700/50">
<h4 className="text-rose-400 font-medium text-sm mb-2">S + σ + T</h4>
<p className="text-xs mb-2"> S σ</p>
<div className="text-xs text-slate-500">
<p></p>
<p>Delta = N(d) T</p>
</div>
</div>
</div>
<div className="bg-amber-900/20 p-4 rounded-lg border border-amber-700/50">
<h4 className="text-amber-400 font-medium text-xs mb-2 flex items-center gap-2">
<Target className="w-4 h-4" />
</h4>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center">
<p className="text-emerald-400"> &lt; 1% (绿)</p>
<p className="text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-amber-400"> 1%-5% ()</p>
<p className="text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-rose-400"> &gt; 5% ()</p>
<p className="text-slate-500"></p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2">
<BookMarked className="w-5 h-5 text-cyan-400" />
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<p> S 线</p>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-emerald-400 font-medium"></p>
<p className="text-slate-500"> = + </p>
</div>
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-emerald-400 font-medium">Delta 线</p>
<p className="text-slate-500">S 线ATM0.5ITM1OTM0</p>
</div>
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-blue-400 font-medium">Gamma 线</p>
<p className="text-slate-500">ATM </p>
</div>
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-rose-400 font-medium">Theta 线</p>
<p className="text-slate-500">ATM ITM/OTM </p>
</div>
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-purple-400 font-medium">Vega 线</p>
<p className="text-slate-500">ATM T </p>
</div>
<div className="bg-slate-800/50 p-3 rounded">
<p className="text-cyan-400 font-medium"></p>
<p className="text-slate-500">Delta Gamma -</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="examples" className="space-y-6 mt-6">
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2">
<FastForward className="w-5 h-5 text-emerald-400" />
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400 space-y-6">
<div className="bg-slate-800/50 p-4 rounded-lg border border-emerald-700/50">
<h4 className="text-emerald-400 font-medium text-sm mb-3"> ATM Call </h4>
<ol className="text-xs space-y-2 list-decimal list-inside">
<li>"定价计算器"</li>
<li>"ATM Call"</li>
<li>S=100, K=100, T=91, σ=20%</li>
<li>$4.62, Delta0.598</li>
<li>"希腊字母图谱""Delta 曲线" S=100 Delta </li>
</ol>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg border border-blue-700/50">
<h4 className="text-blue-400 font-medium text-sm mb-3"> Theta </h4>
<ol className="text-xs space-y-2 list-decimal list-inside">
<li>"反向推算""S + 希腊字母 → 权利金"</li>
<li> S=100 Delta 0.598</li>
<li> Theta "天" -0.016</li>
<li>"T 由 Theta 反推"</li>
<li>σ20%, T91 &lt; 1%</li>
</ol>
</div>
<div className="bg-slate-800/50 p-4 rounded-lg border border-purple-700/50">
<h4 className="text-purple-400 font-medium text-sm mb-3"> </h4>
<ol className="text-xs space-y-2 list-decimal list-inside">
<li>"反向推算""权利金 + 希腊字母 → S"</li>
<li>=4.62 Delta=0.598 Gamma=0.038</li>
<li> SσT </li>
<li></li>
<li> &gt; 5%</li>
</ol>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-800">
<th className="text-left py-2 px-3 text-slate-400 font-medium"></th>
<th className="text-left py-2 px-3 text-slate-400 font-medium"></th>
<th className="text-left py-2 px-3 text-slate-400 font-medium"></th>
<th className="text-left py-2 px-3 text-slate-400 font-medium"></th>
</tr>
</thead>
<tbody className="text-slate-300">
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-emerald-400 font-mono">S</td>
<td className="py-2 px-3">/</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3">100</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-emerald-400 font-mono">K</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3">100</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-emerald-400 font-mono">T</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3">(DTE)</td>
<td className="py-2 px-3">91 = 0.25</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-emerald-400 font-mono">σ</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3">()</td>
<td className="py-2 px-3">20%</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-blue-400 font-mono">Δ</td>
<td className="py-2 px-3">Delta</td>
<td className="py-2 px-3">1</td>
<td className="py-2 px-3">Call: [0,1], Put: [-1,0]</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-blue-400 font-mono">Γ</td>
<td className="py-2 px-3">Gamma</td>
<td className="py-2 px-3">Delta </td>
<td className="py-2 px-3">ATM</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-rose-400 font-mono">Θ</td>
<td className="py-2 px-3">Theta</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3"></td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-purple-400 font-mono">ν</td>
<td className="py-2 px-3">Vega</td>
<td className="py-2 px-3">1%</td>
<td className="py-2 px-3">ATM</td>
</tr>
<tr>
<td className="py-2 px-3 text-cyan-400 font-mono">ρ</td>
<td className="py-2 px-3">Rho</td>
<td className="py-2 px-3">1%</td>
<td className="py-2 px-3"></td>
</tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="text-center text-xs text-slate-600 py-4">
<p>Black-Scholes-Merton (1973) </p>
<p> r=0, q=0使</p>
</div>
</div>
);
}
function GreekGuideCard({ title, symbol, color, borderColor, icon, description, callFormula, putFormula, range, keyInsight, behavior }: {
title: string; symbol: string; color: string; borderColor: string; icon: React.ReactNode;
description: string; callFormula: string; putFormula: string; range: string; keyInsight: string; behavior: string;
}) {
return (
<Card className={`bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200`}>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<span className="text-[#F0B90B]">{icon}</span>
<div>
<CardTitle className="text-[#1E2026] text-base font-semibold">{title}</CardTitle>
<p className="text-xs text-[#848E9C] font-mono">{symbol}</p>
</div>
</div>
</CardHeader>
<CardContent className="text-sm text-[#848E9C] space-y-3">
<p className="text-[#1E2026]">{description}</p>
<div className="bg-[#F5F5F5] p-3 rounded space-y-2 text-xs font-mono">
<p className="text-[#0ECB81]">Call: {callFormula}</p>
<p className="text-[#F6465D]">Put: {putFormula}</p>
</div>
<Badge variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]">: {range}</Badge>
<div className="space-y-1">
<p className="text-xs"><strong className="text-[#1E2026]"></strong>{keyInsight}</p>
<p className="text-xs"><strong className="text-[#1E2026]"></strong>{behavior}</p>
</div>
</CardContent>
</Card>
);
}
function RelationshipItem({ title, formula, description }: { title: string; formula: string; description: string }) {
return (
<div className="bg-[#F5F5F5] rounded-lg p-4 border border-[#E6E8EA]">
<div className="flex items-start gap-3">
<CheckCircle className="w-4 h-4 text-[#0ECB81] mt-0.5 shrink-0" />
<div>
<h4 className="text-[#1E2026] font-medium text-sm">{title}</h4>
<p className="font-mono text-xs text-[#F0B90B] my-1">{formula}</p>
<p className="text-xs text-[#848E9C]">{description}</p>
</div>
</div>
</div>
);
}

@ -0,0 +1,23 @@
import { Activity, Sigma } from 'lucide-react';
export default function Header() {
return (
<header className="border-b border-[#E6E8EA] bg-white sticky top-0 z-50">
<div className="container mx-auto px-8 py-4 max-w-[1200px] flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-[#F0B90B]/20 rounded-lg">
<Sigma className="w-6 h-6 text-[#F0B90B]" />
</div>
<div>
<h1 className="text-xl font-semibold text-[#1E2026] tracking-tight"></h1>
<p className="text-xs text-[#848E9C]">Black-Scholes-Merton · · · </p>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-[#848E9C]">
<Activity className="w-4 h-4 text-[#0ECB81]" />
<span className="hidden sm:inline"></span>
</div>
</div>
</header>
);
}

@ -0,0 +1,460 @@
import { useState, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { calculateBSM, solvePriceFromGreeksAndS, solveFromPriceAndGreeks, solveImpliedVolatility, solveWithTUnknown, getDecayPhase } from '@/lib/bsm';
import { Search, ArrowRight, ArrowLeft, Sigma, DollarSign, Activity, AlertTriangle, CheckCircle, TrendingUp, TrendingDown, Clock, Calendar } from 'lucide-react';
export default function ReverseSolver() {
const [mode, setMode] = useState<'s-greeks-to-price' | 'price-greeks-to-s' | 'price-to-sigma' | 's-sigma-greeks-to-t'>('s-greeks-to-price');
const [contract, setContract] = useState({ K: 100, T_days: 91 });
const [tUnit, setTUnit] = useState<'days' | 'years'>('days');
const [optionType, setOptionType] = useState<'call' | 'put'>('call');
// Mode 1: S + Greeks → Price
const [m1, setM1] = useState({ S: 100, delta: 0.598, gamma: 0.038, theta: -0.016, vega: 0.10, rho: 0.04 });
const [m1On, setM1On] = useState({ delta: true, gamma: false, theta: false, vega: false, rho: false });
const [m1ThetaUnit, setM1ThetaUnit] = useState<'daily' | 'annual'>('daily');
// Mode 2: Price + Greeks → S
const [m2, setM2] = useState({ price: 4.62, delta: 0.598, gamma: 0.038, theta: -0.016, vega: 0.10, rho: 0.04 });
const [m2On, setM2On] = useState({ price: true, delta: true, gamma: false, theta: false, vega: false, rho: false });
const [m2ThetaUnit, setM2ThetaUnit] = useState<'daily' | 'annual'>('daily');
// Mode 3: Price + S → Sigma
const [m3, setM3] = useState({ price: 4.62, S: 100 });
// Mode 4: S + Sigma + Greeks → T
const [m4, setM4] = useState({ S: 100, sigma: 0.20, price: 4.62, delta: 0.598, gamma: 0.038, theta: -0.016, vega: 0.10 });
const [m4On, setM4On] = useState({ price: false, delta: true, gamma: false, theta: false, vega: false });
const [m4ThetaUnit, setM4ThetaUnit] = useState<'daily' | 'annual'>('daily');
const T = tUnit === 'days' ? contract.T_days / 365 : contract.T_days;
const upd = (fn: Function, key: string, val: string) => { const n = parseFloat(val); if (!isNaN(n)) fn((p: any) => ({ ...p, [key]: n })); };
const res1 = useMemo(() => {
const t: any = {};
if (m1On.delta) t.delta = m1.delta;
if (m1On.gamma) t.gamma = m1.gamma;
if (m1On.theta) t.theta = m1.theta;
if (m1On.vega) t.vega = m1.vega;
if (m1On.rho) t.rho = m1.rho;
if (Object.keys(t).length === 0) return null;
return solvePriceFromGreeksAndS(m1.S, contract.K, T, optionType, t, m1ThetaUnit === 'daily' ? 'daily' : 'annual');
}, [m1, m1On, contract, T, optionType, m1ThetaUnit]);
const res2 = useMemo(() => {
const t: any = {};
if (m2On.delta) t.delta = m2.delta;
if (m2On.gamma) t.gamma = m2.gamma;
if (m2On.theta) t.theta = m2.theta;
if (m2On.vega) t.vega = m2.vega;
if (m2On.rho) t.rho = m2.rho;
if (Object.keys(t).length === 0 || !m2On.price) return null;
return solveFromPriceAndGreeks(m2.price, contract.K, T, optionType, t, m2ThetaUnit === 'daily' ? 'daily' : 'annual');
}, [m2, m2On, contract, T, optionType, m2ThetaUnit]);
const res3 = useMemo(() => solveImpliedVolatility(m3.price, m3.S, contract.K, T, optionType), [m3, contract, T, optionType]);
const res4 = useMemo(() => {
const t: any = {};
if (m4On.price) t.price = m4.price;
if (m4On.delta) t.delta = m4.delta;
if (m4On.gamma) t.gamma = m4.gamma;
if (m4On.theta) t.theta = m4.theta;
if (m4On.vega) t.vega = m4.vega;
if (Object.keys(t).length === 0) return null;
return solveWithTUnknown(m4.S, contract.K, m4.sigma, optionType, t, m4ThetaUnit === 'daily' ? 'daily' : 'annual');
}, [m4, m4On, contract, optionType, m4ThetaUnit]);
const truth1 = useMemo(() => {
if (!res1) return null;
return calculateBSM({ S: m1.S, K: contract.K, T: res1.T, sigma: res1.sigma }, optionType);
}, [res1, m1.S, contract, optionType]);
const preset = (type: string) => {
setContract({ K: 100, T_days: 91 });
switch (type) {
case 'atm_call':
setOptionType('call');
setM1({ S: 100, delta: 0.598, gamma: 0.038, theta: -0.016, vega: 0.10, rho: 0.04 });
setM1On({ delta: true, gamma: false, theta: false, vega: false, rho: false });
setM2({ price: 4.62, delta: 0.598, gamma: 0.038, theta: -0.016, vega: 0.10, rho: 0.04 });
setM2On({ price: true, delta: true, gamma: false, theta: false, vega: false, rho: false });
setM3({ price: 4.62, S: 100 });
setM4({ S: 100, sigma: 0.20, price: 4.62, delta: 0.598, gamma: 0.038, theta: -0.016, vega: 0.10 });
setM4On({ price: false, delta: true, gamma: false, theta: false, vega: false });
break;
case 'itm_call':
setOptionType('call');
setM1({ S: 110, delta: 0.842, gamma: 0.019, theta: -0.014, vega: 0.06, rho: 0.07 });
setM1On({ delta: true, gamma: false, theta: false, vega: false, rho: false });
setM2({ price: 11.99, delta: 0.842, gamma: 0.019, theta: -0.014, vega: 0.06, rho: 0.07 });
setM2On({ price: true, delta: true, gamma: false, theta: false, vega: false, rho: false });
setM3({ price: 11.99, S: 110 });
setM4({ S: 110, sigma: 0.20, price: 11.99, delta: 0.842, gamma: 0.019, theta: -0.014, vega: 0.06 });
setM4On({ price: false, delta: true, gamma: false, theta: false, vega: false });
break;
case 'otm_call':
setOptionType('call');
setM1({ S: 90, delta: 0.287, gamma: 0.028, theta: -0.012, vega: 0.08, rho: 0.02 });
setM1On({ delta: true, gamma: false, theta: false, vega: false, rho: false });
setM2({ price: 1.86, delta: 0.287, gamma: 0.028, theta: -0.012, vega: 0.08, rho: 0.02 });
setM2On({ price: true, delta: true, gamma: false, theta: false, vega: false, rho: false });
setM3({ price: 1.86, S: 90 });
setM4({ S: 90, sigma: 0.20, price: 1.86, delta: 0.287, gamma: 0.028, theta: -0.012, vega: 0.08 });
setM4On({ price: false, delta: true, gamma: false, theta: false, vega: false });
break;
}
};
const DecayBadge = ({ dte }: { dte: number }) => {
const phase = getDecayPhase(dte);
return <Badge variant="outline" className={`${phase.color} border-current`}>{phase.name} · {dte}DTE</Badge>;
};
const TInput = ({ label, value, onChange }: { label: string; value: number; onChange: (v: string) => void }) => (
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs">{label}</Label>
<div className="flex">
<Input type="number" value={value} onChange={e => onChange(e.target.value)} className="border-[#E6E8EA] text-[#1E2026] rounded-r-none" />
<div className="flex">
<Button size="sm" className={`rounded-none ${tUnit === 'days' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2`} onClick={() => setTUnit('days')}><Calendar className="w-3 h-3 mr-1" /></Button>
<Button size="sm" className={`rounded-l-none ${tUnit === 'years' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2`} onClick={() => setTUnit('years')}></Button>
</div>
</div>
<p className="text-[10px] text-[#848E9C]">{tUnit === 'days' ? `${contract.T_days}天 ≈ ${T.toFixed(3)}` : `${contract.T_days}年 ≈ ${Math.round(contract.T_days * 365)}`}</p>
</div>
);
return (
<div className="space-y-8">
<div className="bg-[#F5F5F5] border border-[#E6E8EA] rounded-lg p-6">
<h3 className="text-lg font-semibold text-[#1E2026] mb-3"></h3>
<p className="text-sm text-[#848E9C]">
<strong className="text-[#F6465D]">Theta</strong> Theta T <strong className="text-[#F0B90B]"></strong>
r=0, q=0
</p>
</div>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader className="pb-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<CardTitle className="text-[#1E2026] text-base"></CardTitle>
<div className="flex gap-2">
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('atm_call')}>ATM Call</Button>
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('itm_call')}>ITM Call</Button>
<Button size="sm" variant="outline" className="text-xs border-[#E6E8EA] text-[#848E9C]" onClick={() => preset('otm_call')}>OTM Call</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4 items-end">
<div className="space-y-2"><Label className="text-[#848E9C] text-xs"> K</Label><Input type="number" value={contract.K} onChange={e => upd(setContract, 'K', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] w-24" /></div>
<TInput label="剩余期限" value={contract.T_days} onChange={v => upd(setContract, 'T_days', v)} />
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex gap-2">
<Button size="sm" className={`${optionType === 'call' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('call')}><TrendingUp className="w-3 h-3 mr-1" /> Call</Button>
<Button size="sm" className={`${optionType === 'put' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('put')}><TrendingDown className="w-3 h-3 mr-1" /> Put</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<Tabs value={mode} onValueChange={v => setMode(v as any)} className="w-full">
<TabsList className="grid w-full grid-cols-4 bg-white border border-[#E6E8EA] rounded-[6px]">
<TabsTrigger value="s-greeks-to-price" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs md:text-sm"><ArrowRight className="w-3 h-3 mr-1" />S + </TabsTrigger>
<TabsTrigger value="price-greeks-to-s" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs md:text-sm"><ArrowLeft className="w-3 h-3 mr-1" /> + S</TabsTrigger>
<TabsTrigger value="price-to-sigma" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs md:text-sm"><Sigma className="w-3 h-3 mr-1" /> + S σ</TabsTrigger>
<TabsTrigger value="s-sigma-greeks-to-t" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs md:text-sm"><Clock className="w-3 h-3 mr-1" />S + σ + T</TabsTrigger>
</TabsList>
{/* Mode 1 */}
<TabsContent value="s-greeks-to-price" className="mt-8 space-y-6">
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px]">
<CardHeader>
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2"><Search className="w-5 h-5 text-[#F0B90B]" /> S + </CardTitle>
<p className="text-sm text-[#848E9C]"> S Theta <strong className="text-[#F0B90B]">σ T</strong>Theta /</p>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<GInput label="期货价格 S" value={m1.S} onChange={v => upd(setM1, 'S', v)} color="text-blue-400" icon={<DollarSign className="w-4 h-4" />} on={true} toggle={() => {}} req />
<GInput label="Delta" value={m1.delta} onChange={v => upd(setM1, 'delta', v)} color="text-emerald-400" icon={<Activity className="w-4 h-4" />} on={m1On.delta} toggle={() => setM1On(p => ({ ...p, delta: !p.delta }))} />
<GInput label="Gamma" value={m1.gamma} onChange={v => upd(setM1, 'gamma', v)} color="text-blue-400" icon={<Activity className="w-4 h-4" />} on={m1On.gamma} toggle={() => setM1On(p => ({ ...p, gamma: !p.gamma }))} />
<div className="relative">
<GInput label={`Theta (${m1ThetaUnit === 'daily' ? '日' : '年'})`} value={m1.theta} onChange={v => upd(setM1, 'theta', v)} color="text-rose-400" icon={<Clock className="w-4 h-4" />} on={m1On.theta} toggle={() => setM1On(p => ({ ...p, theta: !p.theta }))} />
{m1On.theta && (
<div className="absolute top-0 right-0 -mt-1 -mr-1">
<div className="flex bg-slate-800 rounded border border-slate-700 overflow-hidden">
<button onClick={() => setM1ThetaUnit('daily')} className={`px-2 py-0.5 text-[10px] ${m1ThetaUnit === 'daily' ? 'bg-rose-600 text-white' : 'text-slate-400'}`}></button>
<button onClick={() => setM1ThetaUnit('annual')} className={`px-2 py-0.5 text-[10px] ${m1ThetaUnit === 'annual' ? 'bg-rose-600 text-white' : 'text-slate-400'}`}></button>
</div>
</div>
)}
</div>
<GInput label="Vega" value={m1.vega} onChange={v => upd(setM1, 'vega', v)} color="text-purple-400" icon={<Activity className="w-4 h-4" />} on={m1On.vega} toggle={() => setM1On(p => ({ ...p, vega: !p.vega }))} />
<GInput label="Rho" value={m1.rho} onChange={v => upd(setM1, 'rho', v)} color="text-cyan-400" icon={<Activity className="w-4 h-4" />} on={m1On.rho} toggle={() => setM1On(p => ({ ...p, rho: !p.rho }))} />
</div>
{res1 && (
<div className="bg-slate-800/50 rounded-lg p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-white"></h4>
<div className="flex gap-2">
<Badge className="bg-emerald-600"></Badge>
{res1.T_from_theta && <Badge className="bg-rose-600">T Theta </Badge>}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-slate-800 rounded-lg space-y-3">
<p className="text-sm text-slate-400"></p>
<p className="text-3xl font-bold text-emerald-400">${res1.price.toFixed(4)}</p>
<p className="text-xs text-slate-500">σ = {res1.sigma.toFixed(4)} ({(res1.sigma * 100).toFixed(2)}%)</p>
</div>
<div className="text-center p-4 bg-slate-800 rounded-lg space-y-3">
<p className="text-sm text-slate-400"></p>
<p className="text-3xl font-bold text-rose-400">{res1.DTE} <span className="text-lg"></span></p>
<p className="text-xs text-slate-500">T = {res1.T.toFixed(4)} </p>
<div className="flex justify-center mt-1"><DecayBadge dte={res1.DTE} /></div>
</div>
<div className="space-y-2">
<RRow label="Delta" tgt={m1On.delta ? m1.delta : undefined} cmp={res1.greeks.delta} res={res1.residuals.delta} />
<RRow label="Gamma" tgt={m1On.gamma ? m1.gamma : undefined} cmp={res1.greeks.gamma} res={res1.residuals.gamma} />
<RRow label={`Theta(${m1ThetaUnit === 'daily' ? '日' : '年'})`} tgt={m1On.theta ? m1.theta : undefined} cmp={m1ThetaUnit === 'daily' ? res1.greeks.thetaDaily : res1.greeks.theta} res={res1.residuals.theta} />
<RRow label="Vega" tgt={m1On.vega ? m1.vega : undefined} cmp={res1.greeks.vega} res={res1.residuals.vega} />
<RRow label="Rho" tgt={m1On.rho ? m1.rho : undefined} cmp={res1.greeks.rho} res={res1.residuals.rho} />
</div>
</div>
{truth1 && (
<div className="mt-4 bg-slate-800/30 p-3 rounded text-xs text-slate-500">
<strong className="text-slate-300"></strong>={truth1.intrinsic.toFixed(4)} ={truth1.timeValue.toFixed(4)} ITM={(truth1.itmProb * 100).toFixed(2)}%
</div>
)}
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Mode 2 */}
<TabsContent value="price-greeks-to-s" className="mt-6 space-y-6">
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2"><Search className="w-5 h-5 text-blue-400" /> + S</CardTitle>
<p className="text-sm text-slate-400"> Theta <strong className="text-amber-400">SσT</strong></p>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<GInput label="权利金" value={m2.price} onChange={v => upd(setM2, 'price', v)} color="text-emerald-400" icon={<DollarSign className="w-4 h-4" />} on={m2On.price} toggle={() => setM2On(p => ({ ...p, price: !p.price }))} req />
<GInput label="Delta" value={m2.delta} onChange={v => upd(setM2, 'delta', v)} color="text-emerald-400" icon={<Activity className="w-4 h-4" />} on={m2On.delta} toggle={() => setM2On(p => ({ ...p, delta: !p.delta }))} />
<GInput label="Gamma" value={m2.gamma} onChange={v => upd(setM2, 'gamma', v)} color="text-blue-400" icon={<Activity className="w-4 h-4" />} on={m2On.gamma} toggle={() => setM2On(p => ({ ...p, gamma: !p.gamma }))} />
<div className="relative">
<GInput label={`Theta (${m2ThetaUnit === 'daily' ? '日' : '年'})`} value={m2.theta} onChange={v => upd(setM2, 'theta', v)} color="text-rose-400" icon={<Clock className="w-4 h-4" />} on={m2On.theta} toggle={() => setM2On(p => ({ ...p, theta: !p.theta }))} />
{m2On.theta && (
<div className="absolute top-0 right-0 -mt-1 -mr-1">
<div className="flex bg-slate-800 rounded border border-slate-700 overflow-hidden">
<button onClick={() => setM2ThetaUnit('daily')} className={`px-2 py-0.5 text-[10px] ${m2ThetaUnit === 'daily' ? 'bg-rose-600 text-white' : 'text-slate-400'}`}></button>
<button onClick={() => setM2ThetaUnit('annual')} className={`px-2 py-0.5 text-[10px] ${m2ThetaUnit === 'annual' ? 'bg-rose-600 text-white' : 'text-slate-400'}`}></button>
</div>
</div>
)}
</div>
<GInput label="Vega" value={m2.vega} onChange={v => upd(setM2, 'vega', v)} color="text-purple-400" icon={<Activity className="w-4 h-4" />} on={m2On.vega} toggle={() => setM2On(p => ({ ...p, vega: !p.vega }))} />
<GInput label="Rho" value={m2.rho} onChange={v => upd(setM2, 'rho', v)} color="text-cyan-400" icon={<Activity className="w-4 h-4" />} on={m2On.rho} toggle={() => setM2On(p => ({ ...p, rho: !p.rho }))} />
</div>
{res2 && (
<div className="bg-slate-800/50 rounded-lg p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-white"></h4>
<div className="flex gap-2">
<Badge className="bg-blue-600"></Badge>
{res2.T_from_theta && <Badge className="bg-rose-600">T Theta </Badge>}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-slate-800 rounded-lg space-y-3">
<p className="text-sm text-slate-400"> S</p>
<p className="text-3xl font-bold text-blue-400">${res2.S.toFixed(4)}</p>
<p className="text-xs text-slate-500">σ = {res2.sigma.toFixed(4)} ({(res2.sigma * 100).toFixed(2)}%)</p>
</div>
<div className="text-center p-4 bg-slate-800 rounded-lg space-y-3">
<p className="text-sm text-slate-400"></p>
<p className="text-3xl font-bold text-rose-400">{res2.DTE} <span className="text-lg"></span></p>
<p className="text-xs text-slate-500">T = {res2.T.toFixed(4)} </p>
<div className="flex justify-center mt-1"><DecayBadge dte={res2.DTE} /></div>
</div>
<div className="space-y-2">
<RRow label="权利金" tgt={m2On.price ? m2.price : undefined} cmp={res2.greeks.price} res={res2.residuals.price} />
<RRow label="Delta" tgt={m2On.delta ? m2.delta : undefined} cmp={res2.greeks.delta} res={res2.residuals.delta} />
<RRow label="Gamma" tgt={m2On.gamma ? m2.gamma : undefined} cmp={res2.greeks.gamma} res={res2.residuals.gamma} />
<RRow label={`Theta(${m2ThetaUnit === 'daily' ? '日' : '年'})`} tgt={m2On.theta ? m2.theta : undefined} cmp={m2ThetaUnit === 'daily' ? res2.greeks.thetaDaily : res2.greeks.theta} res={res2.residuals.theta} />
<RRow label="Vega" tgt={m2On.vega ? m2.vega : undefined} cmp={res2.greeks.vega} res={res2.residuals.vega} />
<RRow label="Rho" tgt={m2On.rho ? m2.rho : undefined} cmp={res2.greeks.rho} res={res2.residuals.rho} />
</div>
</div>
<div className="mt-4 bg-slate-800/30 p-3 rounded text-xs text-slate-500">
<strong className="text-slate-300"></strong>={res2.greeks.intrinsic.toFixed(4)} ={res2.greeks.timeValue.toFixed(4)} ITM={(res2.greeks.itmProb * 100).toFixed(2)}%
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Mode 3 */}
<TabsContent value="price-to-sigma" className="mt-6 space-y-6">
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2"><Search className="w-5 h-5 text-purple-400" /> + </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="space-y-2"><Label className="text-slate-400 text-xs"></Label><Input type="number" value={m3.price} onChange={e => upd(setM3, 'price', e.target.value)} className="bg-slate-800 border-slate-700 text-white" /></div>
<div className="space-y-2"><Label className="text-slate-400 text-xs"> S</Label><Input type="number" value={m3.S} onChange={e => upd(setM3, 'S', e.target.value)} className="bg-slate-800 border-slate-700 text-white" /></div>
</div>
{res3 !== null && (
<div className="bg-slate-800/50 rounded-lg p-6 border border-slate-700 mt-4">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-white"></h4>
<Badge className="bg-purple-600">Newton-Raphson </Badge>
</div>
<div className="text-center p-4 bg-slate-800 rounded-lg">
<p className="text-sm text-slate-400 mb-2"> σ</p>
<p className="text-3xl font-bold text-purple-400">{(res3 * 100).toFixed(2)}%</p>
<p className="text-xs text-slate-500 mt-1">: {res3.toFixed(6)}</p>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Mode 4 */}
<TabsContent value="s-sigma-greeks-to-t" className="mt-6 space-y-6">
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white text-base flex items-center gap-2"><Clock className="w-5 h-5 text-rose-400" /> S + σ + T + </CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<GInput label="期货价格 S" value={m4.S} onChange={v => upd(setM4, 'S', v)} color="text-blue-400" icon={<DollarSign className="w-4 h-4" />} on={true} toggle={() => {}} req />
<GInput label="波动率 σ" value={m4.sigma} onChange={v => upd(setM4, 'sigma', v)} color="text-purple-400" icon={<Sigma className="w-4 h-4" />} on={true} toggle={() => {}} req />
<GInput label="权利金 (辅助)" value={m4.price} onChange={v => upd(setM4, 'price', v)} color="text-emerald-400" icon={<DollarSign className="w-4 h-4" />} on={m4On.price} toggle={() => setM4On(p => ({ ...p, price: !p.price }))} />
<GInput label="Delta" value={m4.delta} onChange={v => upd(setM4, 'delta', v)} color="text-emerald-400" icon={<Activity className="w-4 h-4" />} on={m4On.delta} toggle={() => setM4On(p => ({ ...p, delta: !p.delta }))} />
<GInput label="Gamma" value={m4.gamma} onChange={v => upd(setM4, 'gamma', v)} color="text-blue-400" icon={<Activity className="w-4 h-4" />} on={m4On.gamma} toggle={() => setM4On(p => ({ ...p, gamma: !p.gamma }))} />
<div className="relative">
<GInput label={`Theta (${m4ThetaUnit === 'daily' ? '日' : '年'})`} value={m4.theta} onChange={v => upd(setM4, 'theta', v)} color="text-rose-400" icon={<Clock className="w-4 h-4" />} on={m4On.theta} toggle={() => setM4On(p => ({ ...p, theta: !p.theta }))} />
{m4On.theta && (
<div className="absolute top-0 right-0 -mt-1 -mr-1">
<div className="flex bg-slate-800 rounded border border-slate-700 overflow-hidden">
<button onClick={() => setM4ThetaUnit('daily')} className={`px-2 py-0.5 text-[10px] ${m4ThetaUnit === 'daily' ? 'bg-rose-600 text-white' : 'text-slate-400'}`}></button>
<button onClick={() => setM4ThetaUnit('annual')} className={`px-2 py-0.5 text-[10px] ${m4ThetaUnit === 'annual' ? 'bg-rose-600 text-white' : 'text-slate-400'}`}></button>
</div>
</div>
)}
</div>
<GInput label="Vega" value={m4.vega} onChange={v => upd(setM4, 'vega', v)} color="text-purple-400" icon={<Activity className="w-4 h-4" />} on={m4On.vega} toggle={() => setM4On(p => ({ ...p, vega: !p.vega }))} />
</div>
{res4 && (
<div className="bg-slate-800/50 rounded-lg p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-white"></h4>
<Badge className="bg-rose-600"></Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-slate-800 rounded-lg space-y-3">
<p className="text-sm text-slate-400"> T</p>
<p className="text-3xl font-bold text-rose-400">{Math.round(res4.T * 365)} <span className="text-lg"></span></p>
<p className="text-xs text-slate-500">{res4.T.toFixed(4)} </p>
<div className="flex justify-center mt-1"><DecayBadge dte={Math.round(res4.T * 365)} /></div>
</div>
<div className="text-center p-4 bg-slate-800 rounded-lg space-y-3">
<p className="text-sm text-slate-400"></p>
<p className="text-3xl font-bold text-emerald-400">${res4.price.toFixed(4)}</p>
<p className="text-xs text-slate-500">S={m4.S}, σ={m4.sigma}</p>
</div>
<div className="space-y-2">
{res4.residuals.price !== undefined && <RRow label="权利金残差" tgt={m4.price} cmp={res4.greeks.price} res={res4.residuals.price} />}
{res4.residuals.delta !== undefined && <RRow label="Delta" tgt={m4.delta} cmp={res4.greeks.delta} res={res4.residuals.delta} />}
{res4.residuals.gamma !== undefined && <RRow label="Gamma" tgt={m4.gamma} cmp={res4.greeks.gamma} res={res4.residuals.gamma} />}
{res4.residuals.theta !== undefined && <RRow label={`Theta(${m4ThetaUnit === 'daily' ? '日' : '年'})`} tgt={m4.theta} cmp={m4ThetaUnit === 'daily' ? res4.greeks.thetaDaily : res4.greeks.theta} res={res4.residuals.theta} />}
{res4.residuals.vega !== undefined && <RRow label="Vega" tgt={m4.vega} cmp={res4.greeks.vega} res={res4.residuals.vega} />}
</div>
</div>
<div className="mt-4 bg-slate-800/30 p-3 rounded text-xs text-slate-500">
<strong className="text-slate-300"></strong>Delta = N(d) d = N¹(Δ) d = [ln(S/K) + 0.5σ²T]/(σT) T
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
<Card className="bg-slate-900 border-slate-800">
<CardHeader><CardTitle className="text-white text-base"></CardTitle></CardHeader>
<CardContent className="text-sm text-slate-400 space-y-4">
<div>
<h4 className="text-slate-200 font-medium mb-1">1. S + Delta(+Theta) </h4>
<p> ThetaT Delta σ</p>
<p><strong className="text-amber-400"> Theta</strong> T [1/365, 3] T Delta σ Theta (σ, T) </p>
<p className="text-xs text-slate-500 mt-1">Theta -0.016 $0.016 -5.84 $5.84</p>
</div>
<div>
<h4 className="text-slate-200 font-medium mb-1">2. Theta T </h4>
<p className="font-mono text-xs text-slate-500">Θ -S·N'(d)·σ/(2T)</p>
<p>Theta T SσTheta T</p>
</div>
<div>
<h4 className="text-slate-200 font-medium mb-1">3. </h4>
<p> r=0, q=0 BSM </p>
<p className="font-mono text-xs text-slate-500">d = [ln(S/K) + 0.5σ²T] / (σT) · C = S·N(d) - K·N(d)</p>
</div>
</CardContent>
</Card>
</div>
);
}
function GInput({ label, value, onChange, color, icon, on, toggle, req = false }: { label: string; value: number; onChange: (v: string) => void; color: string; icon: React.ReactNode; on: boolean; toggle: () => void; req?: boolean }) {
return (
<div className={`bg-[#F5F5F5] rounded-lg p-3 border ${on ? 'border-[#E6E8EA]' : 'border-[#E6E8EA] opacity-50'}`}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"><span className={color}>{icon}</span><span className="text-xs text-[#848E9C]">{label}</span></div>
{!req && <button onClick={toggle} className={`w-5 h-5 rounded flex items-center justify-center text-xs ${on ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#E6E8EA] text-[#848E9C]'}`}>{on ? <CheckCircle className="w-3 h-3" /> : '+'}</button>}
</div>
<Input type="number" step="0.0001" value={value} onChange={e => onChange(e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" disabled={!on && !req} />
</div>
);
}
function RRow({ label, tgt, cmp, res }: { label: string; tgt?: number; cmp: number; res?: number }) {
if (tgt === undefined) return null;
const absRes = res !== undefined ? Math.abs(res) : 0;
const relErr = Math.abs(tgt) > 0.001 ? (absRes / Math.abs(tgt)) * 100 : 0;
const ok = relErr < 1; const fair = relErr < 5;
return (
<div className="flex items-center justify-between py-1.5 border-b border-[#E6E8EA]">
<div className="flex items-center gap-3">
<span className="text-xs text-[#848E9C] w-20">{label}</span>
<span className="text-xs text-[#848E9C]">: {tgt.toFixed(4)}</span>
<ArrowRight className="w-3 h-3 text-[#848E9C]" />
<span className="text-sm font-mono text-[#1E2026]">{cmp.toFixed(4)}</span>
</div>
{res !== undefined && (
<div className="flex items-center gap-2">
<span className={`text-xs ${ok ? 'text-[#0ECB81]' : fair ? 'text-[#F0B90B]' : 'text-[#F6465D]'}`}>{res > 0 ? '+' : ''}{res.toFixed(4)} ({relErr.toFixed(2)}%)</span>
{ok ? <CheckCircle className="w-3 h-3 text-[#0ECB81]" /> : <AlertTriangle className="w-3 h-3 text-[#F0B90B]" />}
</div>
)}
</div>
);
}

@ -0,0 +1,179 @@
import { useState, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Settings, Clock } from 'lucide-react';
import { calculateBSM } from '@/lib/bsm';
import type { BSParams } from '@/lib/bsm';
interface DecayRow {
dte: number;
price: number;
thetaDaily: number;
gamma: number;
ratio: number;
dailyDecay: number;
feature: string;
}
export default function TimeDecayResearch() {
const [params, setParams] = useState({ S: 100, K: 100, sigma: 0.20 });
const [optionType, setOptionType] = useState<'call' | 'put'>('call');
const [moneyness, setMoneyness] = useState(0);
const update = (key: string, val: string) => { const n = parseFloat(val); if (!isNaN(n)) setParams(p => ({ ...p, [key]: n })); };
const S = params.S;
const K = params.K * (1 + moneyness / 100);
const T = 30 / 365;
const bsParams: BSParams = { S, K, T, sigma: params.sigma };
const g = calculateBSM(bsParams, optionType);
const snap = useMemo(() => {
const price = g.price;
const intrinsic = optionType === 'call' ? Math.max(0, S - K) : Math.max(0, K - S);
const timeValue = price - intrinsic;
return {
price, intrinsic, timeValue,
timeValueRatio: price > 0 ? timeValue / price : 0,
delta: g.delta, gamma: g.gamma,
thetaDaily: g.thetaDaily, vega: g.vega,
thetaGammaRatio: g.gamma !== 0 ? g.thetaDaily / g.gamma : 0,
gammaThetaRatio: g.thetaDaily !== 0 ? g.gamma / Math.abs(g.thetaDaily) : 0,
dte: Math.round(T * 365),
};
}, [S, K, T, params.sigma, optionType]);
const decayTable: DecayRow[] = useMemo(() => {
const rows: DecayRow[] = [];
const dtes = [180, 120, 90, 60, 45, 30, 21, 14, 7, 3, 1];
for (const dte of dtes) {
const t = dte / 365;
const bsParams: BSParams = { S, K, T: t, sigma: params.sigma };
const gg = calculateBSM(bsParams, optionType);
const intrinsic = optionType === 'call' ? Math.max(0, S - K) : Math.max(0, K - S);
const price = gg.price;
const timeValue = price - intrinsic;
const dailyDecay = timeValue > 0 ? Math.abs(gg.thetaDaily) / timeValue : 0;
let feature = '';
if (dte >= 60) feature = '缓慢衰减期';
else if (dte >= 30) feature = '甜点区';
else if (dte >= 14) feature = '加速衰减';
else if (dte >= 7) feature = '高速衰减';
else feature = '最后阶段';
rows.push({ dte, price, thetaDaily: gg.thetaDaily, gamma: gg.gamma, ratio: gg.gamma !== 0 ? gg.thetaDaily / gg.gamma : 0, dailyDecay, feature });
}
return rows;
}, [S, K, params.sigma, optionType]);
return (
<div className="space-y-8">
<div className="bg-[#F5F5F5] border border-[#E6E8EA] rounded-lg p-6">
<h3 className="text-lg font-semibold text-[#1E2026] mb-3"></h3>
<p className="text-sm text-[#848E9C]">
线 Theta Gamma
σ·S·T·N'(d)
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200 lg:col-span-2">
<CardHeader className="pb-3">
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2"><Settings className="w-4 h-4 text-[#F0B90B]" /></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> S</Label><Input type="number" value={params.S} onChange={e => update('S', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> K</Label><Input type="number" value={params.K} onChange={e => update('K', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1"><Label className="text-[#848E9C] text-xs"> σ</Label><Input type="number" step="0.01" value={params.sigma} onChange={e => update('sigma', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] h-9" /></div>
<div className="space-y-1">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex gap-2">
<Button size="sm" className={`flex-1 ${optionType === 'call' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('call')}>Call</Button>
<Button size="sm" className={`flex-1 ${optionType === 'put' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('put')}>Put</Button>
</div>
</div>
<div className="space-y-1">
<Label className="text-[#848E9C] text-xs">ATM %</Label>
<Input type="number" value={moneyness} onChange={e => setMoneyness(Number(e.target.value))} step="1" className="border-[#E6E8EA] text-[#1E2026] h-9" />
</div>
</div>
<div className="bg-[#F5F5F5] p-4 rounded-lg text-xs text-[#848E9C] space-y-2">
<p><strong className="text-[#1E2026]">:</strong> V_time = σ·S·T·N'(d)</p>
<p><strong className="text-[#1E2026]">Gamma-Theta (r=0):</strong> Θ = -½σ²S²Γ = -(σ·S·Γ)·(σ·S)/2</p>
<p><strong className="text-[#1E2026]">:</strong> TDTE 29%</p>
</div>
</CardContent>
</Card>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader className="pb-3">
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2"><Clock className="w-4 h-4 text-[#F0B90B]" /></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<Mini label="价格" val={snap.price} color="text-[#1E2026]" />
<Mini label="内在价值" val={snap.intrinsic} color="text-[#848E9C]" />
<Mini label="时间价值" val={snap.timeValue} color="text-[#F0B90B]" />
<Mini label="时间价值占比" val={snap.timeValueRatio * 100} color="text-[#1EAEDB]" unit="%" />
<Mini label="Delta" val={snap.delta} color="text-[#0ECB81]" />
<Mini label="Gamma" val={snap.gamma} color="text-[#1EAEDB]" />
<Mini label="Theta (日)" val={snap.thetaDaily} color="text-[#F6465D]" />
<Mini label="Vega" val={snap.vega} color="text-[#F0B90B]" />
</div>
<Separator className="bg-[#E6E8EA]" />
<div className="text-xs space-y-2">
<p className="text-[#848E9C]">Theta/Gamma : <span className="text-[#1E2026] font-mono">{snap.thetaGammaRatio.toFixed(2)}</span></p>
<p className="text-[#848E9C]">Gamma/Theta : <span className="text-[#1E2026] font-mono">{snap.gammaThetaRatio.toFixed(2)}</span></p>
<p className="text-[#848E9C]">: <span className="text-[#1E2026]">{snap.dte} (DTE)</span></p>
</div>
</div>
</CardContent>
</Card>
</div>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader>
<CardTitle className="text-[#1E2026] text-base">DTE </CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="border-b border-[#E6E8EA]">
<th className="text-left py-2 px-3 text-[#848E9C]">DTE </th>
<th className="text-right py-2 px-3 text-[#848E9C]"></th>
<th className="text-right py-2 px-3 text-[#848E9C]">Theta ()</th>
<th className="text-right py-2 px-3 text-[#848E9C]">Gamma</th>
<th className="text-right py-2 px-3 text-[#848E9C]">Theta/Gamma</th>
<th className="text-right py-2 px-3 text-[#848E9C]"></th>
<th className="text-left py-2 px-3 text-[#848E9C]"></th>
</tr></thead>
<tbody>{decayTable.map((row, i) => (
<tr key={i} className="border-b border-[#E6E8EA] hover:bg-[#F5F5F5]">
<td className="py-2 px-3 text-[#1E2026] font-medium">{row.dte}D</td>
<td className="py-2 px-3 text-right text-[#1E2026]">{row.price.toFixed(4)}</td>
<td className="py-2 px-3 text-right text-[#F6465D]">{row.thetaDaily.toFixed(6)}</td>
<td className="py-2 px-3 text-right text-[#1EAEDB]">{row.gamma.toFixed(6)}</td>
<td className="py-2 px-3 text-right text-[#848E9C]">{row.ratio.toFixed(2)}</td>
<td className="py-2 px-3 text-right text-[#F0B90B]">{(row.dailyDecay * 100).toFixed(2)}%</td>
<td className="py-2 px-3 text-left text-[#848E9C] text-xs">{row.feature}</td>
</tr>
))}</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
function Mini({ label, val, color, unit = '' }: { label: string; val: number; color: string; unit?: string }) {
return (
<div className="bg-[#F5F5F5] rounded p-2 border border-[#E6E8EA]">
<p className="text-[10px] text-[#848E9C]">{label}</p>
<p className={`text-sm font-bold ${color}`}>{val.toFixed(4)}{unit}</p>
</div>
);
}

@ -0,0 +1,130 @@
import { useState, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { generateGreeksCurve } from '@/lib/bsm';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { TrendingUp, TrendingDown, Calendar, BarChart3 } from 'lucide-react';
export default function Visualization() {
const [params, setParams] = useState({ K: 100, T_days: 91, sigma: 0.20 });
const [tUnit, setTUnit] = useState<'days' | 'years'>('days');
const [optionType, setOptionType] = useState<'call' | 'put'>('call');
const [chartType, setChartType] = useState<'price' | 'delta' | 'gamma' | 'theta' | 'vega' | 'combined'>('price');
const T = tUnit === 'days' ? params.T_days / 365 : params.T_days;
const updateParam = (key: string, val: string) => { const n = parseFloat(val); if (!isNaN(n)) setParams(p => ({ ...p, [key]: n })); };
const data = useMemo(() => generateGreeksCurve(params.K, T, params.sigma, optionType, 200, params.K * 0.5, params.K * 1.5), [params, T, optionType]);
const config = chartType === 'price' ? { lines: [{ key: 'price', color: '#0ECB81', name: '期权价格' }, { key: 'intrinsic', color: '#848E9C', name: '内在价值' }, { key: 'timeValue', color: '#F0B90B', name: '时间价值' }], yLabel: '价格 ($)' }
: chartType === 'delta' ? { lines: [{ key: 'delta', color: '#0ECB81', name: 'Delta' }], yLabel: 'Delta' }
: chartType === 'gamma' ? { lines: [{ key: 'gamma', color: '#1EAEDB', name: 'Gamma' }], yLabel: 'Gamma' }
: chartType === 'theta' ? { lines: [{ key: 'theta', color: '#F6465D', name: 'Theta (日)' }], yLabel: 'Theta' }
: chartType === 'vega' ? { lines: [{ key: 'vega', color: '#F0B90B', name: 'Vega' }], yLabel: 'Vega' }
: { lines: [{ key: 'delta', color: '#0ECB81', name: 'Delta' }, { key: 'gamma', color: '#1EAEDB', name: 'Gamma' }], yLabel: '数值' };
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const p = payload[0]?.payload;
return (
<div className="bg-white border border-[#E6E8EA] rounded-lg p-3 shadow-lg">
<p className="text-[#1E2026] font-medium mb-2"> S = ${p?.S}</p>
{payload.map((e: any) => <p key={e.dataKey} className="text-sm" style={{ color: e.color }}>{e.name}: {e.value?.toFixed(4)}</p>)}
<div className="mt-2 pt-2 border-t border-[#E6E8EA] text-xs text-[#848E9C]">
<p>ITM/OTM: {p?.S > params.K ? 'ITM' : p?.S < params.K ? 'OTM' : 'ATM'}</p>
<p>: {(p?.S - params.K).toFixed(2)}</p>
</div>
</div>
);
}
return null;
};
return (
<div className="space-y-8">
<div className="bg-[#F5F5F5] border border-[#E6E8EA] rounded-lg p-6">
<h3 className="text-lg font-semibold text-[#1E2026] mb-3"></h3>
<p className="text-sm text-[#848E9C]">
S 线 ATM S=K/
</p>
</div>
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardContent className="p-6">
<div className="flex flex-wrap gap-4 items-end">
<div className="space-y-2"><Label className="text-[#848E9C] text-xs"> K</Label><Input type="number" value={params.K} onChange={e => updateParam('K', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] w-24" /></div>
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex">
<Input type="number" value={params.T_days} onChange={e => updateParam('T_days', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] w-24 rounded-r-none" />
<Button size="sm" className={`rounded-none ${tUnit === 'days' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2`} onClick={() => setTUnit('days')}><Calendar className="w-3 h-3 mr-1" /></Button>
<Button size="sm" className={`rounded-l-none ${tUnit === 'years' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'} text-xs px-2`} onClick={() => setTUnit('years')}></Button>
</div>
</div>
<div className="space-y-2"><Label className="text-[#848E9C] text-xs"> σ</Label><Input type="number" step="0.01" value={params.sigma} onChange={e => updateParam('sigma', e.target.value)} className="border-[#E6E8EA] text-[#1E2026] w-24" /></div>
<div className="space-y-2">
<Label className="text-[#848E9C] text-xs"></Label>
<div className="flex gap-2">
<Button size="sm" className={`${optionType === 'call' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('call')}><TrendingUp className="w-3 h-3 mr-1" /> Call</Button>
<Button size="sm" className={`${optionType === 'put' ? 'bg-[#F0B90B] text-[#1E2026]' : 'bg-[#F5F5F5] text-[#848E9C]'}`} onClick={() => setOptionType('put')}><TrendingDown className="w-3 h-3 mr-1" /> Put</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<Tabs value={chartType} onValueChange={v => setChartType(v as any)} className="w-full">
<TabsList className="grid w-full grid-cols-6 bg-white border border-[#E6E8EA] rounded-[6px]">
<TabsTrigger value="price" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs"></TabsTrigger>
<TabsTrigger value="delta" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs">Delta线</TabsTrigger>
<TabsTrigger value="gamma" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs">Gamma线</TabsTrigger>
<TabsTrigger value="theta" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs">Theta线</TabsTrigger>
<TabsTrigger value="vega" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs">Vega线</TabsTrigger>
<TabsTrigger value="combined" className="data-[state=active]:bg-[#F0B90B] data-[state=active]:text-[#1E2026] text-xs"></TabsTrigger>
</TabsList>
<TabsContent value={chartType} className="mt-8">
<Card className="bg-white border border-[#E6E8EA] rounded-[12px] shadow-[rgba(32,32,37,0.05)_0px_3px_5px_0px] hover:shadow-[rgba(8,8,8,0.05)_0px_3px_5px_5px] transition-all duration-200">
<CardHeader>
<CardTitle className="text-[#1E2026] text-base flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-[#F0B90B]" />
{chartType === 'price' && '期权价格分解: 内在价值 + 时间价值'}
{chartType === 'delta' && 'Delta 随标的价格变化的 S 型曲线'}
{chartType === 'gamma' && 'Gamma 钟形分布: ATM 区域峰值'}
{chartType === 'theta' && 'Theta 时间衰减曲线'}
{chartType === 'vega' && 'Vega 波动率敏感度分布'}
{chartType === 'combined' && 'Delta 与 Gamma 联合对比'}
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#E6E8EA" />
<XAxis dataKey="S" stroke="#848E9C" tick={{ fill: '#848E9C', fontSize: 12 }} label={{ value: '标的资产价格 S', position: 'insideBottom', offset: -5, fill: '#848E9C' }} />
<YAxis stroke="#848E9C" tick={{ fill: '#848E9C', fontSize: 12 }} label={{ value: config.yLabel, angle: -90, position: 'insideLeft', fill: '#848E9C' }} />
<Tooltip content={<CustomTooltip />} />
<Legend wrapperStyle={{ color: '#848E9C' }} />
{config.lines.map(l => <Line key={l.key} type="monotone" dataKey={l.key} stroke={l.color} strokeWidth={2} dot={false} name={l.name} activeDot={{ r: 6, fill: l.color }} />)}
</LineChart>
</ResponsiveContainer>
</div>
<div className="mt-4 text-xs text-[#848E9C] bg-[#F5F5F5] p-4 rounded-lg border border-[#E6E8EA]">
<strong className="text-[#1E2026]"></strong>
{chartType === 'price' && 'ATM 附近时间价值最大;深度 ITM 价格趋近于内在价值;深度 OTM 价格趋近于 0。'}
{chartType === 'delta' && 'Call Delta 从 0深度 OTM平滑上升至 1深度 ITMATM 附近约为 0.5+。'}
{chartType === 'gamma' && 'Gamma 在 ATM 处达到峰值,向两侧指数衰减。临近到期时 ATM Gamma 发散增长。'}
{chartType === 'theta' && 'ATM Theta 绝对值最大(时间衰减最快),深度 ITM/OTM 衰减趋缓。'}
{chartType === 'vega' && 'Vega 同样在 ATM 处最大,与到期时间平方根 √T 成正比。'}
{chartType === 'combined' && 'Delta 的拐点处 Gamma 最大,两者呈现微分-积分关系。'}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

@ -0,0 +1,84 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {
xl: "calc(var(--radius) + 4px)",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
xs: "calc(var(--radius) - 6px)",
},
boxShadow: {
xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
},
plugins: [require("tailwindcss-animate")],
}

@ -0,0 +1,34 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

@ -0,0 +1,17 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,18 @@
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import { inspectAttr } from 'kimi-plugin-inspect-react'
// https://vite.dev/config/
export default defineConfig({
base: './',
plugins: [inspectAttr(), react()],
server: {
port: 3000,
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
Loading…
Cancel
Save