tlc-claude-code 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/review.md +17 -4
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +12 -0
- package/bin/install.js +171 -2
- package/bin/postinstall.js +45 -26
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +3 -1
- package/server/index.js +228 -2
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +3 -1
- package/server/lib/memory-api.test.js +3 -5
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +69 -4
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +42 -4
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +37 -2
- package/server/lib/project-scanner.test.js +152 -0
- package/server/lib/remember-command.js +2 -0
- package/server/lib/remember-command.test.js +23 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +1 -1
- package/server/lib/semantic-recall.test.js +17 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +182 -1
- package/server/lib/workspace-api.test.js +474 -0
- package/server/package-lock.json +737 -0
- package/server/package.json +3 -0
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
- package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TLC Enforcement Layer 2: Smart intent detection + routing
|
|
3
|
+
# Fires on every UserPromptSubmit. Parses user message for intent and
|
|
4
|
+
# injects specific TLC command routing — not a generic reminder.
|
|
5
|
+
#
|
|
6
|
+
# Output appears as <user-prompt-submit-hook> and Claude treats it as user instruction.
|
|
7
|
+
# Survives context compaction because it is re-injected every turn.
|
|
8
|
+
|
|
9
|
+
# Only active in TLC projects
|
|
10
|
+
[ ! -f ".tlc.json" ] && exit 0
|
|
11
|
+
|
|
12
|
+
# Read user's message from stdin
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
MSG=$(echo "$INPUT" | jq -r '.user_prompt // empty' 2>/dev/null)
|
|
15
|
+
|
|
16
|
+
# If we can't parse the message, fall back to generic reminder
|
|
17
|
+
if [ -z "$MSG" ]; then
|
|
18
|
+
echo "[TLC PROJECT] All work uses /tlc commands. Never write code without tests. Run /tlc if unsure."
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Lowercase for matching
|
|
23
|
+
MSG_LOWER=$(echo "$MSG" | tr '[:upper:]' '[:lower:]')
|
|
24
|
+
|
|
25
|
+
# Detect intent and inject specific routing
|
|
26
|
+
# Priority order: most specific first
|
|
27
|
+
|
|
28
|
+
# Plan intent
|
|
29
|
+
if echo "$MSG_LOWER" | grep -qE '\b(plan|break.*(down|into)|design|architect|roadmap)\b'; then
|
|
30
|
+
if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
|
|
31
|
+
echo "[TLC-ENFORCE] Planning detected. You MUST use /tlc:plan for this. Plans go in .planning/phases/ files, not in chat. Invoke Skill(skill=\"tlc:plan\") now."
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Build/implement intent
|
|
37
|
+
if echo "$MSG_LOWER" | grep -qE '\b(build|implement|create|add|code|write|develop|make)\b.*(feature|function|module|component|endpoint|api|page|service|handler|route|model)'; then
|
|
38
|
+
if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
|
|
39
|
+
echo "[TLC-ENFORCE] Implementation detected. You MUST use /tlc:build for this. Tests before code — Red, Green, Refactor. Run /tlc:progress first, then invoke Skill(skill=\"tlc:build\"). Do NOT write code directly."
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Fix/bug intent
|
|
45
|
+
if echo "$MSG_LOWER" | grep -qE '\b(fix|bug|broken|not working|failing|crash|error)\b'; then
|
|
46
|
+
if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
|
|
47
|
+
echo "[TLC-ENFORCE] Bug/fix detected. Use /tlc:quick for small fixes (still test-first) or /tlc:autofix if tests are failing. Do NOT write code directly without tests."
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Refactor intent
|
|
53
|
+
if echo "$MSG_LOWER" | grep -qE '\b(refactor|clean.?up|restructure|reorganize|simplify)\b'; then
|
|
54
|
+
if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
|
|
55
|
+
echo "[TLC-ENFORCE] Refactoring detected. Use /tlc:refactor for step-by-step standards refactoring with tests. Invoke Skill(skill=\"tlc:refactor\")."
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Deploy intent
|
|
61
|
+
if echo "$MSG_LOWER" | grep -qE '\b(deploy|ship|release|publish|push to prod|staging)\b'; then
|
|
62
|
+
if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
|
|
63
|
+
echo "[TLC-ENFORCE] Deployment detected. Use /tlc:deploy for deployment. Secrets must come from HashiCorp Vault or environment — never hardcode. Invoke Skill(skill=\"tlc:deploy\")."
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Generic fallback for TLC projects
|
|
69
|
+
echo "[TLC PROJECT] All work uses /tlc commands. Never write code without tests. Run /tlc if unsure."
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TLC Enforcement Layer 2b: Session initialization
|
|
3
|
+
# 1. Inject TLC awareness
|
|
4
|
+
# 2. Ensure TLC server is running
|
|
5
|
+
# 3. Probe LLM providers and write persistent router state
|
|
6
|
+
|
|
7
|
+
if [ ! -f ".tlc.json" ]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
echo "TLC project detected. All work goes through /tlc commands. Run /tlc for current status and next action."
|
|
12
|
+
|
|
13
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
14
|
+
|
|
15
|
+
# ─── TLC Server ───────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
TLC_PORT="${TLC_PORT:-3147}"
|
|
18
|
+
if curl -sf --max-time 1 "http://localhost:${TLC_PORT}/api/health" > /dev/null 2>&1; then
|
|
19
|
+
: # Server is running
|
|
20
|
+
else
|
|
21
|
+
PLIST="$HOME/Library/LaunchAgents/com.tlc.server.plist"
|
|
22
|
+
|
|
23
|
+
if [ -f "$PLIST" ]; then
|
|
24
|
+
launchctl kickstart -k "gui/$(id -u)/com.tlc.server" 2>/dev/null
|
|
25
|
+
elif [ -f "$PROJECT_DIR/server/index.js" ]; then
|
|
26
|
+
nohup node "$PROJECT_DIR/server/index.js" > "$HOME/.tlc/logs/server.log" 2>&1 &
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
for i in 1 2 3; do
|
|
30
|
+
sleep 1
|
|
31
|
+
curl -sf --max-time 1 "http://localhost:${TLC_PORT}/api/health" > /dev/null 2>&1 && break
|
|
32
|
+
done
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# ─── LLM Router: Probe Providers ─────────────────────────
|
|
36
|
+
#
|
|
37
|
+
# Writes .tlc/.router-state.json with provider availability.
|
|
38
|
+
# Skills read this file instead of probing from scratch.
|
|
39
|
+
# State has a TTL — re-probed if older than 1 hour.
|
|
40
|
+
|
|
41
|
+
STATE_DIR="$PROJECT_DIR/.tlc"
|
|
42
|
+
STATE_FILE="$STATE_DIR/.router-state.json"
|
|
43
|
+
mkdir -p "$STATE_DIR"
|
|
44
|
+
|
|
45
|
+
# Check if state is fresh (less than 1 hour old)
|
|
46
|
+
STALE=true
|
|
47
|
+
if [ -f "$STATE_FILE" ]; then
|
|
48
|
+
STATE_AGE=$(( $(date +%s) - $(stat -f %m "$STATE_FILE" 2>/dev/null || stat -c %Y "$STATE_FILE" 2>/dev/null || echo 0) ))
|
|
49
|
+
if [ "$STATE_AGE" -lt 3600 ]; then
|
|
50
|
+
STALE=false
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ "$STALE" = true ]; then
|
|
55
|
+
# Probe each provider
|
|
56
|
+
CLAUDE_PATH=$(which claude 2>/dev/null || echo "")
|
|
57
|
+
CODEX_PATH=$(which codex 2>/dev/null || echo "")
|
|
58
|
+
GEMINI_PATH=$(which gemini 2>/dev/null || echo "")
|
|
59
|
+
|
|
60
|
+
CLAUDE_OK="false"
|
|
61
|
+
CODEX_OK="false"
|
|
62
|
+
GEMINI_OK="false"
|
|
63
|
+
|
|
64
|
+
[ -n "$CLAUDE_PATH" ] && CLAUDE_OK="true"
|
|
65
|
+
[ -n "$CODEX_PATH" ] && CODEX_OK="true"
|
|
66
|
+
[ -n "$GEMINI_PATH" ] && GEMINI_OK="true"
|
|
67
|
+
|
|
68
|
+
# Count available
|
|
69
|
+
AVAILABLE=0
|
|
70
|
+
[ "$CLAUDE_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
71
|
+
[ "$CODEX_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
72
|
+
[ "$GEMINI_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
73
|
+
|
|
74
|
+
# Read configured providers from .tlc.json
|
|
75
|
+
CONFIGURED_PROVIDERS=""
|
|
76
|
+
if command -v jq >/dev/null 2>&1; then
|
|
77
|
+
CONFIGURED_PROVIDERS=$(jq -r '.router.providers // {} | keys[]' .tlc.json 2>/dev/null | tr '\n' ',' | sed 's/,$//')
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Write state file
|
|
81
|
+
cat > "$STATE_FILE" <<STATEEOF
|
|
82
|
+
{
|
|
83
|
+
"probed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
84
|
+
"ttl_seconds": 3600,
|
|
85
|
+
"providers": {
|
|
86
|
+
"claude": {
|
|
87
|
+
"available": $CLAUDE_OK,
|
|
88
|
+
"path": "$CLAUDE_PATH"
|
|
89
|
+
},
|
|
90
|
+
"codex": {
|
|
91
|
+
"available": $CODEX_OK,
|
|
92
|
+
"path": "$CODEX_PATH"
|
|
93
|
+
},
|
|
94
|
+
"gemini": {
|
|
95
|
+
"available": $GEMINI_OK,
|
|
96
|
+
"path": "$GEMINI_PATH"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"summary": {
|
|
100
|
+
"available_count": $AVAILABLE,
|
|
101
|
+
"configured": "$CONFIGURED_PROVIDERS"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
STATEEOF
|
|
105
|
+
|
|
106
|
+
# Report to Claude
|
|
107
|
+
if [ "$AVAILABLE" -gt 0 ]; then
|
|
108
|
+
PROVIDERS_LIST=""
|
|
109
|
+
[ "$CLAUDE_OK" = "true" ] && PROVIDERS_LIST="${PROVIDERS_LIST}claude, "
|
|
110
|
+
[ "$CODEX_OK" = "true" ] && PROVIDERS_LIST="${PROVIDERS_LIST}codex, "
|
|
111
|
+
[ "$GEMINI_OK" = "true" ] && PROVIDERS_LIST="${PROVIDERS_LIST}gemini, "
|
|
112
|
+
PROVIDERS_LIST=$(echo "$PROVIDERS_LIST" | sed 's/, $//')
|
|
113
|
+
echo "LLM Router: ${AVAILABLE} providers available (${PROVIDERS_LIST}). State written to .tlc/.router-state.json. All routing skills MUST read this file for provider availability — do not probe manually."
|
|
114
|
+
else
|
|
115
|
+
echo "LLM Router: No external providers detected. Running Claude-only mode. Install codex or gemini for multi-LLM reviews."
|
|
116
|
+
fi
|
|
117
|
+
else
|
|
118
|
+
# State is fresh — just report it
|
|
119
|
+
if command -v jq >/dev/null 2>&1 && [ -f "$STATE_FILE" ]; then
|
|
120
|
+
COUNT=$(jq -r '.summary.available_count' "$STATE_FILE" 2>/dev/null)
|
|
121
|
+
echo "LLM Router: ${COUNT} providers available (cached). State at .tlc/.router-state.json."
|
|
122
|
+
fi
|
|
123
|
+
fi
|
package/CLAUDE.md
CHANGED
|
@@ -49,6 +49,10 @@ When the user says X → invoke `Skill(skill="tlc:...")`:
|
|
|
49
49
|
| "quick task", "small fix" | `/tlc:quick` |
|
|
50
50
|
| "dashboard" | `/tlc:dashboard` |
|
|
51
51
|
| "review PR" | `/tlc:review-pr` |
|
|
52
|
+
| "watch ci", "fix ci", "ci failing" | `/tlc:watchci` |
|
|
53
|
+
| "e2e", "screenshot", "visual check" | `/tlc:e2e-verify` |
|
|
54
|
+
| "guard", "check process", "validate" | `/tlc:guard` |
|
|
55
|
+
| "preflight", "am I done", "check gaps" | `/tlc:preflight` |
|
|
52
56
|
|
|
53
57
|
## TLC File System
|
|
54
58
|
|
|
@@ -73,6 +77,14 @@ Use `Task` tool to spawn sub-agents for independent work. Keep main conversation
|
|
|
73
77
|
|
|
74
78
|
Claim tasks before starting: `/tlc:claim`. Release if blocked: `/tlc:release`. Check team: `/tlc:who`. Pull before claiming, push after.
|
|
75
79
|
|
|
80
|
+
## Memory Auto-Capture
|
|
81
|
+
|
|
82
|
+
Conversations are automatically captured via the Claude Code `Stop` hook. After each response, the hook POSTs the exchange to the TLC server's capture endpoint. The pattern detector classifies decisions, gotchas, and preferences into team memory files under `.tlc/memory/team/`.
|
|
83
|
+
|
|
84
|
+
- **Resilience:** If the server is unreachable, exchanges spool to `.tlc/memory/.spool.jsonl` and drain on the next successful capture.
|
|
85
|
+
- **Endpoint hardening:** Payloads are capped at 100KB, deduplicated within a 60s window, and rate-limited to 100 captures/minute per project.
|
|
86
|
+
- **Disable:** Remove the `Stop` hook entry from `.claude/settings.json`.
|
|
87
|
+
|
|
76
88
|
---
|
|
77
89
|
|
|
78
90
|
<!-- TLC-STANDARDS -->
|
package/bin/install.js
CHANGED
|
@@ -33,6 +33,7 @@ const COMMANDS = [
|
|
|
33
33
|
'sync.md',
|
|
34
34
|
'new-project.md',
|
|
35
35
|
'init.md',
|
|
36
|
+
'bootstrap.md',
|
|
36
37
|
'import-project.md',
|
|
37
38
|
'discuss.md',
|
|
38
39
|
'plan.md',
|
|
@@ -78,10 +79,30 @@ const COMMANDS = [
|
|
|
78
79
|
'deploy.md',
|
|
79
80
|
// Multi-Model
|
|
80
81
|
'llm.md',
|
|
82
|
+
// Plugins (auto-run via hooks)
|
|
83
|
+
'watchci.md',
|
|
84
|
+
'e2e-verify.md',
|
|
85
|
+
'guard.md',
|
|
86
|
+
'preflight.md',
|
|
87
|
+
// Memory
|
|
88
|
+
'remember.md',
|
|
89
|
+
'recall.md',
|
|
90
|
+
// Dashboard
|
|
91
|
+
'dashboard.md',
|
|
81
92
|
// Help
|
|
82
93
|
'help.md'
|
|
83
94
|
];
|
|
84
95
|
|
|
96
|
+
// Hook scripts that power the plugin system
|
|
97
|
+
const HOOKS = [
|
|
98
|
+
'tlc-block-tools.sh',
|
|
99
|
+
'tlc-prompt-guard.sh',
|
|
100
|
+
'tlc-session-init.sh',
|
|
101
|
+
'tlc-post-push.sh',
|
|
102
|
+
'tlc-post-build.sh',
|
|
103
|
+
'tlc-capture-exchange.sh'
|
|
104
|
+
];
|
|
105
|
+
|
|
85
106
|
function getGlobalDir() {
|
|
86
107
|
const claudeConfig = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude');
|
|
87
108
|
return path.join(claudeConfig, 'commands');
|
|
@@ -115,15 +136,20 @@ function info(msg) {
|
|
|
115
136
|
|
|
116
137
|
function install(targetDir, installType) {
|
|
117
138
|
const commandsDir = path.join(targetDir, 'tlc');
|
|
139
|
+
const packageRoot = path.join(__dirname, '..');
|
|
118
140
|
|
|
119
141
|
// Create directory
|
|
120
142
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
121
143
|
|
|
122
144
|
// Copy command files with version injection
|
|
123
|
-
|
|
145
|
+
// Try .claude/commands/tlc/ first (npm package structure), fall back to root (dev)
|
|
146
|
+
const commandsSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'commands', 'tlc'))
|
|
147
|
+
? path.join(packageRoot, '.claude', 'commands', 'tlc')
|
|
148
|
+
: packageRoot;
|
|
149
|
+
|
|
124
150
|
let installed = 0;
|
|
125
151
|
for (const file of COMMANDS) {
|
|
126
|
-
const src = path.join(
|
|
152
|
+
const src = path.join(commandsSrcDir, file);
|
|
127
153
|
const dest = path.join(commandsDir, file);
|
|
128
154
|
if (fs.existsSync(src)) {
|
|
129
155
|
// Read, replace {{VERSION}}, write
|
|
@@ -135,6 +161,19 @@ function install(targetDir, installType) {
|
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
success(`Installed ${installed} commands to ${c.cyan}${commandsDir}${c.reset}`);
|
|
164
|
+
|
|
165
|
+
// Install hooks (plugin system)
|
|
166
|
+
const hooksInstalled = installHooks(targetDir, packageRoot);
|
|
167
|
+
if (hooksInstalled > 0) {
|
|
168
|
+
success(`Installed ${hooksInstalled} hooks to ${c.cyan}${path.dirname(targetDir)}/.claude/hooks/${c.reset}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Install settings template (with hooks wiring)
|
|
172
|
+
const settingsInstalled = installSettings(targetDir);
|
|
173
|
+
if (settingsInstalled) {
|
|
174
|
+
success(`Installed settings template with hook wiring`);
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
log('');
|
|
139
178
|
log(`${c.green}Done!${c.reset} Restart Claude Code to load commands.`);
|
|
140
179
|
log('');
|
|
@@ -150,6 +189,136 @@ function install(targetDir, installType) {
|
|
|
150
189
|
log('');
|
|
151
190
|
}
|
|
152
191
|
|
|
192
|
+
function installHooks(targetDir, packageRoot) {
|
|
193
|
+
// For local install: .claude/commands -> go up to .claude/hooks
|
|
194
|
+
// For global install: ~/.claude/commands -> go up to ~/.claude/hooks
|
|
195
|
+
const claudeDir = path.dirname(targetDir);
|
|
196
|
+
const hooksDestDir = path.join(claudeDir, 'hooks');
|
|
197
|
+
fs.mkdirSync(hooksDestDir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
// Try .claude/hooks/ first (npm package), fall back to root .claude/hooks/ (dev)
|
|
200
|
+
const hooksSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'hooks'))
|
|
201
|
+
? path.join(packageRoot, '.claude', 'hooks')
|
|
202
|
+
: null;
|
|
203
|
+
|
|
204
|
+
if (!hooksSrcDir) return 0;
|
|
205
|
+
|
|
206
|
+
let copied = 0;
|
|
207
|
+
for (const file of HOOKS) {
|
|
208
|
+
const src = path.join(hooksSrcDir, file);
|
|
209
|
+
const dest = path.join(hooksDestDir, file);
|
|
210
|
+
if (fs.existsSync(src)) {
|
|
211
|
+
fs.copyFileSync(src, dest);
|
|
212
|
+
// Make executable
|
|
213
|
+
fs.chmodSync(dest, 0o755);
|
|
214
|
+
copied++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return copied;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function installSettings(targetDir) {
|
|
221
|
+
// For local install: .claude/commands -> go up to .claude/settings.json
|
|
222
|
+
// For global install: ~/.claude/commands -> go up to ~/.claude/settings.json
|
|
223
|
+
const claudeDir = path.dirname(targetDir);
|
|
224
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
225
|
+
|
|
226
|
+
// The settings template with full hook wiring
|
|
227
|
+
const settingsTemplate = {
|
|
228
|
+
permissions: {
|
|
229
|
+
allow: [
|
|
230
|
+
"Bash(npm *)", "Bash(npx *)", "Bash(node *)", "Bash(git *)",
|
|
231
|
+
"Bash(gh *)", "Bash(ssh *)", "Bash(scp *)", "Bash(rsync *)",
|
|
232
|
+
"Bash(curl *)", "Bash(wget *)", "Bash(docker *)", "Bash(docker-compose *)",
|
|
233
|
+
"Bash(pytest*)", "Bash(python *)", "Bash(pip *)", "Bash(go *)",
|
|
234
|
+
"Bash(cargo *)", "Bash(make *)", "Bash(cat *)", "Bash(ls *)",
|
|
235
|
+
"Bash(pwd*)", "Bash(cd *)", "Bash(mkdir *)", "Bash(cp *)",
|
|
236
|
+
"Bash(mv *)", "Bash(which *)", "Bash(echo *)", "Bash(jq *)",
|
|
237
|
+
"Bash(wc *)", "Bash(head *)", "Bash(tail *)", "Bash(sort *)",
|
|
238
|
+
"Bash(uniq *)", "Bash(xargs *)"
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
hooks: {
|
|
242
|
+
PreToolUse: [{
|
|
243
|
+
matcher: "EnterPlanMode|TaskCreate|TaskUpdate|TaskList|TaskGet|ExitPlanMode",
|
|
244
|
+
hooks: [{
|
|
245
|
+
type: "command",
|
|
246
|
+
command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-block-tools.sh",
|
|
247
|
+
timeout: 5
|
|
248
|
+
}]
|
|
249
|
+
}],
|
|
250
|
+
UserPromptSubmit: [{
|
|
251
|
+
hooks: [{
|
|
252
|
+
type: "command",
|
|
253
|
+
command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-prompt-guard.sh",
|
|
254
|
+
timeout: 5
|
|
255
|
+
}]
|
|
256
|
+
}],
|
|
257
|
+
SessionStart: [{
|
|
258
|
+
hooks: [{
|
|
259
|
+
type: "command",
|
|
260
|
+
command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-session-init.sh",
|
|
261
|
+
timeout: 5
|
|
262
|
+
}]
|
|
263
|
+
}],
|
|
264
|
+
PostToolUse: [
|
|
265
|
+
{
|
|
266
|
+
matcher: "Bash",
|
|
267
|
+
hooks: [{
|
|
268
|
+
type: "command",
|
|
269
|
+
command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-post-push.sh",
|
|
270
|
+
timeout: 5
|
|
271
|
+
}]
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
matcher: "Skill",
|
|
275
|
+
hooks: [{
|
|
276
|
+
type: "command",
|
|
277
|
+
command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-post-build.sh",
|
|
278
|
+
timeout: 5
|
|
279
|
+
}]
|
|
280
|
+
}
|
|
281
|
+
],
|
|
282
|
+
Stop: [{
|
|
283
|
+
hooks: [{
|
|
284
|
+
type: "command",
|
|
285
|
+
command: "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tlc-capture-exchange.sh",
|
|
286
|
+
timeout: 30
|
|
287
|
+
}]
|
|
288
|
+
}]
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if (fs.existsSync(settingsPath)) {
|
|
293
|
+
// Merge: preserve existing permissions, add missing hooks
|
|
294
|
+
try {
|
|
295
|
+
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
296
|
+
// Merge permissions (union)
|
|
297
|
+
if (existing.permissions && existing.permissions.allow) {
|
|
298
|
+
const existingSet = new Set(existing.permissions.allow);
|
|
299
|
+
for (const perm of settingsTemplate.permissions.allow) {
|
|
300
|
+
existingSet.add(perm);
|
|
301
|
+
}
|
|
302
|
+
existing.permissions.allow = [...existingSet];
|
|
303
|
+
} else {
|
|
304
|
+
existing.permissions = settingsTemplate.permissions;
|
|
305
|
+
}
|
|
306
|
+
// Add hooks if not present
|
|
307
|
+
if (!existing.hooks) {
|
|
308
|
+
existing.hooks = settingsTemplate.hooks;
|
|
309
|
+
}
|
|
310
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
311
|
+
return true;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
// If we can't parse existing, don't overwrite
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settingsTemplate, null, 2) + '\n');
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
153
322
|
async function main() {
|
|
154
323
|
const args = process.argv.slice(2);
|
|
155
324
|
|
package/bin/postinstall.js
CHANGED
|
@@ -4,44 +4,63 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
7
|
+
const packageRoot = path.join(__dirname, '..');
|
|
8
|
+
const claudeHome = path.join(os.homedir(), '.claude');
|
|
9
|
+
|
|
10
|
+
// Source directories (inside npm package)
|
|
11
|
+
const commandsSrcDir = path.join(packageRoot, '.claude', 'commands', 'tlc');
|
|
12
|
+
const hooksSrcDir = path.join(packageRoot, '.claude', 'hooks');
|
|
13
|
+
|
|
14
|
+
// Destination directories (user's home)
|
|
15
|
+
const commandsDestDir = path.join(claudeHome, 'commands', 'tlc');
|
|
16
|
+
const hooksDestDir = path.join(claudeHome, 'hooks');
|
|
10
17
|
|
|
11
|
-
// Create destination directory if it doesn't exist
|
|
12
18
|
function ensureDir(dir) {
|
|
13
19
|
if (!fs.existsSync(dir)) {
|
|
14
20
|
fs.mkdirSync(dir, { recursive: true });
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
// Copy all .md files
|
|
24
|
+
// Copy all .md command files
|
|
19
25
|
function copyCommands() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ensureDir(destDir);
|
|
26
|
+
if (!fs.existsSync(commandsSrcDir)) return 0;
|
|
27
|
+
ensureDir(commandsDestDir);
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const files = fs.readdirSync(commandsSrcDir).filter(f => f.endsWith('.md'));
|
|
30
|
+
let copied = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
fs.copyFileSync(path.join(commandsSrcDir, file), path.join(commandsDestDir, file));
|
|
33
|
+
copied++;
|
|
34
|
+
}
|
|
35
|
+
return copied;
|
|
36
|
+
}
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
// Copy all .sh hook files
|
|
39
|
+
function copyHooks() {
|
|
40
|
+
if (!fs.existsSync(hooksSrcDir)) return 0;
|
|
41
|
+
ensureDir(hooksDestDir);
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
const files = fs.readdirSync(hooksSrcDir).filter(f => f.endsWith('.sh'));
|
|
44
|
+
let copied = 0;
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const dest = path.join(hooksDestDir, file);
|
|
47
|
+
fs.copyFileSync(path.join(hooksSrcDir, file), dest);
|
|
48
|
+
fs.chmodSync(dest, 0o755);
|
|
49
|
+
copied++;
|
|
50
|
+
}
|
|
51
|
+
return copied;
|
|
52
|
+
}
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
function postinstall() {
|
|
55
|
+
try {
|
|
56
|
+
const commands = copyCommands();
|
|
57
|
+
const hooks = copyHooks();
|
|
42
58
|
|
|
43
|
-
if (
|
|
44
|
-
console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${
|
|
59
|
+
if (commands > 0) {
|
|
60
|
+
console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${commands} commands to ~/.claude/commands/tlc/`);
|
|
61
|
+
}
|
|
62
|
+
if (hooks > 0) {
|
|
63
|
+
console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${hooks} hooks to ~/.claude/hooks/`);
|
|
45
64
|
}
|
|
46
65
|
} catch (err) {
|
|
47
66
|
// Silent fail - don't break npm install
|
|
@@ -51,4 +70,4 @@ function copyCommands() {
|
|
|
51
70
|
}
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
|
|
73
|
+
postinstall();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--color-bg-primary: #0a0a0b;--color-bg-secondary: #141416;--color-bg-tertiary: #1e1e21;--color-bg-elevated: #252529;--color-text-primary: #fafafa;--color-text-secondary: #a1a1aa;--color-text-muted: #71717a;--color-border: #27272a;--color-border-hover: #3f3f46;--color-accent: #3b82f6;--color-accent-hover: #2563eb;--color-success: #22c55e;--color-warning: #eab308;--color-error: #ef4444;--color-info: #06b6d4;--space-0: 0;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--space-10: 2.5rem;--space-12: 3rem;--space-16: 4rem;--font-sans: "Inter", system-ui, -apple-system, sans-serif;--font-mono: "JetBrains Mono", ui-monospace, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: 1rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.5rem;--text-3xl: 1.875rem;--font-normal: 400;--font-medium: 500;--font-semibold: 600;--font-bold: 700;--leading-tight: 1.25;--leading-normal: 1.5;--leading-relaxed: 1.625;--radius-sm: .25rem;--radius-md: .375rem;--radius-lg: .5rem;--radius-xl: .75rem;--radius-2xl: 1rem;--radius-full: 9999px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);--shadow-md: 0 4px 6px rgba(0, 0, 0, .3);--shadow-lg: 0 10px 15px rgba(0, 0, 0, .3);--shadow-xl: 0 20px 25px rgba(0, 0, 0, .3);--transition-fast: .1s ease;--transition-base: .2s ease;--transition-slow: .3s ease;--z-dropdown: 1000;--z-sticky: 1020;--z-modal: 1030;--z-popover: 1040;--z-tooltip: 1050;--z-toast: 1060;--sidebar-width: 240px;--sidebar-collapsed-width: 64px;--header-height: 56px;--mobile-nav-height: 64px}[data-theme=light]{--color-bg-primary: #ffffff;--color-bg-secondary: #f4f4f5;--color-bg-tertiary: #e4e4e7;--color-bg-elevated: #ffffff;--color-text-primary: #09090b;--color-text-secondary: #52525b;--color-text-muted: #a1a1aa;--color-border: #e4e4e7;--color-border-hover: #d4d4d8;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .05);--shadow-md: 0 4px 6px rgba(0, 0, 0, .07);--shadow-lg: 0 10px 15px rgba(0, 0, 0, .1);--shadow-xl: 0 20px 25px rgba(0, 0, 0, .1)}@media(prefers-contrast:high){:root{--color-border: #52525b;--color-text-secondary: #d4d4d8}[data-theme=light]{--color-border: #71717a;--color-text-secondary: #3f3f46}}@media(prefers-reduced-motion:reduce){:root{--transition-fast: 0ms;--transition-base: 0ms;--transition-slow: 0ms}}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:var(--font-sans);font-size:16px;line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;padding:0;background-color:var(--color-bg-primary);color:var(--color-text-primary)}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}.skip-link{position:absolute;top:-40px;left:0;padding:var(--space-2) var(--space-4);background:var(--color-accent);color:#fff;z-index:var(--z-tooltip);transition:top var(--transition-fast)}.skip-link:focus{top:0}::-moz-selection{background-color:var(--color-accent);color:#fff}::selection{background-color:var(--color-accent);color:#fff}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--color-bg-secondary)}::-webkit-scrollbar-thumb{background:var(--color-border-hover);border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--color-text-muted)}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-md);padding:.5rem 1rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: var(--color-accent);--tw-ring-offset-width: 2px}.btn:disabled{cursor:not-allowed;opacity:.5}.btn-primary{background-color:var(--color-accent);--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-primary:hover{background-color:var(--color-accent-hover)}.btn-secondary{border-width:1px;border-color:var(--color-border);background-color:var(--color-bg-tertiary);color:var(--color-text-primary)}.btn-secondary:hover{background-color:var(--color-bg-elevated)}.btn-ghost{background-color:transparent;color:var(--color-text-secondary)}.btn-ghost:hover{background-color:var(--color-bg-tertiary);color:var(--color-text-primary)}.btn-danger{background-color:var(--color-error);--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-danger:hover{opacity:.9}.btn-sm{padding:.375rem .75rem;font-size:.875rem;line-height:1.25rem}.btn-lg{padding:.75rem 1.5rem;font-size:1.125rem;line-height:1.75rem}.card{border-radius:var(--radius-lg);border-width:1px;border-color:var(--color-border);background-color:var(--color-bg-secondary)}.card-hover{cursor:pointer;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.card-hover:hover{border-color:var(--color-border-hover);background-color:var(--color-bg-tertiary)}.input{width:100%;padding:.5rem .75rem;border-radius:var(--radius-md);border-width:1px;border-color:var(--color-border);background-color:var(--color-bg-tertiary);color:var(--color-text-primary)}.input::-moz-placeholder{color:var(--color-text-muted)}.input::placeholder{color:var(--color-text-muted)}.input:focus{border-color:transparent;outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: var(--color-accent)}.input:disabled{cursor:not-allowed;opacity:.5}.badge{display:inline-flex;align-items:center;gap:.25rem;border-radius:9999px;padding:.125rem .5rem;font-size:.75rem;line-height:1rem;font-weight:500}.badge-success{color:var(--color-success);background-color:color-mix(in srgb,var(--color-success) 20%,transparent)}.badge-warning{color:var(--color-warning);background-color:color-mix(in srgb,var(--color-warning) 20%,transparent)}.badge-error{color:var(--color-error);background-color:color-mix(in srgb,var(--color-error) 20%,transparent)}.badge-info{color:var(--color-info);background-color:color-mix(in srgb,var(--color-info) 20%,transparent)}.badge-neutral{background-color:var(--color-bg-tertiary);color:var(--color-text-secondary)}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-1{top:-.25rem}.-top-10{top:-2.5rem}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-1\.5{left:.375rem}.left-2{left:.5rem}.left-3{left:.75rem}.left-4{left:1rem}.left-full{left:100%}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.-mb-px{margin-bottom:-1px}.-ml-2{margin-left:-.5rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-10{margin-left:2.5rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.ml-9{margin-left:2.25rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[18px\]{height:18px}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[400px\]{max-height:400px}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-\[100px\]{min-height:100px}.min-h-\[200px\]{min-height:200px}.min-h-\[400px\]{min-height:400px}.min-h-\[60vh\]{min-height:60vh}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-1\/3{width:33.333333%}.w-1\/4{width:25%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-5\/6{width:83.333333%}.w-56{width:14rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[180px\]{min-width:180px}.min-w-\[18px\]{min-width:18px}.min-w-\[300px\]{min-width:300px}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-\[400px\]{max-width:400px}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-border>:not([hidden])~:not([hidden]){border-color:var(--color-border)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-x{border-left-width:1px;border-right-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-accent{border-color:var(--color-accent)}.border-border{border-color:var(--color-border)}.border-error{border-color:var(--color-error)}.border-info{border-color:var(--color-info)}.border-transparent{border-color:transparent}.border-warning{border-color:var(--color-warning)}.border-l-error{border-left-color:var(--color-error)}.border-l-info{border-left-color:var(--color-info)}.border-l-success{border-left-color:var(--color-success)}.border-l-warning{border-left-color:var(--color-warning)}.bg-accent{background-color:var(--color-accent)}.bg-bg-elevated{background-color:var(--color-bg-elevated)}.bg-bg-primary{background-color:var(--color-bg-primary)}.bg-bg-secondary{background-color:var(--color-bg-secondary)}.bg-bg-tertiary{background-color:var(--color-bg-tertiary)}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/80{background-color:#000c}.bg-border{background-color:var(--color-border)}.bg-error{background-color:var(--color-error)}.bg-info{background-color:var(--color-info)}.bg-success{background-color:var(--color-success)}.bg-text-muted{background-color:var(--color-text-muted)}.bg-transparent{background-color:transparent}.bg-warning{background-color:var(--color-warning)}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0{padding-bottom:0}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-8{padding-left:2rem}.pl-9{padding-left:2.25rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pr-8{padding-right:2rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-\[20vh\]{padding-top:20vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-accent{color:var(--color-accent)}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-error{color:var(--color-error)}.text-info{color:var(--color-info)}.text-success{color:var(--color-success)}.text-text-muted{color:var(--color-text-muted)}.text-text-primary{color:var(--color-text-primary)}.text-text-secondary{color:var(--color-text-secondary)}.text-warning{color:var(--color-warning)}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: var(--shadow-lg);--tw-shadow-colored: var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.placeholder\:text-text-muted::-moz-placeholder{color:var(--color-text-muted)}.placeholder\:text-text-muted::placeholder{color:var(--color-text-muted)}.first\:mt-0:first-child{margin-top:0}.hover\:bg-accent-hover:hover{background-color:var(--color-accent-hover)}.hover\:bg-bg-tertiary:hover{background-color:var(--color-bg-tertiary)}.hover\:text-error:hover{color:var(--color-error)}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-text-primary:hover{color:var(--color-text-primary)}.hover\:text-text-secondary:hover{color:var(--color-text-secondary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-md:hover{--tw-shadow: var(--shadow-md);--tw-shadow-colored: var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow: var(--shadow-sm);--tw-shadow-colored: var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:border-accent:focus{border-color:var(--color-accent)}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-accent:focus{--tw-ring-color: var(--color-accent)}.focus\:ring-error:focus{--tw-ring-color: var(--color-error)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:640px){.sm\:block{display:block}.sm\:flex-row{flex-direction:row}}@media(min-width:768px){.md\:block{display:block}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
|