chore: 配置与杂项(makefile/.env.example/CI 元数据)
This commit is contained in:
@@ -0,0 +1,314 @@
|
|||||||
|
---
|
||||||
|
name: i18n-translate
|
||||||
|
description: >-
|
||||||
|
Complete and maintain frontend i18n translations for this project. Covers
|
||||||
|
finding missing translation keys, detecting untranslated entries, and adding
|
||||||
|
translations for all supported locales (en, zh, zh-TW, fr, ja, ru, vi). Use for any
|
||||||
|
task involving frontend locale files, missing translation keys, untranslated
|
||||||
|
UI text, `t(...)` keys, `useTranslation()`, static i18n keys, button/label/
|
||||||
|
toast/dialog/placeholder/validation copy, or adding/fixing even a single
|
||||||
|
i18n key. Use when review findings mention missing i18n, when new UI text
|
||||||
|
needs translation, or when the user asks to add translations, fix i18n, or
|
||||||
|
complete missing translations. Always load and follow this skill before
|
||||||
|
translating, adding locale keys, or editing frontend i18n files.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frontend i18n Translation Workflow
|
||||||
|
|
||||||
|
## Mandatory Preflight
|
||||||
|
|
||||||
|
- Read this entire `SKILL.md` before any frontend i18n work, including one-key fixes.
|
||||||
|
- Before editing locale files, confirm the source text comes from a `t(...)` key, `en.json`, existing UI copy, or an explicitly requested new UI string.
|
||||||
|
- Use the user conversation only to understand the task target. Do not copy conversation text, review wording, or task descriptions directly into locale values.
|
||||||
|
- Before translating each key, re-think the intended UI copy from the code and locale context instead of treating the surrounding chat as the translation source.
|
||||||
|
|
||||||
|
### Hard Constraint: Locale Writes Go Through the Script
|
||||||
|
|
||||||
|
- You MUST NOT edit `web/src/i18n/locales/*.json` directly with text-editing tools (StrReplace, Write, search-and-replace, manual JSON edits, etc.). This applies even to a single key.
|
||||||
|
- ALL locale writes MUST go through the `add-missing-keys.mjs` script, followed by `bun run i18n:sync`. The script is the only sanctioned way to add or change locale values.
|
||||||
|
- Why this is mandatory, not optional:
|
||||||
|
- Hand-editing reliably drops one or more of the seven locales (`en`, `zh`, `zh-TW`, `fr`, `ja`, `ru`, `vi`), leaving keys missing in some languages.
|
||||||
|
- Hand-editing breaks the required alphabetical key order and introduces JSON syntax errors (trailing commas, mismatched quotes).
|
||||||
|
- The script writes all seven files atomically with consistent sorting, so the locale set stays in sync by construction.
|
||||||
|
- The script does not do the translation for you. You still must reason out each locale's copy and populate the script's `newKeys` object; the script only handles insertion, sorting, and writing. Do not skip the script just because the thinking happens regardless.
|
||||||
|
|
||||||
|
## Scope Checklist
|
||||||
|
|
||||||
|
Before editing files, treat the task as covered by this skill if it involves:
|
||||||
|
|
||||||
|
- `i18n`, translation, locale files, language packs, missing keys, or untranslated text
|
||||||
|
- `t('...')`, `useTranslation()`, `static-keys.ts`, or `locales/*.json`
|
||||||
|
- UI copy in buttons, labels, toasts, dialogs, placeholders, validation messages, descriptions, or table/empty states
|
||||||
|
- A review finding about missing i18n keys
|
||||||
|
|
||||||
|
Do not skip this workflow because the fix is "just one key".
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
- Locale files: `web/src/i18n/locales/{en,zh,zh-TW,fr,ja,ru,vi}.json`
|
||||||
|
- Format: flat JSON under `"translation"` key, keys are English source strings
|
||||||
|
- Base locale: `en.json` (most keys), fallback: `zh` (Chinese)
|
||||||
|
- Sync script: `bun run i18n:sync` (from `web/`)
|
||||||
|
- All `t()` calls must have corresponding keys in every locale file
|
||||||
|
|
||||||
|
## Small Fix Path
|
||||||
|
|
||||||
|
For a single known missing key (still script-only, no direct JSON edits):
|
||||||
|
|
||||||
|
1. Confirm the exact key at the call site and verify it is absent from all locale files.
|
||||||
|
2. Add the key via `add-missing-keys.mjs`, populating its `newKeys` object for every supported locale: `en`, `zh`, `zh-TW`, `fr`, `ja`, `ru`, `vi`. Even one key goes through the script; do not hand-edit the JSON.
|
||||||
|
3. The script preserves the flat `"translation"` object and keeps keys alphabetically sorted automatically.
|
||||||
|
4. Run a targeted search for the key in code and locale files.
|
||||||
|
5. Run `bun run i18n:sync` to normalize file order. This step is mandatory, not optional.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1: Run sync and read report
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web && bun run i18n:sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Read `web/src/i18n/locales/_reports/_sync-report.json` to see per-locale status (missingCount, extrasCount, untranslatedCount).
|
||||||
|
|
||||||
|
### Step 2: Find missing keys (used in code but not in locale files)
|
||||||
|
|
||||||
|
Create and run `web/scripts/find-missing-keys.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const LOCALES_DIR = path.resolve('src/i18n/locales')
|
||||||
|
const SRC_DIR = path.resolve('src')
|
||||||
|
|
||||||
|
const en = JSON.parse(await fs.readFile(path.join(LOCALES_DIR, 'en.json'), 'utf8'))
|
||||||
|
const enKeys = new Set(Object.keys(en.translation))
|
||||||
|
|
||||||
|
const tCallRegex = /\bt\(\s*['"`]([^'"`\n]+?)['"`]\s*[,)]/g
|
||||||
|
const tCallMultilineRegex = /\bt\(\s*['"`]([^'"`]+?)['"`]\s*\)/g
|
||||||
|
|
||||||
|
async function walkDir(dir) {
|
||||||
|
const files = []
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name)
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (['node_modules', '.git', 'locales', '_reports', '_extras'].includes(entry.name)) continue
|
||||||
|
files.push(...(await walkDir(fullPath)))
|
||||||
|
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
||||||
|
files.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await walkDir(SRC_DIR)
|
||||||
|
const missingKeys = new Map()
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = await fs.readFile(file, 'utf8')
|
||||||
|
const relPath = path.relative(SRC_DIR, file)
|
||||||
|
for (const regex of [tCallRegex, tCallMultilineRegex]) {
|
||||||
|
regex.lastIndex = 0
|
||||||
|
let match
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const key = match[1]
|
||||||
|
if (key.startsWith('{{') || key.includes('${')) continue
|
||||||
|
if (!enKeys.has(key)) {
|
||||||
|
if (!missingKeys.has(key)) missingKeys.set(key, [])
|
||||||
|
missingKeys.get(key).push(relPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingKeys.size === 0) {
|
||||||
|
console.log('All t() keys found in en.json!')
|
||||||
|
} else {
|
||||||
|
console.log(`Found ${missingKeys.size} missing keys:\n`)
|
||||||
|
for (const [key, files] of [...missingKeys.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
||||||
|
console.log(` "${key}"`)
|
||||||
|
for (const f of [...new Set(files)]) console.log(` -> ${f}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Find untranslated entries (value equals English)
|
||||||
|
|
||||||
|
Create and run `web/scripts/find-untranslated.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const LOCALES_DIR = path.resolve('src/i18n/locales')
|
||||||
|
const en = JSON.parse(await fs.readFile(path.join(LOCALES_DIR, 'en.json'), 'utf8'))
|
||||||
|
const enTrans = en.translation
|
||||||
|
|
||||||
|
// Brand names, URLs, technical terms — skip these
|
||||||
|
const skipPatterns = [
|
||||||
|
/^https?:\/\//, /^smtp\./, /^socks5:/, /^name@/, /^noreply@/,
|
||||||
|
/^org-/, /^price_/, /^whsec_/, /^edit_this$/, /^my-status$/,
|
||||||
|
/^_copy$/, /^gpt-/, /^checkout\./, /^footer\./, /^\[?\{/,
|
||||||
|
/^"default/, /^\/status\//, /^\/your\//, /^example\.com/,
|
||||||
|
/^AZURE_/, /^AccessKey/, /^OAuth/, /^Client /, /^Webhook URL/,
|
||||||
|
/^API URL$/, /^Well-Known/, /^Worker URL$/, /^Uptime Kuma/,
|
||||||
|
/^New API/, /^Baidu V2$/, /^Zhipu V4$/, /^Quota:$/,
|
||||||
|
]
|
||||||
|
|
||||||
|
const brandNames = new Set([
|
||||||
|
'AIGC2D','Anthropic','API2GPT','Claude','Cloudflare','Cohere','DeepSeek',
|
||||||
|
'Discord','DoubaoVideo','FastGPT','Gemini','GitHub','Jimeng','JustSong',
|
||||||
|
'LingYiWanWu','LinuxDO','Midjourney','MidjourneyPlus','MiniMax','Mistral',
|
||||||
|
'MokaAI','Moonshot','NewAPI','OhMyGPT','Ollama','OpenAI','OpenAIMax',
|
||||||
|
'OpenRouter','Passkey','Perplexity','QuantumNous','Replicate','SiliconFlow',
|
||||||
|
'Stripe','Submodel','SunoAPI','Telegram','Tencent','Vertex AI','VolcEngine',
|
||||||
|
'WeChat','Xinference','Xunfei','AI Proxy','One API',
|
||||||
|
])
|
||||||
|
|
||||||
|
const locales = ['fr', 'ja', 'ru', 'zh', 'zh-TW', 'vi']
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
const locFile = JSON.parse(await fs.readFile(path.join(LOCALES_DIR, `${locale}.json`), 'utf8'))
|
||||||
|
const locTrans = locFile.translation
|
||||||
|
const untranslated = {}
|
||||||
|
|
||||||
|
for (const [key, enVal] of Object.entries(enTrans)) {
|
||||||
|
const locVal = locTrans[key]
|
||||||
|
if (locVal === undefined || locVal !== enVal) continue
|
||||||
|
if (brandNames.has(key)) continue
|
||||||
|
if (skipPatterns.some(p => p.test(key))) continue
|
||||||
|
if (typeof enVal === 'string' && enVal.length < 4) continue
|
||||||
|
if (/[a-zA-Z]{3,}/.test(String(enVal))) untranslated[key] = enVal
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = Object.keys(untranslated).length
|
||||||
|
if (count > 0) {
|
||||||
|
console.log(`\n=== ${locale} (${count} untranslated) ===`)
|
||||||
|
for (const [k, v] of Object.entries(untranslated))
|
||||||
|
console.log(` ${JSON.stringify(k)}: ${JSON.stringify(v)}`)
|
||||||
|
} else {
|
||||||
|
console.log(`\n=== ${locale}: all translated ===`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Add translations
|
||||||
|
|
||||||
|
This script is the ONLY sanctioned way to write locale values. You MUST NOT bypass it by hand-filling the JSON files. Create `web/scripts/add-missing-keys.mjs` with this exact structure:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const LOCALES_DIR = path.resolve('src/i18n/locales')
|
||||||
|
|
||||||
|
function stableStringify(obj) {
|
||||||
|
return JSON.stringify(obj, null, 2) + '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKeys = {
|
||||||
|
en: { /* "key": "English value" */ },
|
||||||
|
zh: { /* "key": "中文翻译" */ },
|
||||||
|
'zh-TW': { /* "key": "繁體中文翻譯" */ },
|
||||||
|
fr: { /* "key": "Traduction française" */ },
|
||||||
|
ja: { /* "key": "日本語翻訳" */ },
|
||||||
|
ru: { /* "key": "Русский перевод" */ },
|
||||||
|
vi: { /* "key": "Bản dịch tiếng Việt" */ },
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let totalAdded = 0
|
||||||
|
|
||||||
|
for (const [locale, trans] of Object.entries(newKeys)) {
|
||||||
|
const filePath = path.join(LOCALES_DIR, `${locale}.json`)
|
||||||
|
const json = JSON.parse(await fs.readFile(filePath, 'utf8'))
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
for (const [key, value] of Object.entries(trans)) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(json.translation, key)) {
|
||||||
|
json.translation[key] = value
|
||||||
|
count++
|
||||||
|
} else if (json.translation[key] !== value) {
|
||||||
|
json.translation[key] = value
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
json.translation = Object.fromEntries(
|
||||||
|
Object.entries(json.translation).sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
)
|
||||||
|
await fs.writeFile(filePath, stableStringify(json), 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${locale}: ${count} translations applied`)
|
||||||
|
totalAdded += count
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nTotal: ${totalAdded} translations applied`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => { console.error(err); process.exitCode = 1 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Populate the `newKeys` object with actual translations for each locale.
|
||||||
|
|
||||||
|
### Step 5: Verify and clean up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
node scripts/add-missing-keys.mjs # apply translations
|
||||||
|
node scripts/find-missing-keys.mjs # verify: should say "All t() keys found"
|
||||||
|
bun run i18n:sync # normalize file order
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete temporary scripts after completion.
|
||||||
|
|
||||||
|
## Translation Guidelines
|
||||||
|
|
||||||
|
### Source Text Rules
|
||||||
|
|
||||||
|
- Reconsider every key's UI meaning before translating: component location, user action, placeholder variables, button/label/toast/dialog/validation context, and whether the copy is a noun, command, status, or full sentence.
|
||||||
|
- Prefer the English key or `en` value as the source text. Use the call site only to clarify meaning, tone, and constraints.
|
||||||
|
- Do not copy chat messages, review comments, issue descriptions, or task wording as translation text.
|
||||||
|
- If the source text is unclear, inspect the code and locale files first. Ask the user for exact source copy only when the intended UI text remains ambiguous.
|
||||||
|
|
||||||
|
### Length and Layout Awareness
|
||||||
|
|
||||||
|
- Consider whether translated text may overflow the UI before choosing final wording, especially for buttons, table headers, menu items, labels, toasts, dialog titles, tabs, badges, and empty states.
|
||||||
|
- For languages that often expand relative to English, especially French, Russian, and Vietnamese, prefer natural but compact wording.
|
||||||
|
- Do not sacrifice meaning just to shorten text. When the call site has limited space, choose the shortest clear translation that preserves the UI intent.
|
||||||
|
- For interpolated variables, counts, model names, provider names, quotas, and dates, consider the longest realistic rendered text, not only the translation string itself.
|
||||||
|
|
||||||
|
| Language | Code | Notes |
|
||||||
|
|----------|------|-------|
|
||||||
|
| English | en | Base locale, key = value |
|
||||||
|
| Chinese | zh | Fallback locale, must be complete |
|
||||||
|
| Traditional Chinese | zh-TW | Use natural Traditional Chinese wording |
|
||||||
|
| French | fr | Many English cognates are valid (e.g., "Configuration") |
|
||||||
|
| Japanese | ja | Use katakana for technical loanwords |
|
||||||
|
| Russian | ru | Use formal register |
|
||||||
|
| Vietnamese | vi | Use standard Vietnamese |
|
||||||
|
|
||||||
|
**Keep as English (do not translate):**
|
||||||
|
- Brand/product names (OpenAI, Claude, Gemini, etc.)
|
||||||
|
- URLs and email placeholders
|
||||||
|
- Technical identifiers (JSON keys, API paths, model names)
|
||||||
|
- Code-like strings (gpt-3.5-turbo, price_xxx, etc.)
|
||||||
|
|
||||||
|
**Always translate:**
|
||||||
|
- UI labels, button text, error messages, descriptions
|
||||||
|
- Time units (hours, minutes, months, years)
|
||||||
|
- Action words (Move, Show, Delete, etc.)
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
1. All scripts run from `web/` directory
|
||||||
|
2. Use `node scripts/xxx.mjs` (ESM format with top-level await)
|
||||||
|
3. Sort keys alphabetically when writing locale files
|
||||||
|
4. Always run `bun run i18n:sync` as the final step
|
||||||
|
5. Delete temporary scripts after completion
|
||||||
|
6. The `{{variable}}` placeholders in keys must be preserved in all translations
|
||||||
|
7. NEVER edit `locales/*.json` directly. Any non-script write to a locale file (StrReplace, Write, manual JSON edit) is non-compliant, including single-key fixes.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
name: shadcn-ui
|
||||||
|
description: >-
|
||||||
|
Give the assistant project-aware shadcn/ui context: components.json,
|
||||||
|
composition patterns, CLI, registries, theming, and MCP. Use when working on
|
||||||
|
web UI, shadcn components, or presets. Overview aligns with
|
||||||
|
https://ui.shadcn.com/docs/skills.md; full upstream skill text is vendored
|
||||||
|
under vendor/shadcn/.
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Canonical overview: https://ui.shadcn.com/docs/skills.md -->
|
||||||
|
|
||||||
|
# Skills (shadcn/ui)
|
||||||
|
|
||||||
|
Skills give AI assistants project-aware context about shadcn/ui. When used, the assistant knows how to find, install, compose, and customize components using the correct APIs and patterns for your project.
|
||||||
|
|
||||||
|
For example, you can ask:
|
||||||
|
|
||||||
|
- _"Add a login form with email and password fields."_
|
||||||
|
- _"Create a settings page with a form for updating profile information."_
|
||||||
|
- _"Build a dashboard with a sidebar, stats cards, and a data table."_
|
||||||
|
- _"Switch to --preset [CODE]"_
|
||||||
|
- _"Can you add a hero from @tailark?"_
|
||||||
|
|
||||||
|
The skill reads your project's `components.json` and provides your framework, aliases, installed components, icon library, and base library so it can generate correct code on the first try.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install (ecosystem vs this repo)
|
||||||
|
|
||||||
|
Official install from [Skills — shadcn/ui](https://ui.shadcn.com/docs/skills.md):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add shadcn/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
That installs the skill where the `skills` CLI is available. **This repository** keeps the same intent under `.agents/skills/shadcn-ui/` (overview here + **vendored** upstream docs in [`vendor/shadcn/`](./vendor/shadcn/)) and runs the shadcn CLI from the frontend app root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web && bunx shadcn@latest info --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Learn more about skills at [skills.sh](https://skills.sh).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's included (and where)
|
||||||
|
|
||||||
|
### Project context
|
||||||
|
|
||||||
|
Run **`shadcn info --json`** (here: `cd web && bunx shadcn@latest info --json`) for framework, Tailwind version, aliases, base (`radix` | `base`), icon library, installed components, and resolved paths.
|
||||||
|
|
||||||
|
### CLI commands
|
||||||
|
|
||||||
|
Full command reference (vendored): [`vendor/shadcn/cli.md`](./vendor/shadcn/cli.md).
|
||||||
|
|
||||||
|
### Theming and customization
|
||||||
|
|
||||||
|
Vendored: [`vendor/shadcn/customization.md`](./vendor/shadcn/customization.md). Live docs: [Theming](https://ui.shadcn.com/docs/theming).
|
||||||
|
|
||||||
|
### Registry authoring
|
||||||
|
|
||||||
|
Not duplicated as a single file in the vendor tree; see [Registry](https://ui.shadcn.com/docs/registry) and `build` in [`vendor/shadcn/cli.md`](./vendor/shadcn/cli.md).
|
||||||
|
|
||||||
|
### MCP server
|
||||||
|
|
||||||
|
Vendored: [`vendor/shadcn/mcp.md`](./vendor/shadcn/mcp.md). Live docs: [MCP Server](https://ui.shadcn.com/docs/mcp).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Project detection** — Applies when `components.json` exists (here: `web/components.json`).
|
||||||
|
2. **Context injection** — Use `shadcn info --json` as ground truth for imports and APIs.
|
||||||
|
3. **Pattern enforcement** — Use [`vendor/shadcn/rules/`](./vendor/shadcn/rules/) for concrete markup checks; the complete official workflow reference is listed below for deeper CLI, registry, and preset questions.
|
||||||
|
4. **Component discovery** — `shadcn docs`, `shadcn search`, MCP, or registries — see the official workflow reference and MCP doc when deeper context is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Learn more (web)
|
||||||
|
|
||||||
|
- [CLI](https://ui.shadcn.com/docs/cli) — complements [`vendor/shadcn/cli.md`](./vendor/shadcn/cli.md)
|
||||||
|
- [Theming](https://ui.shadcn.com/docs/theming)
|
||||||
|
- [Registry](https://ui.shadcn.com/docs/registry)
|
||||||
|
- [skills.sh](https://skills.sh)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vendored upstream bundle (deep rules)
|
||||||
|
|
||||||
|
Snapshot from [shadcn-ui/ui `skills/shadcn`](https://github.com/shadcn-ui/ui/tree/main/skills/shadcn); revision note in [`vendor/shadcn/UPSTREAM.txt`](./vendor/shadcn/UPSTREAM.txt). The upstream workflow is stored as a reference file, with its original skill frontmatter removed, so the vendored copy is not discovered as a second local skill.
|
||||||
|
|
||||||
|
| Doc | Path |
|
||||||
|
| --- | --- |
|
||||||
|
| Official shadcn/ui workflow reference | [`vendor/shadcn/official-shadcn-ui-workflow.md`](./vendor/shadcn/official-shadcn-ui-workflow.md) |
|
||||||
|
| CLI reference | [`vendor/shadcn/cli.md`](./vendor/shadcn/cli.md) |
|
||||||
|
| Theming / customization | [`vendor/shadcn/customization.md`](./vendor/shadcn/customization.md) |
|
||||||
|
| MCP | [`vendor/shadcn/mcp.md`](./vendor/shadcn/mcp.md) |
|
||||||
|
| Forms | [`vendor/shadcn/rules/forms.md`](./vendor/shadcn/rules/forms.md) |
|
||||||
|
| Composition | [`vendor/shadcn/rules/composition.md`](./vendor/shadcn/rules/composition.md) |
|
||||||
|
| Icons | [`vendor/shadcn/rules/icons.md`](./vendor/shadcn/rules/icons.md) |
|
||||||
|
| Styling | [`vendor/shadcn/rules/styling.md`](./vendor/shadcn/rules/styling.md) |
|
||||||
|
| Base vs Radix | [`vendor/shadcn/rules/base-vs-radix.md`](./vendor/shadcn/rules/base-vs-radix.md) |
|
||||||
|
|
||||||
|
**Workflow:** Prefer this **root** `SKILL.md` for repo paths (`web`, Bun). Read **`vendor/shadcn/official-shadcn-ui-workflow.md`** only when you need the complete official component, registry, or preset workflow. Use **`vendor/shadcn/rules/*.md`** when validating concrete markup.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Source: https://github.com/shadcn-ui/ui/tree/56161142f1b83f612462772d18883807b5f0d601/skills/shadcn
|
||||||
|
Branch: main
|
||||||
|
Fetched: 2026-04-29
|
||||||
|
Local file note: upstream SKILL.md is stored as official-shadcn-ui-workflow.md with its original frontmatter removed to avoid exposing the vendored copy as a separate local skill.
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
# shadcn CLI Reference
|
||||||
|
|
||||||
|
Configuration is read from `components.json`.
|
||||||
|
|
||||||
|
> **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. Check `packageManager` from project context to choose the right one. Examples below use `npx shadcn@latest` but substitute the correct runner for the project.
|
||||||
|
|
||||||
|
> **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager from the project's lockfile; there is no `--package-manager` flag.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build
|
||||||
|
- Templates: next, vite, start, react-router, astro
|
||||||
|
- Presets: named, code, URL formats and fields
|
||||||
|
- Switching presets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `init` — Initialize or create a project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest init [components...] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Initializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.
|
||||||
|
|
||||||
|
| Flag | Short | Description | Default |
|
||||||
|
| ----------------------- | ----- | --------------------------------------------------------- | ------- |
|
||||||
|
| `--template <template>` | `-t` | Template (next, start, vite, next-monorepo, react-router) | — |
|
||||||
|
| `--preset [name]` | `-p` | Preset configuration (named, code, or URL) | — |
|
||||||
|
| `--yes` | `-y` | Skip confirmation prompt | `true` |
|
||||||
|
| `--defaults` | `-d` | Use defaults (`--template=next --preset=base-nova`) | `false` |
|
||||||
|
| `--force` | `-f` | Force overwrite existing configuration | `false` |
|
||||||
|
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||||
|
| `--name <name>` | `-n` | Name for new project | — |
|
||||||
|
| `--silent` | `-s` | Mute output | `false` |
|
||||||
|
| `--rtl` | | Enable RTL support | — |
|
||||||
|
| `--reinstall` | | Re-install existing UI components | `false` |
|
||||||
|
| `--monorepo` | | Scaffold a monorepo project | — |
|
||||||
|
| `--no-monorepo` | | Skip the monorepo prompt | — |
|
||||||
|
|
||||||
|
`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.
|
||||||
|
|
||||||
|
### `apply` — Apply a preset to an existing project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest apply [preset] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Applies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components.
|
||||||
|
|
||||||
|
| Flag | Short | Description | Default |
|
||||||
|
| ------------------- | ----- | ------------------------------------------ | ------- |
|
||||||
|
| `--preset <preset>` | — | Preset configuration (named, code, or URL) | — |
|
||||||
|
| `--yes` | `-y` | Skip confirmation prompt | `false` |
|
||||||
|
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||||
|
| `--silent` | `-s` | Mute output | `false` |
|
||||||
|
|
||||||
|
`[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match.
|
||||||
|
If no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`.
|
||||||
|
|
||||||
|
### `add` — Add components
|
||||||
|
|
||||||
|
> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add [components...] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Accepts component names, registry-prefixed names (`@magicui/shimmer-button`), URLs, or local paths.
|
||||||
|
|
||||||
|
| Flag | Short | Description | Default |
|
||||||
|
| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||||
|
| `--yes` | `-y` | Skip confirmation prompt | `false` |
|
||||||
|
| `--overwrite` | `-o` | Overwrite existing files | `false` |
|
||||||
|
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||||
|
| `--all` | `-a` | Add all available components | `false` |
|
||||||
|
| `--path <path>` | `-p` | Target path for the component | — |
|
||||||
|
| `--silent` | `-s` | Mute output | `false` |
|
||||||
|
| `--dry-run` | | Preview all changes without writing files | `false` |
|
||||||
|
| `--diff [path]` | | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — |
|
||||||
|
| `--view [path]` | | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — |
|
||||||
|
|
||||||
|
#### Dry-Run Mode
|
||||||
|
|
||||||
|
Use `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview all changes.
|
||||||
|
npx shadcn@latest add button --dry-run
|
||||||
|
|
||||||
|
# Show diffs for all files (top 5).
|
||||||
|
npx shadcn@latest add button --diff
|
||||||
|
|
||||||
|
# Show the diff for a specific file.
|
||||||
|
npx shadcn@latest add button --diff button.tsx
|
||||||
|
|
||||||
|
# Show contents for all files (top 5).
|
||||||
|
npx shadcn@latest add button --view
|
||||||
|
|
||||||
|
# Show the full content of a specific file.
|
||||||
|
npx shadcn@latest add button --view button.tsx
|
||||||
|
|
||||||
|
# Works with URLs too.
|
||||||
|
npx shadcn@latest add https://api.npoint.io/abc123 --dry-run
|
||||||
|
|
||||||
|
# CSS diffs.
|
||||||
|
npx shadcn@latest add button --diff globals.css
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use dry-run:**
|
||||||
|
|
||||||
|
- When the user asks "what files will this add?" or "what will this change?" — use `--dry-run`.
|
||||||
|
- Before overwriting existing components — use `--diff` to preview the changes first.
|
||||||
|
- When the user wants to inspect component source code without installing — use `--view`.
|
||||||
|
- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.
|
||||||
|
- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.
|
||||||
|
|
||||||
|
> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.
|
||||||
|
|
||||||
|
#### Smart Merge from Upstream
|
||||||
|
|
||||||
|
See [Updating Components in official-shadcn-ui-workflow.md](./official-shadcn-ui-workflow.md#updating-components) for the full workflow.
|
||||||
|
|
||||||
|
### `search` — Search registries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest search <registries...> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Fuzzy search across registries. Also aliased as `npx shadcn@latest list`. Without `-q`, lists all items.
|
||||||
|
|
||||||
|
| Flag | Short | Description | Default |
|
||||||
|
| ------------------- | ----- | ---------------------- | ------- |
|
||||||
|
| `--query <query>` | `-q` | Search query | — |
|
||||||
|
| `--limit <number>` | `-l` | Max items per registry | `100` |
|
||||||
|
| `--offset <number>` | `-o` | Items to skip | `0` |
|
||||||
|
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||||
|
|
||||||
|
### `view` — View item details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest view <items...> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays item info including file contents. Example: `npx shadcn@latest view @shadcn/button`.
|
||||||
|
|
||||||
|
### `docs` — Get component documentation URLs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest docs <components...> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.
|
||||||
|
|
||||||
|
Example output for `npx shadcn@latest docs input button`:
|
||||||
|
|
||||||
|
```
|
||||||
|
base radix
|
||||||
|
|
||||||
|
input
|
||||||
|
docs https://ui.shadcn.com/docs/components/radix/input
|
||||||
|
examples https://raw.githubusercontent.com/.../examples/input-example.tsx
|
||||||
|
|
||||||
|
button
|
||||||
|
docs https://ui.shadcn.com/docs/components/radix/button
|
||||||
|
examples https://raw.githubusercontent.com/.../examples/button-example.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Some components include an `api` link to the underlying library (e.g. `cmdk` for the command component).
|
||||||
|
|
||||||
|
### `diff` — Check for updates
|
||||||
|
|
||||||
|
Do not use this command. Use `npx shadcn@latest add --diff` instead.
|
||||||
|
|
||||||
|
### `info` — Project information
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest info [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.
|
||||||
|
|
||||||
|
| Flag | Short | Description | Default |
|
||||||
|
| ------------- | ----- | ----------------- | ------- |
|
||||||
|
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||||
|
|
||||||
|
**Project Info fields:**
|
||||||
|
|
||||||
|
| Field | Type | Meaning |
|
||||||
|
| -------------------- | --------- | ------------------------------------------------------------------ |
|
||||||
|
| `framework` | `string` | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |
|
||||||
|
| `frameworkVersion` | `string` | Framework version (e.g. `15.2.4`) |
|
||||||
|
| `isSrcDir` | `boolean` | Whether the project uses a `src/` directory |
|
||||||
|
| `isRSC` | `boolean` | Whether React Server Components are enabled |
|
||||||
|
| `isTsx` | `boolean` | Whether the project uses TypeScript |
|
||||||
|
| `tailwindVersion` | `string` | `"v3"` or `"v4"` |
|
||||||
|
| `tailwindConfigFile` | `string` | Path to the Tailwind config file |
|
||||||
|
| `tailwindCssFile` | `string` | Path to the global CSS file |
|
||||||
|
| `aliasPrefix` | `string` | Import alias prefix (e.g. `@`, `~`, `@/`) |
|
||||||
|
| `packageManager` | `string` | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`) |
|
||||||
|
|
||||||
|
**Components.json fields:**
|
||||||
|
|
||||||
|
| Field | Type | Meaning |
|
||||||
|
| -------------------- | --------- | ------------------------------------------------------------------------------------------ |
|
||||||
|
| `base` | `string` | Primitive library (`radix` or `base`) — determines component APIs and available props |
|
||||||
|
| `style` | `string` | Visual style (e.g. `nova`, `vega`) |
|
||||||
|
| `rsc` | `boolean` | RSC flag from config |
|
||||||
|
| `tsx` | `boolean` | TypeScript flag |
|
||||||
|
| `tailwind.config` | `string` | Tailwind config path |
|
||||||
|
| `tailwind.css` | `string` | Global CSS path — this is where custom CSS variables go |
|
||||||
|
| `iconLibrary` | `string` | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |
|
||||||
|
| `aliases.components` | `string` | Component import alias (e.g. `@/components`) |
|
||||||
|
| `aliases.utils` | `string` | Utils import alias (e.g. `@/lib/utils`) |
|
||||||
|
| `aliases.ui` | `string` | UI component alias (e.g. `@/components/ui`) |
|
||||||
|
| `aliases.lib` | `string` | Lib alias (e.g. `@/lib`) |
|
||||||
|
| `aliases.hooks` | `string` | Hooks alias (e.g. `@/hooks`) |
|
||||||
|
| `resolvedPaths` | `object` | Absolute file-system paths for each alias |
|
||||||
|
| `registries` | `object` | Configured custom registries |
|
||||||
|
|
||||||
|
**Links fields:**
|
||||||
|
|
||||||
|
The `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.
|
||||||
|
|
||||||
|
### `build` — Build a custom registry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest build [registry] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Builds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.
|
||||||
|
|
||||||
|
| Flag | Short | Description | Default |
|
||||||
|
| ----------------- | ----- | ----------------- | ------------ |
|
||||||
|
| `--output <path>` | `-o` | Output directory | `./public/r` |
|
||||||
|
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
| Value | Framework | Monorepo support |
|
||||||
|
| -------------- | -------------- | ---------------- |
|
||||||
|
| `next` | Next.js | Yes |
|
||||||
|
| `vite` | Vite | Yes |
|
||||||
|
| `start` | TanStack Start | Yes |
|
||||||
|
| `react-router` | React Router | Yes |
|
||||||
|
| `astro` | Astro | Yes |
|
||||||
|
| `laravel` | Laravel | No |
|
||||||
|
|
||||||
|
All templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Presets
|
||||||
|
|
||||||
|
Three ways to specify a preset via `--preset`:
|
||||||
|
|
||||||
|
1. **Named:** `--preset nova` or `--preset lyra`
|
||||||
|
2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`)
|
||||||
|
3. **URL:** `--preset "https://ui.shadcn.com/init?base=radix&style=nova&..."`
|
||||||
|
|
||||||
|
> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.
|
||||||
|
> Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset.
|
||||||
|
|
||||||
|
## Switching Presets
|
||||||
|
|
||||||
|
Ask the user first: **overwrite**, **merge**, or **skip** existing components?
|
||||||
|
|
||||||
|
- **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components.
|
||||||
|
- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./official-shadcn-ui-workflow.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.
|
||||||
|
- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.
|
||||||
|
|
||||||
|
Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
# Customization & Theming
|
||||||
|
|
||||||
|
Components reference semantic CSS variable tokens. Change the variables to change every component.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- How it works (CSS variables → Tailwind utilities → components)
|
||||||
|
- Color variables and OKLCH format
|
||||||
|
- Dark mode setup
|
||||||
|
- Changing the theme (presets, CSS variables)
|
||||||
|
- Adding custom colors (Tailwind v3 and v4)
|
||||||
|
- Border radius
|
||||||
|
- Customizing components (variants, className, wrappers)
|
||||||
|
- Checking for updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. CSS variables defined in `:root` (light) and `.dark` (dark mode).
|
||||||
|
2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.
|
||||||
|
3. Components use these utilities — changing a variable changes all components that reference it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Color Variables
|
||||||
|
|
||||||
|
Every color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
| -------------------------------------------- | -------------------------------- |
|
||||||
|
| `--background` / `--foreground` | Page background and default text |
|
||||||
|
| `--card` / `--card-foreground` | Card surfaces |
|
||||||
|
| `--primary` / `--primary-foreground` | Primary buttons and actions |
|
||||||
|
| `--secondary` / `--secondary-foreground` | Secondary actions |
|
||||||
|
| `--muted` / `--muted-foreground` | Muted/disabled states |
|
||||||
|
| `--accent` / `--accent-foreground` | Hover and accent states |
|
||||||
|
| `--destructive` / `--destructive-foreground` | Error and destructive actions |
|
||||||
|
| `--border` | Default border color |
|
||||||
|
| `--input` | Form input borders |
|
||||||
|
| `--ring` | Focus ring color |
|
||||||
|
| `--chart-1` through `--chart-5` | Chart/data visualization |
|
||||||
|
| `--sidebar-*` | Sidebar-specific colors |
|
||||||
|
| `--surface` / `--surface-foreground` | Secondary surface |
|
||||||
|
|
||||||
|
Colors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dark Mode
|
||||||
|
|
||||||
|
Class-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ThemeProvider } from "next-themes"
|
||||||
|
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changing the Theme
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply a preset code from ui.shadcn.com.
|
||||||
|
npx shadcn@latest apply --preset a2r6bw
|
||||||
|
|
||||||
|
# Positional shorthand also works.
|
||||||
|
npx shadcn@latest apply a2r6bw
|
||||||
|
|
||||||
|
# Switch to a named preset and overwrite existing components.
|
||||||
|
npx shadcn@latest apply --preset nova
|
||||||
|
|
||||||
|
# Preserve existing components instead.
|
||||||
|
npx shadcn@latest init --preset nova --force --no-reinstall
|
||||||
|
|
||||||
|
# Use a custom theme URL.
|
||||||
|
npx shadcn@latest apply --preset "https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Or edit CSS variables directly in `globals.css`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding Custom Colors
|
||||||
|
|
||||||
|
Add variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 1. Define in the global CSS file. */
|
||||||
|
:root {
|
||||||
|
--warning: oklch(0.84 0.16 84);
|
||||||
|
--warning-foreground: oklch(0.28 0.07 46);
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--warning: oklch(0.41 0.11 46);
|
||||||
|
--warning-foreground: oklch(0.99 0.02 95);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 2a. Register with Tailwind v4 (@theme inline). */
|
||||||
|
@theme inline {
|
||||||
|
--color-warning: var(--warning);
|
||||||
|
--color-warning-foreground: var(--warning-foreground);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `tailwindVersion` is `"v3"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 2b. Register with Tailwind v3 (tailwind.config.js).
|
||||||
|
module.exports = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
warning: "oklch(var(--warning) / <alpha-value>)",
|
||||||
|
"warning-foreground":
|
||||||
|
"oklch(var(--warning-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 3. Use in components.
|
||||||
|
<div className="bg-warning text-warning-foreground">Warning</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Border Radius
|
||||||
|
|
||||||
|
`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customizing Components
|
||||||
|
|
||||||
|
See also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.
|
||||||
|
|
||||||
|
Prefer these approaches in order:
|
||||||
|
|
||||||
|
### 1. Built-in variants
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Click
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Tailwind classes via `className`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card className="mx-auto max-w-md">...</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add a new variant
|
||||||
|
|
||||||
|
Edit the component source to add a variant via `cva`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// components/ui/button.tsx
|
||||||
|
warning: "bg-warning text-warning-foreground hover:bg-warning/90",
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Wrapper components
|
||||||
|
|
||||||
|
Compose shadcn/ui primitives into higher-level components:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function ConfirmDialog({ title, description, onConfirm, children }) {
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checking for Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add button --diff
|
||||||
|
```
|
||||||
|
|
||||||
|
To preview exactly what would change before updating, use `--dry-run` and `--diff`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add button --dry-run # see all affected files
|
||||||
|
npx shadcn@latest add button --diff button.tsx # see the diff for a specific file
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Updating Components in official-shadcn-ui-workflow.md](./official-shadcn-ui-workflow.md#updating-components) for the full smart merge workflow.
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
# shadcn MCP Server
|
||||||
|
|
||||||
|
The CLI includes an MCP server that lets AI assistants search, browse, view, and install components from registries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shadcn mcp # start the MCP server (stdio)
|
||||||
|
shadcn mcp init # write config for your editor
|
||||||
|
```
|
||||||
|
|
||||||
|
Editor config files:
|
||||||
|
|
||||||
|
| Editor | Config file |
|
||||||
|
|--------|------------|
|
||||||
|
| Claude Code | `.mcp.json` |
|
||||||
|
| Cursor | `.cursor/mcp.json` |
|
||||||
|
| VS Code | `.vscode/mcp.json` |
|
||||||
|
| OpenCode | `opencode.json` |
|
||||||
|
| Codex | `~/.codex/config.toml` (manual) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
> **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent.
|
||||||
|
|
||||||
|
### `shadcn:get_project_registries`
|
||||||
|
|
||||||
|
Returns registry names from `components.json`. Errors if no `components.json` exists.
|
||||||
|
|
||||||
|
**Input:** none
|
||||||
|
|
||||||
|
### `shadcn:list_items_in_registries`
|
||||||
|
|
||||||
|
Lists all items from one or more registries.
|
||||||
|
|
||||||
|
**Input:** `registries` (string[]), `limit` (number, optional), `offset` (number, optional)
|
||||||
|
|
||||||
|
### `shadcn:search_items_in_registries`
|
||||||
|
|
||||||
|
Fuzzy search across registries.
|
||||||
|
|
||||||
|
**Input:** `registries` (string[]), `query` (string), `limit` (number, optional), `offset` (number, optional)
|
||||||
|
|
||||||
|
### `shadcn:view_items_in_registries`
|
||||||
|
|
||||||
|
View item details including full file contents.
|
||||||
|
|
||||||
|
**Input:** `items` (string[]) — e.g. `["@shadcn/button", "@shadcn/card"]`
|
||||||
|
|
||||||
|
### `shadcn:get_item_examples_from_registries`
|
||||||
|
|
||||||
|
Find usage examples and demos with source code.
|
||||||
|
|
||||||
|
**Input:** `registries` (string[]), `query` (string) — e.g. `"accordion-demo"`, `"button example"`
|
||||||
|
|
||||||
|
### `shadcn:get_add_command_for_items`
|
||||||
|
|
||||||
|
Returns the CLI install command.
|
||||||
|
|
||||||
|
**Input:** `items` (string[]) — e.g. `["@shadcn/button"]`
|
||||||
|
|
||||||
|
### `shadcn:get_audit_checklist`
|
||||||
|
|
||||||
|
Returns a checklist for verifying components (imports, deps, lint, TypeScript).
|
||||||
|
|
||||||
|
**Input:** none
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuring Registries
|
||||||
|
|
||||||
|
Registries are set in `components.json`. The `@shadcn` registry is always built-in.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registries": {
|
||||||
|
"@acme": "https://acme.com/r/{name}.json",
|
||||||
|
"@private": {
|
||||||
|
"url": "https://private.com/r/{name}.json",
|
||||||
|
"headers": { "Authorization": "Bearer ${MY_TOKEN}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Names must start with `@`.
|
||||||
|
- URLs must contain `{name}`.
|
||||||
|
- `${VAR}` references are resolved from environment variables.
|
||||||
|
|
||||||
|
Community registry index: `https://ui.shadcn.com/r/registries.json`
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# Official shadcn/ui Workflow Reference
|
||||||
|
|
||||||
|
Vendored from the upstream shadcn/ui skill. The original skill frontmatter is intentionally removed so this file stays a reference document instead of being discovered as a separate local skill.
|
||||||
|
|
||||||
|
# shadcn/ui
|
||||||
|
|
||||||
|
A framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI.
|
||||||
|
|
||||||
|
> **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest` — based on the project's `packageManager`. Examples below use `npx shadcn@latest` but substitute the correct runner for the project.
|
||||||
|
|
||||||
|
## Current Project Context
|
||||||
|
|
||||||
|
```json
|
||||||
|
!`npx shadcn@latest info --json`
|
||||||
|
```
|
||||||
|
|
||||||
|
The JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too.
|
||||||
|
2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table.
|
||||||
|
3. **Use built-in variants before custom styles.** `variant="outline"`, `size="sm"`, etc.
|
||||||
|
4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
These rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs.
|
||||||
|
|
||||||
|
### Styling & Tailwind → [styling.md](./rules/styling.md)
|
||||||
|
|
||||||
|
- **`className` for layout, not styling.** Never override component colors or typography.
|
||||||
|
- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`.
|
||||||
|
- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`.
|
||||||
|
- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`.
|
||||||
|
- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`).
|
||||||
|
- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries.
|
||||||
|
- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking.
|
||||||
|
|
||||||
|
### Forms & Inputs → [forms.md](./rules/forms.md)
|
||||||
|
|
||||||
|
- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout.
|
||||||
|
- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`.
|
||||||
|
- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.**
|
||||||
|
- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state.
|
||||||
|
- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading.
|
||||||
|
- **Field validation uses `data-invalid` + `aria-invalid`.** `data-invalid` on `Field`, `aria-invalid` on the control. For disabled: `data-disabled` on `Field`, `disabled` on the control.
|
||||||
|
|
||||||
|
### Component Structure → [composition.md](./rules/composition.md)
|
||||||
|
|
||||||
|
- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`.
|
||||||
|
- **Use `asChild` (radix) or `render` (base) for custom triggers.** Check `base` field from `npx shadcn@latest info`. → [base-vs-radix.md](./rules/base-vs-radix.md)
|
||||||
|
- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className="sr-only"` if visually hidden.
|
||||||
|
- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`.
|
||||||
|
- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`.
|
||||||
|
- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`.
|
||||||
|
- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load.
|
||||||
|
|
||||||
|
### Use Components, Not Custom Markup → [composition.md](./rules/composition.md)
|
||||||
|
|
||||||
|
- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.
|
||||||
|
- **Callouts use `Alert`.** Don't build custom styled divs.
|
||||||
|
- **Empty states use `Empty`.** Don't build custom empty state markup.
|
||||||
|
- **Toast via `sonner`.** Use `toast()` from `sonner`.
|
||||||
|
- **Use `Separator`** instead of `<hr>` or `<div className="border-t">`.
|
||||||
|
- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.
|
||||||
|
- **Use `Badge`** instead of custom styled spans.
|
||||||
|
|
||||||
|
### Icons → [icons.md](./rules/icons.md)
|
||||||
|
|
||||||
|
- **Icons in `Button` use `data-icon`.** `data-icon="inline-start"` or `data-icon="inline-end"` on the icon.
|
||||||
|
- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.
|
||||||
|
- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`.
|
||||||
|
- **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply <code>` for existing projects, or `npx shadcn@latest init --preset <code>` when initializing.
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
These are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Form layout: FieldGroup + Field, not div + Label.
|
||||||
|
<FieldGroup>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||||
|
<Input id="email" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
// Validation: data-invalid on Field, aria-invalid on the control.
|
||||||
|
<Field data-invalid>
|
||||||
|
<FieldLabel>Email</FieldLabel>
|
||||||
|
<Input aria-invalid />
|
||||||
|
<FieldDescription>Invalid email.</FieldDescription>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
// Icons in buttons: data-icon, no sizing classes.
|
||||||
|
<Button>
|
||||||
|
<SearchIcon data-icon="inline-start" />
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
// Spacing: gap-*, not space-y-*.
|
||||||
|
<div className="flex flex-col gap-4"> // correct
|
||||||
|
<div className="space-y-4"> // wrong
|
||||||
|
|
||||||
|
// Equal dimensions: size-*, not w-* h-*.
|
||||||
|
<Avatar className="size-10"> // correct
|
||||||
|
<Avatar className="w-10 h-10"> // wrong
|
||||||
|
|
||||||
|
// Status colors: Badge variants or semantic tokens, not raw colors.
|
||||||
|
<Badge variant="secondary">+20.1%</Badge> // correct
|
||||||
|
<span className="text-emerald-600">+20.1%</span> // wrong
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Selection
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
| -------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
|
| Button/action | `Button` with appropriate variant |
|
||||||
|
| Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` |
|
||||||
|
| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem` |
|
||||||
|
| Data display | `Table`, `Card`, `Badge`, `Avatar` |
|
||||||
|
| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` |
|
||||||
|
| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) |
|
||||||
|
| Feedback | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` |
|
||||||
|
| Command palette | `Command` inside `Dialog` |
|
||||||
|
| Charts | `Chart` (wraps Recharts) |
|
||||||
|
| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` |
|
||||||
|
| Empty states | `Empty` |
|
||||||
|
| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` |
|
||||||
|
| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` |
|
||||||
|
|
||||||
|
## Key Fields
|
||||||
|
|
||||||
|
The injected project context contains these key fields:
|
||||||
|
|
||||||
|
- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode.
|
||||||
|
- **`isRSC`** → when `true`, components using `useState`, `useEffect`, event handlers, or browser APIs need `"use client"` at the top of the file. Always reference this field when advising on the directive.
|
||||||
|
- **`tailwindVersion`** → `"v4"` uses `@theme inline` blocks; `"v3"` uses `tailwind.config.js`.
|
||||||
|
- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one.
|
||||||
|
- **`style`** → component visual treatment (e.g. `nova`, `vega`).
|
||||||
|
- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props.
|
||||||
|
- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`.
|
||||||
|
- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc.
|
||||||
|
- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA).
|
||||||
|
- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`).
|
||||||
|
- **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information.
|
||||||
|
|
||||||
|
See [cli.md — `info` command](./cli.md) for the full field reference.
|
||||||
|
|
||||||
|
## Component Docs, Examples, and Usage
|
||||||
|
|
||||||
|
Run `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest docs button dialog select
|
||||||
|
```
|
||||||
|
|
||||||
|
**When creating, fixing, debugging, or using a component, always run `npx shadcn@latest docs` and fetch the URLs first.** This ensures you're working with the correct API and usage patterns rather than guessing.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh.
|
||||||
|
2. **Check installed components first** — before running `add`, always check the `components` list from project context or list the `resolvedPaths.ui` directory. Don't import components that haven't been added, and don't re-add ones already installed.
|
||||||
|
3. **Find components** — `npx shadcn@latest search`.
|
||||||
|
4. **Get docs and examples** — run `npx shadcn@latest docs <component>` to get URLs, then fetch them. Use `npx shadcn@latest view` to browse registry items you haven't installed. To preview changes to installed components, use `npx shadcn@latest add --diff`.
|
||||||
|
5. **Install or update** — `npx shadcn@latest add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below).
|
||||||
|
6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project.
|
||||||
|
7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on.
|
||||||
|
8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user.
|
||||||
|
9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?
|
||||||
|
- **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values.
|
||||||
|
- **Inspect incoming preset**: `npx shadcn@latest preset decode <code>`. Use `preset url <code>` or `preset open <code>` to share or open the preset builder.
|
||||||
|
- **Overwrite**: `npx shadcn@latest apply <code>`. Overwrites detected components, fonts, and CSS variables.
|
||||||
|
- **Partial**: `npx shadcn@latest apply <code> --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms.
|
||||||
|
- **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually.
|
||||||
|
- **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
|
||||||
|
- **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
|
||||||
|
|
||||||
|
## Updating Components
|
||||||
|
|
||||||
|
When the user asks to update a component from upstream while keeping their local changes, use `--dry-run` and `--diff` to intelligently merge. **NEVER fetch raw files from GitHub manually — always use the CLI.**
|
||||||
|
|
||||||
|
1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected.
|
||||||
|
2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local.
|
||||||
|
3. Decide per file based on the diff:
|
||||||
|
- No local changes → safe to overwrite.
|
||||||
|
- Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications.
|
||||||
|
- User says "just update everything" → use `--overwrite`, but confirm first.
|
||||||
|
4. **Never use `--overwrite` without the user's explicit approval.**
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new project.
|
||||||
|
npx shadcn@latest init --name my-app --preset base-nova
|
||||||
|
npx shadcn@latest init --name my-app --preset a2r6bw --template vite
|
||||||
|
|
||||||
|
# Create a monorepo project.
|
||||||
|
npx shadcn@latest init --name my-app --preset base-nova --monorepo
|
||||||
|
npx shadcn@latest init --name my-app --preset base-nova --template next --monorepo
|
||||||
|
|
||||||
|
# Initialize existing project.
|
||||||
|
npx shadcn@latest init --preset base-nova
|
||||||
|
npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (base style implied)
|
||||||
|
|
||||||
|
# Apply a preset to an existing project.
|
||||||
|
npx shadcn@latest apply a2r6bw
|
||||||
|
npx shadcn@latest apply a2r6bw --only theme
|
||||||
|
npx shadcn@latest apply a2r6bw --only font
|
||||||
|
npx shadcn@latest apply a2r6bw --only theme,font
|
||||||
|
|
||||||
|
# Inspect preset codes and project preset state.
|
||||||
|
npx shadcn@latest preset decode a2r6bw
|
||||||
|
npx shadcn@latest preset url a2r6bw
|
||||||
|
npx shadcn@latest preset open a2r6bw
|
||||||
|
npx shadcn@latest preset resolve
|
||||||
|
npx shadcn@latest preset resolve --json
|
||||||
|
|
||||||
|
# Add components.
|
||||||
|
npx shadcn@latest add button card dialog
|
||||||
|
npx shadcn@latest add @magicui/shimmer-button
|
||||||
|
npx shadcn@latest add --all
|
||||||
|
|
||||||
|
# Preview changes before adding/updating.
|
||||||
|
npx shadcn@latest add button --dry-run
|
||||||
|
npx shadcn@latest add button --diff button.tsx
|
||||||
|
npx shadcn@latest add @acme/form --view button.tsx
|
||||||
|
|
||||||
|
# Search registries.
|
||||||
|
npx shadcn@latest search @shadcn -q "sidebar"
|
||||||
|
npx shadcn@latest search @tailark -q "stats"
|
||||||
|
|
||||||
|
# Get component docs and example URLs.
|
||||||
|
npx shadcn@latest docs button dialog select
|
||||||
|
|
||||||
|
# View registry item details (for items not yet installed).
|
||||||
|
npx shadcn@latest view @shadcn/button
|
||||||
|
```
|
||||||
|
|
||||||
|
**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma`
|
||||||
|
**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo)
|
||||||
|
**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com).
|
||||||
|
|
||||||
|
## Detailed References
|
||||||
|
|
||||||
|
- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states
|
||||||
|
- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading
|
||||||
|
- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects
|
||||||
|
- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index
|
||||||
|
- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion
|
||||||
|
- [cli.md](./cli.md) — Commands, flags, presets, templates
|
||||||
|
- [customization.md](./customization.md) — Theming, CSS variables, extending components
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
# Base vs Radix
|
||||||
|
|
||||||
|
API differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- Composition: asChild vs render
|
||||||
|
- Button / trigger as non-button element
|
||||||
|
- Select (items prop, placeholder, positioning, multiple, object values)
|
||||||
|
- ToggleGroup (type vs multiple)
|
||||||
|
- Slider (scalar vs array)
|
||||||
|
- Accordion (type and defaultValue)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Composition: asChild (radix) vs render (base)
|
||||||
|
|
||||||
|
Radix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DialogTrigger>
|
||||||
|
<div>
|
||||||
|
<Button>Open</Button>
|
||||||
|
</div>
|
||||||
|
</DialogTrigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (radix):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>Open</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DialogTrigger render={<Button />}>Open</DialogTrigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Button / trigger as non-button element (base only)
|
||||||
|
|
||||||
|
When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.
|
||||||
|
|
||||||
|
**Incorrect (base):** missing `nativeButton={false}`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button render={<a href="/docs" />}>Read the docs</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button render={<a href="/docs" />} nativeButton={false}>
|
||||||
|
Read the docs
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (radix):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button asChild>
|
||||||
|
<a href="/docs">Read the docs</a>
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Same for triggers whose `render` is not a `Button`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// base.
|
||||||
|
<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
|
||||||
|
Pick date
|
||||||
|
</PopoverTrigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Select
|
||||||
|
|
||||||
|
**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.
|
||||||
|
|
||||||
|
**Incorrect (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger>
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const items = [
|
||||||
|
{ label: "Select a fruit", value: null },
|
||||||
|
{ label: "Apple", value: "apple" },
|
||||||
|
{ label: "Banana", value: "banana" },
|
||||||
|
]
|
||||||
|
|
||||||
|
<Select items={items}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{items.map((item) => (
|
||||||
|
<SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (radix):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a fruit" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="apple">Apple</SelectItem>
|
||||||
|
<SelectItem value="banana">Banana</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.
|
||||||
|
|
||||||
|
**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// base.
|
||||||
|
<SelectContent alignItemWithTrigger={false} side="bottom">
|
||||||
|
|
||||||
|
// radix.
|
||||||
|
<SelectContent position="popper">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Select — multiple selection and object values (base only)
|
||||||
|
|
||||||
|
Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.
|
||||||
|
|
||||||
|
**Correct (base — multiple selection):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Select items={items} multiple defaultValue={[]}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue>
|
||||||
|
{(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
...
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base — object values):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue>{(value) => value.name}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
...
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ToggleGroup
|
||||||
|
|
||||||
|
Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.
|
||||||
|
|
||||||
|
**Incorrect (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ToggleGroup type="single" defaultValue="daily">
|
||||||
|
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Single (no prop needed), defaultValue is always an array.
|
||||||
|
<ToggleGroup defaultValue={["daily"]} spacing={2}>
|
||||||
|
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
|
||||||
|
// Multi-selection.
|
||||||
|
<ToggleGroup multiple>
|
||||||
|
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (radix):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Single, defaultValue is a string.
|
||||||
|
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
|
||||||
|
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
|
||||||
|
// Multi-selection.
|
||||||
|
<ToggleGroup type="multiple">
|
||||||
|
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controlled single value:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// base — wrap/unwrap arrays.
|
||||||
|
const [value, setValue] = React.useState("normal")
|
||||||
|
<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>
|
||||||
|
|
||||||
|
// radix — plain string.
|
||||||
|
const [value, setValue] = React.useState("normal")
|
||||||
|
<ToggleGroup type="single" value={value} onValueChange={setValue}>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slider
|
||||||
|
|
||||||
|
Base accepts a plain number for a single thumb. Radix always requires an array.
|
||||||
|
|
||||||
|
**Incorrect (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Slider defaultValue={[50]} max={100} step={1} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Slider defaultValue={50} max={100} step={1} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (radix):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Slider defaultValue={[50]} max={100} step={1} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// base.
|
||||||
|
const [value, setValue] = React.useState([0.3, 0.7])
|
||||||
|
<Slider value={value} onValueChange={(v) => setValue(v as number[])} />
|
||||||
|
|
||||||
|
// radix.
|
||||||
|
const [value, setValue] = React.useState([0.3, 0.7])
|
||||||
|
<Slider value={value} onValueChange={setValue} />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accordion
|
||||||
|
|
||||||
|
Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.
|
||||||
|
|
||||||
|
**Incorrect (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Accordion type="single" collapsible defaultValue="item-1">
|
||||||
|
<AccordionItem value="item-1">...</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (base):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Accordion defaultValue={["item-1"]}>
|
||||||
|
<AccordionItem value="item-1">...</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
// Multi-select.
|
||||||
|
<Accordion multiple defaultValue={["item-1", "item-2"]}>
|
||||||
|
<AccordionItem value="item-1">...</AccordionItem>
|
||||||
|
<AccordionItem value="item-2">...</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (radix):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Accordion type="single" collapsible defaultValue="item-1">
|
||||||
|
<AccordionItem value="item-1">...</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
```
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# Component Composition
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- Items always inside their Group component
|
||||||
|
- Callouts use Alert
|
||||||
|
- Empty states use Empty component
|
||||||
|
- Toast notifications use sonner
|
||||||
|
- Choosing between overlay components
|
||||||
|
- Dialog, Sheet, and Drawer always need a Title
|
||||||
|
- Card structure
|
||||||
|
- Button has no isPending or isLoading prop
|
||||||
|
- TabsTrigger must be inside TabsList
|
||||||
|
- Avatar always needs AvatarFallback
|
||||||
|
- Use Separator instead of raw hr or border divs
|
||||||
|
- Use Skeleton for loading placeholders
|
||||||
|
- Use Badge instead of custom styled spans
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Items always inside their Group component
|
||||||
|
|
||||||
|
Never render items directly inside the content container.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="apple">Apple</SelectItem>
|
||||||
|
<SelectItem value="banana">Banana</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="apple">Apple</SelectItem>
|
||||||
|
<SelectItem value="banana">Banana</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies to all group-based components:
|
||||||
|
|
||||||
|
| Item | Group |
|
||||||
|
|------|-------|
|
||||||
|
| `SelectItem`, `SelectLabel` | `SelectGroup` |
|
||||||
|
| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |
|
||||||
|
| `MenubarItem` | `MenubarGroup` |
|
||||||
|
| `ContextMenuItem` | `ContextMenuGroup` |
|
||||||
|
| `CommandItem` | `CommandGroup` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Callouts use Alert
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert>
|
||||||
|
<AlertTitle>Warning</AlertTitle>
|
||||||
|
<AlertDescription>Something needs attention.</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Empty states use Empty component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Empty>
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
|
||||||
|
<EmptyTitle>No projects yet</EmptyTitle>
|
||||||
|
<EmptyDescription>Get started by creating a new project.</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
<EmptyContent>
|
||||||
|
<Button>Create Project</Button>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Toast notifications use sonner
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
toast.success("Changes saved.")
|
||||||
|
toast.error("Something went wrong.")
|
||||||
|
toast("File deleted.", {
|
||||||
|
action: { label: "Undo", onClick: () => undoDelete() },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Choosing between overlay components
|
||||||
|
|
||||||
|
| Use case | Component |
|
||||||
|
|----------|-----------|
|
||||||
|
| Focused task that requires input | `Dialog` |
|
||||||
|
| Destructive action confirmation | `AlertDialog` |
|
||||||
|
| Side panel with details or filters | `Sheet` |
|
||||||
|
| Mobile-first bottom panel | `Drawer` |
|
||||||
|
| Quick info on hover | `HoverCard` |
|
||||||
|
| Small contextual content on click | `Popover` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dialog, Sheet, and Drawer always need a Title
|
||||||
|
|
||||||
|
`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Profile</DialogTitle>
|
||||||
|
<DialogDescription>Update your profile.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
...
|
||||||
|
</DialogContent>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Card structure
|
||||||
|
|
||||||
|
Use full composition — don't dump everything into `CardContent`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Team Members</CardTitle>
|
||||||
|
<CardDescription>Manage your team.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>...</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button>Invite</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Button has no isPending or isLoading prop
|
||||||
|
|
||||||
|
Compose with `Spinner` + `data-icon` + `disabled`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button disabled>
|
||||||
|
<Spinner data-icon="inline-start" />
|
||||||
|
Saving...
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TabsTrigger must be inside TabsList
|
||||||
|
|
||||||
|
Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Tabs defaultValue="account">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="account">Account</TabsTrigger>
|
||||||
|
<TabsTrigger value="password">Password</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="account">...</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Avatar always needs AvatarFallback
|
||||||
|
|
||||||
|
Always include `AvatarFallback` for when the image fails to load:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src="/avatar.png" alt="User" />
|
||||||
|
<AvatarFallback>JD</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use existing components instead of custom markup
|
||||||
|
|
||||||
|
| Instead of | Use |
|
||||||
|
|---|---|
|
||||||
|
| `<hr>` or `<div className="border-t">` | `<Separator />` |
|
||||||
|
| `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |
|
||||||
|
| `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Forms & Inputs
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- Forms use FieldGroup + Field
|
||||||
|
- InputGroup requires InputGroupInput/InputGroupTextarea
|
||||||
|
- Buttons inside inputs use InputGroup + InputGroupAddon
|
||||||
|
- Option sets (2–7 choices) use ToggleGroup
|
||||||
|
- FieldSet + FieldLegend for grouping related fields
|
||||||
|
- Field validation and disabled states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Forms use FieldGroup + Field
|
||||||
|
|
||||||
|
Always use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<FieldGroup>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||||
|
<Input id="email" type="email" />
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||||
|
<Input id="password" type="password" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `Field orientation="horizontal"` for settings pages. Use `FieldLabel className="sr-only"` for visually hidden labels.
|
||||||
|
|
||||||
|
**Choosing form controls:**
|
||||||
|
|
||||||
|
- Simple text input → `Input`
|
||||||
|
- Dropdown with predefined options → `Select`
|
||||||
|
- Searchable dropdown → `Combobox`
|
||||||
|
- Native HTML select (no JS) → `native-select`
|
||||||
|
- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)
|
||||||
|
- Single choice from few options → `RadioGroup`
|
||||||
|
- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`
|
||||||
|
- OTP/verification code → `InputOTP`
|
||||||
|
- Multi-line text → `Textarea`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## InputGroup requires InputGroupInput/InputGroupTextarea
|
||||||
|
|
||||||
|
Never use raw `Input` or `Textarea` inside an `InputGroup`.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<InputGroup>
|
||||||
|
<Input placeholder="Search..." />
|
||||||
|
</InputGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput placeholder="Search..." />
|
||||||
|
</InputGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Buttons inside inputs use InputGroup + InputGroupAddon
|
||||||
|
|
||||||
|
Never place a `Button` directly inside or adjacent to an `Input` with custom positioning.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="relative">
|
||||||
|
<Input placeholder="Search..." className="pr-10" />
|
||||||
|
<Button className="absolute right-0 top-0" size="icon">
|
||||||
|
<SearchIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput placeholder="Search..." />
|
||||||
|
<InputGroupAddon>
|
||||||
|
<Button size="icon">
|
||||||
|
<SearchIcon data-icon="inline-start" />
|
||||||
|
</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option sets (2–7 choices) use ToggleGroup
|
||||||
|
|
||||||
|
Don't manually loop `Button` components with active state.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [selected, setSelected] = useState("daily")
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{["daily", "weekly", "monthly"].map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option}
|
||||||
|
variant={selected === option ? "default" : "outline"}
|
||||||
|
onClick={() => setSelected(option)}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||||
|
|
||||||
|
<ToggleGroup spacing={2}>
|
||||||
|
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Combine with `Field` for labelled toggle groups:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Field orientation="horizontal">
|
||||||
|
<FieldTitle id="theme-label">Theme</FieldTitle>
|
||||||
|
<ToggleGroup aria-labelledby="theme-label" spacing={2}>
|
||||||
|
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</Field>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FieldSet + FieldLegend for grouping related fields
|
||||||
|
|
||||||
|
Use `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<FieldSet>
|
||||||
|
<FieldLegend variant="label">Preferences</FieldLegend>
|
||||||
|
<FieldDescription>Select all that apply.</FieldDescription>
|
||||||
|
<FieldGroup className="gap-3">
|
||||||
|
<Field orientation="horizontal">
|
||||||
|
<Checkbox id="dark" />
|
||||||
|
<FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Field validation and disabled states
|
||||||
|
|
||||||
|
Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Invalid.
|
||||||
|
<Field data-invalid>
|
||||||
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||||
|
<Input id="email" aria-invalid />
|
||||||
|
<FieldDescription>Invalid email address.</FieldDescription>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
// Disabled.
|
||||||
|
<Field data-disabled>
|
||||||
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||||
|
<Input id="email" disabled />
|
||||||
|
</Field>
|
||||||
|
```
|
||||||
|
|
||||||
|
Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Icons
|
||||||
|
|
||||||
|
**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Icons in Button use data-icon attribute
|
||||||
|
|
||||||
|
Add `data-icon="inline-start"` (prefix) or `data-icon="inline-end"` (suffix) to the icon. No sizing classes on the icon.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button>
|
||||||
|
<SearchIcon className="mr-2 size-4" />
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button>
|
||||||
|
<SearchIcon data-icon="inline-start"/>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button>
|
||||||
|
Next
|
||||||
|
<ArrowRightIcon data-icon="inline-end"/>
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No sizing classes on icons inside components
|
||||||
|
|
||||||
|
Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button>
|
||||||
|
<SearchIcon className="size-4" data-icon="inline-start" />
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<SettingsIcon className="mr-2 size-4" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button>
|
||||||
|
<SearchIcon data-icon="inline-start" />
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<SettingsIcon />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pass icons as component objects, not string keys
|
||||||
|
|
||||||
|
Use `icon={CheckIcon}`, not a string key to a lookup map.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const iconMap = {
|
||||||
|
check: CheckIcon,
|
||||||
|
alert: AlertIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ icon }: { icon: string }) {
|
||||||
|
const Icon = iconMap[icon]
|
||||||
|
return <Icon />
|
||||||
|
}
|
||||||
|
|
||||||
|
<StatusBadge icon="check" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {
|
||||||
|
return <Icon />
|
||||||
|
}
|
||||||
|
|
||||||
|
<StatusBadge icon={CheckIcon} />
|
||||||
|
```
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Styling & Customization
|
||||||
|
|
||||||
|
See [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- Semantic colors
|
||||||
|
- Built-in variants first
|
||||||
|
- className for layout only
|
||||||
|
- No space-x-* / space-y-*
|
||||||
|
- Prefer size-* over w-* h-* when equal
|
||||||
|
- Prefer truncate shorthand
|
||||||
|
- No manual dark: color overrides
|
||||||
|
- Use cn() for conditional classes
|
||||||
|
- No manual z-index on overlay components
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Semantic colors
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="bg-blue-500 text-white">
|
||||||
|
<p className="text-gray-600">Secondary text</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="bg-primary text-primary-foreground">
|
||||||
|
<p className="text-muted-foreground">Secondary text</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No raw color values for status/state indicators
|
||||||
|
|
||||||
|
For positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<span className="text-emerald-600">+20.1%</span>
|
||||||
|
<span className="text-green-500">Active</span>
|
||||||
|
<span className="text-red-600">-3.2%</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Badge variant="secondary">+20.1%</Badge>
|
||||||
|
<Badge>Active</Badge>
|
||||||
|
<span className="text-destructive">-3.2%</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in variants first
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button className="border border-input bg-transparent hover:bg-accent">
|
||||||
|
Click me
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button variant="outline">Click me</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## className for layout only
|
||||||
|
|
||||||
|
Use `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card className="bg-blue-100 text-blue-900 font-bold">
|
||||||
|
<CardContent>Dashboard</CardContent>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card className="max-w-md mx-auto">
|
||||||
|
<CardContent>Dashboard</CardContent>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
To customize a component's appearance, prefer these approaches in order:
|
||||||
|
1. **Built-in variants** — `variant="outline"`, `variant="destructive"`, etc.
|
||||||
|
2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.
|
||||||
|
3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No space-x-* / space-y-*
|
||||||
|
|
||||||
|
Use `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Input />
|
||||||
|
<Input />
|
||||||
|
<Button>Submit</Button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prefer size-* over w-* h-* when equal
|
||||||
|
|
||||||
|
`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prefer truncate shorthand
|
||||||
|
|
||||||
|
`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No manual dark: color overrides
|
||||||
|
|
||||||
|
Use semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use cn() for conditional classes
|
||||||
|
|
||||||
|
Use the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className={`flex items-center ${isActive ? "bg-primary text-primary-foreground" : "bg-muted"}`}>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
<div className={cn("flex items-center", isActive ? "bg-primary text-primary-foreground" : "bg-muted")}>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No manual z-index on overlay components
|
||||||
|
|
||||||
|
`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: vercel-react-best-practices
|
||||||
|
description: React and Next.js performance optimization guidelines from Vercel Engineering. Use when writing, reviewing, or refactoring React/Next.js code involving components, Next.js pages, Server Components, Server Actions, data fetching, bundle size, rendering behavior, or performance improvements.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vercel React Best Practices
|
||||||
|
|
||||||
|
Use this skill for React and Next.js performance work. The full Vercel guide is stored in `references/full-guide.md`; do not read the whole file by default.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Identify the relevant performance area from the task or code under review.
|
||||||
|
2. Search `references/full-guide.md` for the matching section or rule heading.
|
||||||
|
3. Read only the relevant section before changing or reviewing code.
|
||||||
|
4. Prioritize higher-impact categories before lower-impact micro-optimizations.
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
1. Eliminating waterfalls: sequential async work, API route chains, missing `Promise.all`, Suspense boundaries.
|
||||||
|
2. Bundle size optimization: barrel imports, heavy client modules, dynamic imports, deferred third-party libraries.
|
||||||
|
3. Server-side performance: Server Actions auth, RSC serialization, per-request deduplication, cross-request caching, `after()`.
|
||||||
|
4. Client-side data fetching: SWR deduplication, global listeners, passive scroll listeners, localStorage schema.
|
||||||
|
5. Re-render optimization: derived state, effect dependencies, memo boundaries, functional state updates, transitions, refs.
|
||||||
|
6. Rendering performance: hydration mismatches, long lists, static JSX, SVG precision, resource hints, script loading.
|
||||||
|
7. JavaScript performance: repeated lookups, array passes, storage reads, layout thrashing, sort/min-max choices.
|
||||||
|
8. Advanced patterns: one-time initialization, stable callback refs, effect events.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- Full compiled guide: `references/full-guide.md`
|
||||||
|
- Original project: https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
|||||||
|
.github
|
||||||
|
.git
|
||||||
|
*.md
|
||||||
|
.vscode
|
||||||
|
.gitignore
|
||||||
|
Makefile
|
||||||
|
docs
|
||||||
|
.eslintcache
|
||||||
|
.gocache
|
||||||
|
/web/node_modules
|
||||||
|
/web/dist
|
||||||
|
!THIRD-PARTY-LICENSES.md
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
# 端口号
|
||||||
|
# PORT=3000
|
||||||
|
# 前端基础URL
|
||||||
|
# FRONTEND_BASE_URL=https://your-frontend-url.com
|
||||||
|
|
||||||
|
|
||||||
|
# 调试相关配置
|
||||||
|
# 启用pprof
|
||||||
|
# ENABLE_PPROF=true
|
||||||
|
# 启用调试模式
|
||||||
|
# DEBUG=true
|
||||||
|
# Pyroscope 配置
|
||||||
|
# PYROSCOPE_URL=http://localhost:4040
|
||||||
|
# PYROSCOPE_APP_NAME=new-api
|
||||||
|
# PYROSCOPE_BASIC_AUTH_USER=your-user
|
||||||
|
# PYROSCOPE_BASIC_AUTH_PASSWORD=your-password
|
||||||
|
# PYROSCOPE_MUTEX_RATE=5
|
||||||
|
# PYROSCOPE_BLOCK_RATE=5
|
||||||
|
# HOSTNAME=your-hostname
|
||||||
|
|
||||||
|
# 数据库相关配置
|
||||||
|
# 启用错误日志记录
|
||||||
|
# ERROR_LOG_ENABLED=true
|
||||||
|
# 数据库连接字符串
|
||||||
|
# SQL_DSN=user:password@tcp(127.0.0.1:3306)/dbname?parseTime=true
|
||||||
|
# 日志数据库连接字符串
|
||||||
|
# LOG_SQL_DSN=user:password@tcp(127.0.0.1:3306)/logdb?parseTime=true
|
||||||
|
# SQLite数据库路径
|
||||||
|
# SQLITE_PATH=/path/to/sqlite.db
|
||||||
|
# 数据库最大空闲连接数
|
||||||
|
# SQL_MAX_IDLE_CONNS=100
|
||||||
|
# 数据库最大打开连接数
|
||||||
|
# SQL_MAX_OPEN_CONNS=1000
|
||||||
|
# 数据库连接最大生命周期(秒)
|
||||||
|
# SQL_MAX_LIFETIME=60
|
||||||
|
# 慢查询日志阈值(毫秒),0 表示关闭慢查询日志,超出 0-3600000 范围回退默认值 200
|
||||||
|
# SQL_SLOW_THRESHOLD_MS=200
|
||||||
|
|
||||||
|
|
||||||
|
# 缓存相关配置
|
||||||
|
# Redis连接字符串
|
||||||
|
# REDIS_CONN_STRING=redis://user:password@localhost:6379/0
|
||||||
|
# 同步频率(单位:秒)
|
||||||
|
# SYNC_FREQUENCY=60
|
||||||
|
# 内存缓存启用
|
||||||
|
# MEMORY_CACHE_ENABLED=true
|
||||||
|
# 渠道更新频率(单位:秒)
|
||||||
|
# CHANNEL_UPDATE_FREQUENCY=30
|
||||||
|
# 批量更新启用
|
||||||
|
# BATCH_UPDATE_ENABLED=true
|
||||||
|
# 批量更新间隔(单位:秒)
|
||||||
|
# BATCH_UPDATE_INTERVAL=5
|
||||||
|
|
||||||
|
# 任务和功能配置
|
||||||
|
# 更新任务启用
|
||||||
|
# UPDATE_TASK=true
|
||||||
|
|
||||||
|
# 对话超时设置
|
||||||
|
# 所有请求超时时间,单位秒,默认为0,表示不限制
|
||||||
|
# RELAY_TIMEOUT=0
|
||||||
|
# Relay HTTP 客户端空闲连接超时时间,单位秒,默认跟随 Go 标准库,设置为0表示不限制
|
||||||
|
# RELAY_IDLE_CONN_TIMEOUT=90
|
||||||
|
# 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
|
||||||
|
# STREAMING_TIMEOUT=300
|
||||||
|
|
||||||
|
# TLS / HTTP 跳过验证设置
|
||||||
|
# TLS_INSECURE_SKIP_VERIFY=false
|
||||||
|
|
||||||
|
# Gin 可信反向代理(逗号分隔的 IP/CIDR)
|
||||||
|
# 未配置/留空:默认信任 127.0.0.0/8、::1、RFC1918 私网和 fc00::/7,并打印启动告警。
|
||||||
|
# none:严格模式,不信任任何代理且必须单独使用;显式列表完全替代默认值,应填写代理自身地址。
|
||||||
|
# TRUSTED_PROXIES=none
|
||||||
|
# TRUSTED_PROXIES=127.0.0.1,172.20.0.0/16
|
||||||
|
|
||||||
|
# Gemini 识别图片 最大图片数量
|
||||||
|
# GEMINI_VISION_MAX_IMAGE_NUM=16
|
||||||
|
|
||||||
|
# 会话密钥
|
||||||
|
# SESSION_SECRET=random_string
|
||||||
|
# false/未配置:本地 HTTP 模式,关闭 refresh/logout OriginGuard,且不得设置 TRUSTED_URL;兼容本地开发代理。
|
||||||
|
# true:启用 Secure Refresh Cookie 和严格 OriginGuard,必须同时列出全部可信 HTTPS Origin。
|
||||||
|
# SESSION_COOKIE_TRUSTED_URL 多项用英文逗号分隔;不支持通配符、路径或域名后缀匹配。
|
||||||
|
# 这些设置不修改 relay CORS。
|
||||||
|
# SESSION_COOKIE_SECURE=false
|
||||||
|
# SESSION_COOKIE_TRUSTED_URL=https://example.com,https://admin.example.com
|
||||||
|
# 每用户最多保留的活跃登录 Session
|
||||||
|
# USER_SESSION_ACTIVE_LIMIT=50
|
||||||
|
# 单用户在签发窗口内允许创建的 Session 总数(包含已撤销)
|
||||||
|
# USER_SESSION_ISSUANCE_LIMIT=100
|
||||||
|
# Session 签发计数窗口(秒);不得大于 revoked 保留期,超出时会自动钳制
|
||||||
|
# USER_SESSION_ISSUANCE_WINDOW_SECONDS=86400
|
||||||
|
# revoked Session 审计保留天数
|
||||||
|
# USER_SESSION_REVOKED_RETENTION_DAYS=7
|
||||||
|
# 最近一小时全局 Session 签发量超过此值时记录告警,不会拒绝登录
|
||||||
|
# USER_SESSION_HOURLY_ALERT_THRESHOLD=5000
|
||||||
|
|
||||||
|
# 其他配置
|
||||||
|
# 生成默认token
|
||||||
|
# GENERATE_DEFAULT_TOKEN=false
|
||||||
|
# Cohere 安全设置
|
||||||
|
# COHERE_SAFETY_SETTING=NONE
|
||||||
|
# 是否统计图片token
|
||||||
|
# GET_MEDIA_TOKEN=true
|
||||||
|
# 是否在非流(stream=false)情况下统计图片token
|
||||||
|
# GET_MEDIA_TOKEN_NOT_STREAM=false
|
||||||
|
# 设置 Dify 渠道是否输出工作流和节点信息到客户端
|
||||||
|
# DIFY_DEBUG=true
|
||||||
|
|
||||||
|
# LinuxDo相关配置
|
||||||
|
LINUX_DO_TOKEN_ENDPOINT=https://connect.linux.do/oauth2/token
|
||||||
|
LINUX_DO_USER_ENDPOINT=https://connect.linux.do/api/user
|
||||||
|
|
||||||
|
# 节点类型
|
||||||
|
# 如果是主节点则为master
|
||||||
|
# NODE_TYPE=master
|
||||||
|
|
||||||
|
# 可信任重定向域名列表(逗号分隔,支持子域名匹配)
|
||||||
|
# 用于验证支付成功/取消回调URL的域名安全性
|
||||||
|
# 示例: example.com,myapp.io 将允许 example.com, sub.example.com, myapp.io 等
|
||||||
|
# TRUSTED_REDIRECT_DOMAINS=example.com,myapp.io
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Go files
|
||||||
|
*.go text eol=lf
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
*.json text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
|
||||||
|
# JavaScript/TypeScript files
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
|
||||||
|
# Shell scripts
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# GitHub Linguist - Language Detection
|
||||||
|
# ============================================
|
||||||
|
# Keep frontend TypeScript logic visible while excluding the TSX view layer,
|
||||||
|
# whose markup-heavy files otherwise outweigh the primary Go service by bytes.
|
||||||
|
web/**/*.tsx -linguist-detectable
|
||||||
|
electron/** linguist-vendored
|
||||||
|
|
||||||
|
# TanStack Router regenerates this manifest from the route source files.
|
||||||
|
web/src/routeTree.gen.ts linguist-generated
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
WEB_DIR = ./web
|
||||||
|
API_DIR = .
|
||||||
|
DEV_WEB_PORT ?= 5173
|
||||||
|
DEV_COMPOSE_FILE = docker-compose.dev.yml
|
||||||
|
DEV_POSTGRES_SERVICE = postgres
|
||||||
|
DEV_API_SERVICE = new-api
|
||||||
|
DEV_POSTGRES_DB = new-api
|
||||||
|
DEV_POSTGRES_USER = root
|
||||||
|
DEV_SQLITE_PATH ?= one-api.db
|
||||||
|
|
||||||
|
.PHONY: all build-web build-all-web start-api dev dev-api dev-api-rebuild dev-web reset-setup test
|
||||||
|
|
||||||
|
all: build-all-web start-api
|
||||||
|
|
||||||
|
build-web:
|
||||||
|
@echo "Building web frontend..."
|
||||||
|
@cd $(WEB_DIR) && bun install --frozen-lockfile
|
||||||
|
@cd $(WEB_DIR) && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$$(cat ../VERSION) bun run build
|
||||||
|
|
||||||
|
build-all-web: build-web
|
||||||
|
|
||||||
|
start-api:
|
||||||
|
@echo "Starting api dev server..."
|
||||||
|
@cd $(API_DIR) && go run main.go &
|
||||||
|
|
||||||
|
dev-api:
|
||||||
|
@echo "Starting api services (docker)..."
|
||||||
|
@docker compose -f $(DEV_COMPOSE_FILE) up -d
|
||||||
|
|
||||||
|
dev-api-rebuild:
|
||||||
|
@echo "Rebuilding and starting api service (docker)..."
|
||||||
|
@docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_API_SERVICE)
|
||||||
|
|
||||||
|
dev-web:
|
||||||
|
@echo "Starting web frontend dev server..."
|
||||||
|
@echo "Web frontend: http://localhost:$(DEV_WEB_PORT)"
|
||||||
|
@cd $(WEB_DIR) && bun install
|
||||||
|
@cd $(WEB_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_WEB_PORT)
|
||||||
|
|
||||||
|
dev: dev-api dev-web
|
||||||
|
|
||||||
|
# The main package embeds the ignored web/dist output and is covered after build-web.
|
||||||
|
test:
|
||||||
|
@echo "Testing root Go module..."
|
||||||
|
@root_module=$$(GOWORK=off go list -m); \
|
||||||
|
root_packages=$$(GOWORK=off go list -e ./... | grep -vxF "$$root_module"); \
|
||||||
|
GOWORK=off go test $$root_packages
|
||||||
|
@echo "Testing relaykit Go module..."
|
||||||
|
@cd relaykit && GOWORK=off go test ./...
|
||||||
|
|
||||||
|
reset-setup:
|
||||||
|
@echo "Resetting local setup wizard state..."
|
||||||
|
@if docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \
|
||||||
|
echo "Detected running docker dev PostgreSQL. Removing setup record and root users..."; \
|
||||||
|
docker compose -f $(DEV_COMPOSE_FILE) exec -T $(DEV_POSTGRES_SERVICE) \
|
||||||
|
psql -U $(DEV_POSTGRES_USER) -d $(DEV_POSTGRES_DB) \
|
||||||
|
-c 'DELETE FROM setups;' \
|
||||||
|
-c 'DELETE FROM users WHERE role = 100;' \
|
||||||
|
-c "DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
|
||||||
|
echo "Restarting docker dev api so setup status is recalculated..."; \
|
||||||
|
docker compose -f $(DEV_COMPOSE_FILE) restart $(DEV_API_SERVICE); \
|
||||||
|
elif db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; db_path="$${db_path%%\?*}"; [ -f "$$db_path" ]; then \
|
||||||
|
db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; \
|
||||||
|
db_path="$${db_path%%\?*}"; \
|
||||||
|
echo "Detected local SQLite database: $$db_path"; \
|
||||||
|
sqlite3 "$$db_path" \
|
||||||
|
"DELETE FROM setups; DELETE FROM users WHERE role = 100; DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
|
||||||
|
echo "SQLite setup state reset. Restart the local api process before testing the setup wizard."; \
|
||||||
|
else \
|
||||||
|
echo "No running docker dev PostgreSQL or local SQLite database found."; \
|
||||||
|
echo "Start the dev stack with 'make dev-api', or set SQLITE_PATH/DEV_SQLITE_PATH to your local SQLite database."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user