feat(electrobun): project wizard phases 1-5 (WIP)
- sanitize.ts: input sanitization (trim, control chars, path traversal) - provider-scanner.ts: detect Claude/Codex/Ollama/Gemini availability - model-fetcher.ts: live model lists from 4 provider APIs - ModelConfigPanel.svelte: per-provider config (thinking, effort, sandbox, temperature) - WizardStep1-3.svelte: split wizard into composable steps - CustomDropdown/Checkbox/Radio: themed UI components - provider-handlers.ts: provider.scan + provider.models RPC - Wire providers into wizard step 3 (live detection + model lists) - Replace native selects in 5 settings panels with CustomDropdown
This commit is contained in:
parent
b7fc3a0f9b
commit
d4014a193d
25 changed files with 2112 additions and 759 deletions
|
|
@ -1,20 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { t } from './i18n.svelte.ts';
|
||||
import { appRpc } from './rpc.ts';
|
||||
import PathBrowser from './PathBrowser.svelte';
|
||||
import { sanitize, sanitizePath, sanitizeUrl, isValidGitUrl, isValidGithubRepo } from './sanitize.ts';
|
||||
import { getProjects } from './workspace-store.svelte.ts';
|
||||
import WizardStep1 from './WizardStep1.svelte';
|
||||
import WizardStep2 from './WizardStep2.svelte';
|
||||
import WizardStep3 from './WizardStep3.svelte';
|
||||
|
||||
interface ProjectResult {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
autoStart?: boolean;
|
||||
groupId?: string;
|
||||
useWorktrees?: boolean;
|
||||
shell?: string;
|
||||
icon?: string;
|
||||
id: string; name: string; cwd: string; provider?: string; model?: string;
|
||||
systemPrompt?: string; autoStart?: boolean; groupId?: string;
|
||||
useWorktrees?: boolean; shell?: string; icon?: string; color?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -28,55 +23,62 @@
|
|||
|
||||
// ── Wizard step ─────────────────────────────────────────────
|
||||
type SourceType = 'local' | 'git-clone' | 'github' | 'template' | 'remote';
|
||||
type AuthMethod = 'password' | 'key' | 'agent' | 'config';
|
||||
|
||||
let step = $state(1);
|
||||
|
||||
// ── Step 1: Source ──────────────────────────────────────────
|
||||
// ── Step 1 state ────────────────────────────────────────────
|
||||
let sourceType = $state<SourceType>('local');
|
||||
let localPath = $state('');
|
||||
let repoUrl = $state('');
|
||||
let cloneTarget = $state('');
|
||||
let githubRepo = $state('');
|
||||
let selectedTemplate = $state('');
|
||||
let templateTargetDir = $state('~/projects');
|
||||
let remoteHost = $state('');
|
||||
let remoteUser = $state('');
|
||||
let remotePath = $state('');
|
||||
|
||||
// Path validation
|
||||
let remoteAuthMethod = $state<AuthMethod>('agent');
|
||||
let remotePassword = $state('');
|
||||
let remoteKeyPath = $state('~/.ssh/id_ed25519');
|
||||
let remoteSshfs = $state(false);
|
||||
let pathValid = $state<'idle' | 'checking' | 'valid' | 'invalid' | 'not-dir'>('idle');
|
||||
let isGitRepo = $state(false);
|
||||
let gitBranch = $state('');
|
||||
let showBrowser = $state(false);
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let gitProbeStatus = $state<'idle' | 'probing' | 'ok' | 'error'>('idle');
|
||||
let gitProbeBranches = $state<string[]>([]);
|
||||
let githubInfo = $state<{ stars: number; description: string; defaultBranch: string } | null>(null);
|
||||
let githubLoading = $state(false);
|
||||
let cloning = $state(false);
|
||||
let templates = $state<Array<{ id: string; name: string; description: string; icon: string }>>([]);
|
||||
|
||||
// ── Step 2: Configure ──────────────────────────────────────
|
||||
// ── Step 2 state ────────────────────────────────────────────
|
||||
let projectName = $state('');
|
||||
let nameError = $state('');
|
||||
let selectedBranch = $state('');
|
||||
let branches = $state<string[]>([]);
|
||||
let useWorktrees = $state(false);
|
||||
let selectedGroupId = $state(groupId);
|
||||
let projectIcon = $state('📁');
|
||||
let projectIcon = $state('Terminal');
|
||||
let projectColor = $state('var(--ctp-blue)');
|
||||
let shellChoice = $state('bash');
|
||||
|
||||
const SHELLS = ['bash', 'zsh', 'fish', 'sh'];
|
||||
let branchDDOpen = $state(false);
|
||||
let groupDDOpen = $state(false);
|
||||
let shellDDOpen = $state(false);
|
||||
let branchLabel = $derived(selectedBranch || 'main');
|
||||
let groupLabel = $derived(groups.find(g => g.id === selectedGroupId)?.name ?? 'Select');
|
||||
function closeAllDD() { branchDDOpen = false; groupDDOpen = false; shellDDOpen = false; }
|
||||
const EMOJI_GRID = ['📁', '🚀', '⚡', '🔧', '🌐', '📦', '🎯', '💡', '🔬', '🎨', '📊', '🛡️', '🤖', '🧪', '🏗️', '📝'];
|
||||
|
||||
// ── Step 3: Agent ──────────────────────────────────────────
|
||||
let provider = $state<'claude' | 'codex' | 'ollama'>('claude');
|
||||
// ── Step 3 state ────────────────────────────────────────────
|
||||
let provider = $state<string>('claude');
|
||||
let model = $state('');
|
||||
let permissionMode = $state('default');
|
||||
let systemPrompt = $state('');
|
||||
let autoStart = $state(false);
|
||||
let cloning = $state(false);
|
||||
let detectedProviders = $state<Array<{ id: string; available: boolean; hasApiKey: boolean; hasCli: boolean; cliPath: string | null; version: string | null }>>([]);
|
||||
let providerModels = $state<Array<{ id: string; name: string; provider: string }>>([]);
|
||||
let modelsLoading = $state(false);
|
||||
|
||||
// ── Templates ───────────────────────────────────────────────
|
||||
let templates = $state<Array<{ id: string; name: string; description: string; icon: string }>>([]);
|
||||
// ── Debounce timers ─────────────────────────────────────────
|
||||
let pathTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let probeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let githubTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// ── Templates loading ───────────────────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'template' && templates.length === 0) {
|
||||
appRpc.request['project.templates']({}).then(r => {
|
||||
|
|
@ -85,82 +87,108 @@
|
|||
}
|
||||
});
|
||||
|
||||
// ── Path validation (debounced 300ms) ──────────────────────
|
||||
function validatePath(p: string) {
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
if (!p.trim()) { pathValid = 'idle'; isGitRepo = false; return; }
|
||||
pathValid = 'checking';
|
||||
debounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
const result = await appRpc.request['files.statEx']({ path: p });
|
||||
if (!result?.exists) {
|
||||
pathValid = 'invalid';
|
||||
isGitRepo = false;
|
||||
} else if (!result.isDirectory) {
|
||||
pathValid = 'not-dir';
|
||||
isGitRepo = false;
|
||||
} else {
|
||||
pathValid = 'valid';
|
||||
isGitRepo = result.isGitRepo;
|
||||
gitBranch = result.gitBranch ?? '';
|
||||
// Auto-populate name from folder name
|
||||
if (!projectName) {
|
||||
const parts = p.replace(/\/+$/, '').split('/');
|
||||
projectName = parts[parts.length - 1] || '';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
pathValid = 'invalid';
|
||||
isGitRepo = false;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// ── Path validation (debounced) ─────────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'local') validatePath(localPath);
|
||||
if (sourceType === 'local') {
|
||||
if (pathTimer) clearTimeout(pathTimer);
|
||||
if (!localPath.trim()) { pathValid = 'idle'; isGitRepo = false; return; }
|
||||
pathValid = 'checking';
|
||||
pathTimer = setTimeout(async () => {
|
||||
try {
|
||||
const result = await appRpc.request['files.statEx']({ path: localPath });
|
||||
if (!result?.exists) { pathValid = 'invalid'; isGitRepo = false; }
|
||||
else if (!result.isDirectory) { pathValid = 'not-dir'; isGitRepo = false; }
|
||||
else { pathValid = 'valid'; isGitRepo = result.isGitRepo; gitBranch = result.gitBranch ?? ''; if (!projectName) { const parts = localPath.replace(/\/+$/, '').split('/'); projectName = parts[parts.length - 1] || ''; } }
|
||||
} catch { pathValid = 'invalid'; isGitRepo = false; }
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Branch loading ─────────────────────────────────────────
|
||||
async function loadBranches(repoPath: string) {
|
||||
try {
|
||||
const result = await appRpc.request['git.branches']({ path: repoPath });
|
||||
if (result?.branches) {
|
||||
branches = result.branches;
|
||||
selectedBranch = result.current || '';
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// ── Git probe (debounced) ───────────────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'git-clone' && repoUrl.trim()) {
|
||||
if (probeTimer) clearTimeout(probeTimer);
|
||||
gitProbeStatus = 'probing';
|
||||
probeTimer = setTimeout(async () => {
|
||||
if (!isValidGitUrl(repoUrl)) { gitProbeStatus = 'error'; return; }
|
||||
try {
|
||||
const r = await appRpc.request['git.probe']({ url: repoUrl.trim() });
|
||||
if (r?.ok) { gitProbeStatus = 'ok'; gitProbeBranches = r.branches; } else { gitProbeStatus = 'error'; }
|
||||
} catch { gitProbeStatus = 'error'; }
|
||||
}, 600);
|
||||
} else if (sourceType === 'git-clone') { gitProbeStatus = 'idle'; }
|
||||
});
|
||||
|
||||
// ── Step navigation ────────────────────────────────────────
|
||||
// ── GitHub validation (debounced) ───────────────────────────
|
||||
$effect(() => {
|
||||
if (sourceType === 'github' && githubRepo.trim()) {
|
||||
if (githubTimer) clearTimeout(githubTimer);
|
||||
githubInfo = null;
|
||||
if (!isValidGithubRepo(githubRepo)) return;
|
||||
githubLoading = true;
|
||||
githubTimer = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${githubRepo.trim()}`, { signal: AbortSignal.timeout(5000) });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
githubInfo = { stars: data.stargazers_count ?? 0, description: data.description ?? '', defaultBranch: data.default_branch ?? 'main' };
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
githubLoading = false;
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Unique name validation ──────────────────────────────────
|
||||
$effect(() => {
|
||||
const trimmed = projectName.trim().toLowerCase();
|
||||
if (!trimmed) { nameError = ''; return; }
|
||||
const existing = getProjects().map(p => p.name.toLowerCase());
|
||||
nameError = existing.includes(trimmed) ? 'A project with this name already exists' : '';
|
||||
});
|
||||
|
||||
// ── Provider scan on step 3 entry ───────────────────────────
|
||||
$effect(() => {
|
||||
if (step === 3 && detectedProviders.length === 0) {
|
||||
appRpc.request['provider.scan']({}).then(r => {
|
||||
if (r?.providers) { detectedProviders = r.providers; const first = r.providers.find(p => p.available); if (first) provider = first.id; }
|
||||
}).catch(console.error);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Model list on provider change ───────────────────────────
|
||||
$effect(() => {
|
||||
if (step === 3 && provider) {
|
||||
providerModels = []; modelsLoading = true;
|
||||
appRpc.request['provider.models']({ provider }).then(r => {
|
||||
if (r?.models) providerModels = r.models;
|
||||
}).catch(console.error).finally(() => { modelsLoading = false; });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Step 1 validation ──────────────────────────────────────
|
||||
let step1Valid = $derived(() => {
|
||||
switch (sourceType) {
|
||||
case 'local': return pathValid === 'valid';
|
||||
case 'git-clone': return repoUrl.trim().length > 0 && cloneTarget.trim().length > 0;
|
||||
case 'github': return /^[\w.-]+\/[\w.-]+$/.test(githubRepo.trim());
|
||||
case 'template': return selectedTemplate !== '';
|
||||
case 'remote': return remoteHost.trim().length > 0 && remoteUser.trim().length > 0 && remotePath.trim().length > 0;
|
||||
case 'git-clone': return isValidGitUrl(repoUrl) && !!sanitizePath(cloneTarget);
|
||||
case 'github': return isValidGithubRepo(githubRepo);
|
||||
case 'template': return selectedTemplate !== '' && !!sanitizePath(templateTargetDir);
|
||||
case 'remote': return !!sanitize(remoteHost) && !!sanitize(remoteUser) && !!sanitize(remotePath);
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Step navigation ────────────────────────────────────────
|
||||
async function goToStep2() {
|
||||
if (sourceType === 'git-clone') {
|
||||
const url = sanitizeUrl(repoUrl); if (!url) return;
|
||||
const target = sanitizePath(cloneTarget); if (!target) return;
|
||||
cloning = true;
|
||||
try {
|
||||
const result = await appRpc.request['git.clone']({
|
||||
url: repoUrl.trim(),
|
||||
target: cloneTarget.trim(),
|
||||
});
|
||||
if (!result?.ok) {
|
||||
cloning = false;
|
||||
return; // stay on step 1
|
||||
}
|
||||
localPath = cloneTarget.trim();
|
||||
isGitRepo = true;
|
||||
} catch {
|
||||
cloning = false;
|
||||
return;
|
||||
}
|
||||
const result = await appRpc.request['git.clone']({ url, target });
|
||||
if (!result?.ok) { cloning = false; return; }
|
||||
localPath = target; isGitRepo = true;
|
||||
} catch { cloning = false; return; }
|
||||
cloning = false;
|
||||
} else if (sourceType === 'github') {
|
||||
const url = `https://github.com/${githubRepo.trim()}.git`;
|
||||
|
|
@ -169,312 +197,133 @@
|
|||
try {
|
||||
const result = await appRpc.request['git.clone']({ url, target });
|
||||
if (!result?.ok) { cloning = false; return; }
|
||||
localPath = target;
|
||||
isGitRepo = true;
|
||||
localPath = target; isGitRepo = true;
|
||||
} catch { cloning = false; return; }
|
||||
cloning = false;
|
||||
} else if (sourceType === 'template') {
|
||||
const target = sanitizePath(templateTargetDir); if (!target) return;
|
||||
const name = sanitize(projectName) || selectedTemplate;
|
||||
cloning = true;
|
||||
try {
|
||||
const result = await appRpc.request['project.createFromTemplate']({ templateId: selectedTemplate, targetDir: target, projectName: name });
|
||||
if (!result?.ok) { cloning = false; return; }
|
||||
localPath = result.path; if (!projectName) projectName = name;
|
||||
} catch { cloning = false; return; }
|
||||
cloning = false;
|
||||
}
|
||||
|
||||
// Auto-populate name
|
||||
if (!projectName && localPath) {
|
||||
const parts = localPath.replace(/\/+$/, '').split('/');
|
||||
projectName = parts[parts.length - 1] || '';
|
||||
}
|
||||
|
||||
// Load branches if git repo
|
||||
if (!projectName && localPath) { const parts = localPath.replace(/\/+$/, '').split('/'); projectName = parts[parts.length - 1] || ''; }
|
||||
if (isGitRepo && localPath) {
|
||||
await loadBranches(localPath);
|
||||
try { const r = await appRpc.request['git.branches']({ path: localPath }); if (r?.branches) { branches = r.branches; selectedBranch = r.current || ''; } } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
step = 2;
|
||||
}
|
||||
|
||||
function goToStep3() { step = 3; }
|
||||
function goToStep3() { if (nameError) return; step = 3; }
|
||||
function goBack() { step = Math.max(1, step - 1); }
|
||||
|
||||
async function createProject() {
|
||||
const cwd = sourceType === 'remote'
|
||||
? `ssh://${remoteUser}@${remoteHost}:${remotePath}`
|
||||
: localPath.trim();
|
||||
|
||||
const cwd = sourceType === 'remote' ? `ssh://${sanitize(remoteUser)}@${sanitize(remoteHost)}:${sanitize(remotePath)}` : (sanitizePath(localPath) || localPath.trim());
|
||||
const project: ProjectResult = {
|
||||
id: `p-${Date.now()}`,
|
||||
name: projectName.trim() || 'Untitled',
|
||||
cwd,
|
||||
provider,
|
||||
model: model || undefined,
|
||||
systemPrompt: systemPrompt || undefined,
|
||||
autoStart,
|
||||
groupId: selectedGroupId,
|
||||
useWorktrees: useWorktrees || undefined,
|
||||
shell: shellChoice,
|
||||
icon: projectIcon,
|
||||
id: `p-${Date.now()}`, name: sanitize(projectName) || 'Untitled', cwd,
|
||||
provider: provider as string, model: model || undefined, systemPrompt: systemPrompt || undefined,
|
||||
autoStart, groupId: selectedGroupId, useWorktrees: useWorktrees || undefined,
|
||||
shell: shellChoice, icon: projectIcon, color: projectColor,
|
||||
};
|
||||
|
||||
onCreated(project);
|
||||
resetState();
|
||||
}
|
||||
|
||||
function handleBrowserSelect(path: string) {
|
||||
localPath = path;
|
||||
showBrowser = false;
|
||||
validatePath(path);
|
||||
function closeWizard() { resetState(); onClose(); }
|
||||
|
||||
function resetState() {
|
||||
step = 1; sourceType = 'local'; localPath = ''; repoUrl = ''; cloneTarget = '';
|
||||
githubRepo = ''; selectedTemplate = ''; templateTargetDir = '~/projects';
|
||||
remoteHost = ''; remoteUser = ''; remotePath = '';
|
||||
remoteAuthMethod = 'agent'; remotePassword = ''; remoteKeyPath = '~/.ssh/id_ed25519'; remoteSshfs = false;
|
||||
pathValid = 'idle'; isGitRepo = false; gitBranch = '';
|
||||
gitProbeStatus = 'idle'; gitProbeBranches = []; githubInfo = null; githubLoading = false;
|
||||
cloning = false; projectName = ''; nameError = ''; selectedBranch = '';
|
||||
branches = []; useWorktrees = false; selectedGroupId = groupId;
|
||||
projectIcon = 'Terminal'; projectColor = 'var(--ctp-blue)'; shellChoice = 'bash';
|
||||
provider = 'claude'; model = ''; permissionMode = 'default';
|
||||
systemPrompt = ''; autoStart = false; providerModels = []; modelsLoading = false;
|
||||
}
|
||||
|
||||
async function handleNativeBrowse() {
|
||||
try {
|
||||
const result = await appRpc.request['files.pickDirectory']({ startingFolder: localPath || '~/' });
|
||||
if (result?.path) {
|
||||
localPath = result.path;
|
||||
validatePath(result.path);
|
||||
}
|
||||
} catch {
|
||||
// Fallback: native dialog not available, user types path manually
|
||||
console.warn('[wizard] Native folder picker not available');
|
||||
}
|
||||
}
|
||||
|
||||
// Validation indicator
|
||||
function validationIcon(state: typeof pathValid): string {
|
||||
switch (state) {
|
||||
case 'valid': return '✓';
|
||||
case 'invalid': return '✗';
|
||||
case 'not-dir': return '⚠';
|
||||
case 'checking': return '…';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
function validationColor(state: typeof pathValid): string {
|
||||
switch (state) {
|
||||
case 'valid': return 'var(--ctp-green)';
|
||||
case 'invalid': return 'var(--ctp-red)';
|
||||
case 'not-dir': return 'var(--ctp-peach)';
|
||||
default: return 'var(--ctp-overlay0)';
|
||||
// ── Field updater (passed to sub-components) ────────────────
|
||||
function handleUpdate(field: string, value: unknown) {
|
||||
const v = value as string & boolean;
|
||||
switch (field) {
|
||||
case 'sourceType': sourceType = v as SourceType; break;
|
||||
case 'localPath': localPath = v; break;
|
||||
case 'repoUrl': repoUrl = v; break;
|
||||
case 'cloneTarget': cloneTarget = v; break;
|
||||
case 'githubRepo': githubRepo = v; break;
|
||||
case 'selectedTemplate': selectedTemplate = v; break;
|
||||
case 'templateTargetDir': templateTargetDir = v; break;
|
||||
case 'remoteHost': remoteHost = v; break;
|
||||
case 'remoteUser': remoteUser = v; break;
|
||||
case 'remotePath': remotePath = v; break;
|
||||
case 'remoteAuthMethod': remoteAuthMethod = v as AuthMethod; break;
|
||||
case 'remotePassword': remotePassword = v; break;
|
||||
case 'remoteKeyPath': remoteKeyPath = v; break;
|
||||
case 'remoteSshfs': remoteSshfs = v as unknown as boolean; break;
|
||||
case 'projectName': projectName = v; break;
|
||||
case 'selectedBranch': selectedBranch = v; break;
|
||||
case 'useWorktrees': useWorktrees = v as unknown as boolean; break;
|
||||
case 'selectedGroupId': selectedGroupId = v; break;
|
||||
case 'projectIcon': projectIcon = v; break;
|
||||
case 'projectColor': projectColor = v; break;
|
||||
case 'shellChoice': shellChoice = v; break;
|
||||
case 'provider': provider = v; break;
|
||||
case 'model': model = v; break;
|
||||
case 'permissionMode': permissionMode = v; break;
|
||||
case 'systemPrompt': systemPrompt = v; break;
|
||||
case 'autoStart': autoStart = v as unknown as boolean; break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wizard-overlay" role="dialog" aria-label={t('wizard.title' as any)}>
|
||||
<div class="wizard-overlay" role="dialog" aria-label="New Project">
|
||||
<div class="wizard-panel">
|
||||
<!-- Header -->
|
||||
<div class="wz-header">
|
||||
<h2 class="wz-title">{t('wizard.title' as any)}</h2>
|
||||
<h2 class="wz-title">New Project</h2>
|
||||
<div class="wz-steps">
|
||||
{#each [1, 2, 3] as s}
|
||||
<span class="wz-step-dot" class:active={step === s} class:done={step > s}>{s}</span>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="wz-close" onclick={onClose} aria-label={t('common.close' as any)}>✕</button>
|
||||
<button class="wz-close" onclick={closeWizard} aria-label="Close">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Source -->
|
||||
<div class="wz-body" style:display={step === 1 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">{t('wizard.step1.title' as any)}</h3>
|
||||
|
||||
<!-- Source type radios -->
|
||||
<div class="wz-radios">
|
||||
{#each [
|
||||
{ value: 'local', label: t('wizard.step1.local' as any), icon: '📁' },
|
||||
{ value: 'git-clone', label: t('wizard.step1.gitClone' as any), icon: '🔀' },
|
||||
{ value: 'github', label: t('wizard.step1.github' as any), icon: '🐙' },
|
||||
{ value: 'template', label: t('wizard.step1.template' as any), icon: '📋' },
|
||||
{ value: 'remote', label: t('wizard.step1.remote' as any), icon: '🖥️' },
|
||||
] as opt}
|
||||
<label class="wz-radio" class:selected={sourceType === opt.value}>
|
||||
<input type="radio" name="source" value={opt.value}
|
||||
checked={sourceType === opt.value}
|
||||
onchange={() => sourceType = opt.value as SourceType} />
|
||||
<span class="wz-radio-icon">{opt.icon}</span>
|
||||
<span>{opt.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Source-specific inputs -->
|
||||
<div class="wz-source-fields">
|
||||
{#if sourceType === 'local'}
|
||||
<label class="wz-label">{t('wizard.step1.pathLabel' as any)}</label>
|
||||
<div class="wz-path-row" style="position: relative;">
|
||||
<input class="wz-input" type="text"
|
||||
placeholder={t('wizard.step1.pathPlaceholder' as any)}
|
||||
bind:value={localPath}
|
||||
oninput={() => validatePath(localPath)} />
|
||||
<button class="wz-browse-btn" onclick={handleNativeBrowse}
|
||||
title="Open system folder picker"
|
||||
aria-label={t('wizard.step1.browse' as any)}>📂</button>
|
||||
<button class="wz-browse-btn" onclick={() => showBrowser = !showBrowser}
|
||||
title="In-app folder browser"
|
||||
aria-label="Browse">🔍</button>
|
||||
{#if pathValid !== 'idle'}
|
||||
<span class="wz-validation" style:color={validationColor(pathValid)}>
|
||||
{validationIcon(pathValid)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div style:display={showBrowser ? 'block' : 'none'} style="max-height: 16rem; max-width: 32rem; overflow-y: auto; border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; margin-top: 0.5rem;">
|
||||
<PathBrowser onSelect={handleBrowserSelect} onClose={() => showBrowser = false} />
|
||||
</div>
|
||||
{#if pathValid === 'valid' && isGitRepo}
|
||||
<span class="wz-badge git-badge">{t('wizard.step1.gitDetected' as any)} ({gitBranch})</span>
|
||||
{/if}
|
||||
{#if pathValid === 'invalid'}
|
||||
<span class="wz-hint error">{t('wizard.step1.invalidPath' as any)}</span>
|
||||
{/if}
|
||||
{#if pathValid === 'not-dir'}
|
||||
<span class="wz-hint warn">{t('wizard.step1.notDir' as any)}</span>
|
||||
{/if}
|
||||
|
||||
{:else if sourceType === 'git-clone'}
|
||||
<label class="wz-label">{t('wizard.step1.repoUrl' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="https://github.com/user/repo.git" bind:value={repoUrl} />
|
||||
<label class="wz-label">{t('wizard.step1.targetDir' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="~/projects/my-repo" bind:value={cloneTarget} />
|
||||
|
||||
{:else if sourceType === 'github'}
|
||||
<label class="wz-label">{t('wizard.step1.githubRepo' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="owner/repo" bind:value={githubRepo} />
|
||||
|
||||
{:else if sourceType === 'template'}
|
||||
<div class="wz-template-grid">
|
||||
{#each templates as tmpl}
|
||||
<button class="wz-template-card" class:selected={selectedTemplate === tmpl.id}
|
||||
onclick={() => selectedTemplate = tmpl.id}>
|
||||
<span class="wz-template-icon">{tmpl.icon}</span>
|
||||
<span class="wz-template-name">{tmpl.name}</span>
|
||||
<span class="wz-template-desc">{tmpl.description}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:else if sourceType === 'remote'}
|
||||
<label class="wz-label">{t('wizard.step1.hostLabel' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="192.168.1.100" bind:value={remoteHost} />
|
||||
<label class="wz-label">{t('wizard.step1.userLabel' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="user" bind:value={remoteUser} />
|
||||
<label class="wz-label">{t('wizard.step1.pathLabel' as any)}</label>
|
||||
<input class="wz-input" type="text" placeholder="/home/user/project" bind:value={remotePath} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Step 1 footer -->
|
||||
<h3 class="wz-step-title">Source</h3>
|
||||
<WizardStep1 {sourceType} {localPath} {repoUrl} {cloneTarget} {githubRepo} {selectedTemplate} {remoteHost} {remoteUser} {remotePath} {remoteAuthMethod} {remotePassword} {remoteKeyPath} {remoteSshfs} {pathValid} {isGitRepo} {gitBranch} {gitProbeStatus} {gitProbeBranches} {githubInfo} {githubLoading} {cloning} {templates} {templateTargetDir} onUpdate={handleUpdate} />
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={onClose}>{t('common.cancel' as any)}</button>
|
||||
<button class="wz-btn secondary" onclick={closeWizard}>Cancel</button>
|
||||
<div class="wz-footer-right">
|
||||
{#if cloning}
|
||||
<span class="wz-cloning">{t('wizard.cloning' as any)}</span>
|
||||
{/if}
|
||||
<button class="wz-btn primary" disabled={!step1Valid() || cloning} onclick={goToStep2}>
|
||||
{t('wizard.next' as any)}
|
||||
</button>
|
||||
{#if cloning}<span class="wz-cloning">Cloning…</span>{/if}
|
||||
<button class="wz-btn primary" disabled={!step1Valid() || cloning} onclick={goToStep2}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Configure -->
|
||||
<div class="wz-body" style:display={step === 2 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">{t('wizard.step2.title' as any)}</h3>
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.name' as any)}</label>
|
||||
<input class="wz-input" type="text" bind:value={projectName} placeholder="my-project" />
|
||||
|
||||
{#if isGitRepo && branches.length > 0}
|
||||
<label class="wz-label">{t('wizard.step2.branch' as any)}</label>
|
||||
<div class="wz-dd" style="position:relative;">
|
||||
<button class="wz-dd-btn" onclick={() => { closeAllDD(); branchDDOpen = !branchDDOpen; }}>
|
||||
<span>{branchLabel}</span><span class="wz-dd-chev">▾</span>
|
||||
</button>
|
||||
<div class="wz-dd-menu" style:display={branchDDOpen ? 'block' : 'none'}>
|
||||
{#each branches as br}
|
||||
<button class="wz-dd-item" class:active={selectedBranch === br}
|
||||
onclick={() => { selectedBranch = br; branchDDOpen = false; }}>{br}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="wz-toggle-row">
|
||||
<input type="checkbox" bind:checked={useWorktrees} />
|
||||
<span>{t('wizard.step2.worktree' as any)}</span>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.group' as any)}</label>
|
||||
<div class="wz-dd" style="position:relative;">
|
||||
<button class="wz-dd-btn" onclick={() => { closeAllDD(); groupDDOpen = !groupDDOpen; }}>
|
||||
<span>{groupLabel}</span><span class="wz-dd-chev">▾</span>
|
||||
</button>
|
||||
<div class="wz-dd-menu" style:display={groupDDOpen ? 'block' : 'none'}>
|
||||
{#each groups as g}
|
||||
<button class="wz-dd-item" class:active={selectedGroupId === g.id}
|
||||
onclick={() => { selectedGroupId = g.id; groupDDOpen = false; }}>{g.name}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.icon' as any)}</label>
|
||||
<div class="wz-emoji-grid">
|
||||
{#each EMOJI_GRID as emoji}
|
||||
<button class="wz-emoji" class:selected={projectIcon === emoji}
|
||||
onclick={() => projectIcon = emoji}>{emoji}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step2.shell' as any)}</label>
|
||||
<div class="wz-dd" style="position:relative;">
|
||||
<button class="wz-dd-btn" onclick={() => { closeAllDD(); shellDDOpen = !shellDDOpen; }}>
|
||||
<span>{shellChoice}</span><span class="wz-dd-chev">▾</span>
|
||||
</button>
|
||||
<div class="wz-dd-menu" style:display={shellDDOpen ? 'block' : 'none'}>
|
||||
{#each SHELLS as sh}
|
||||
<button class="wz-dd-item" class:active={shellChoice === sh}
|
||||
onclick={() => { shellChoice = sh; shellDDOpen = false; }}>{sh}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="wz-step-title">Configure</h3>
|
||||
<WizardStep2 {projectName} {nameError} {selectedBranch} {branches} {useWorktrees} {selectedGroupId} {groups} {projectIcon} {projectColor} {shellChoice} {isGitRepo} onUpdate={handleUpdate} />
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={goBack}>{t('wizard.back' as any)}</button>
|
||||
<button class="wz-btn primary" onclick={goToStep3}>{t('wizard.next' as any)}</button>
|
||||
<button class="wz-btn secondary" onclick={goBack}>Back</button>
|
||||
<button class="wz-btn primary" disabled={!!nameError} onclick={goToStep3}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Agent -->
|
||||
<div class="wz-body" style:display={step === 3 ? 'flex' : 'none'}>
|
||||
<h3 class="wz-step-title">{t('wizard.step3.title' as any)}</h3>
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.provider' as any)}</label>
|
||||
<div class="wz-segmented">
|
||||
{#each (['claude', 'codex', 'ollama'] as const) as prov}
|
||||
<button class="wz-seg-btn" class:active={provider === prov}
|
||||
onclick={() => provider = prov}>{prov}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.model' as any)}</label>
|
||||
<input class="wz-input" type="text" bind:value={model}
|
||||
placeholder={provider === 'claude' ? 'claude-sonnet-4-20250514' : provider === 'codex' ? 'gpt-5.4' : 'qwen3:8b'} />
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.permission' as any)}</label>
|
||||
<div class="wz-segmented">
|
||||
{#each ['restricted', 'default', 'bypassPermissions'] as pm}
|
||||
<button class="wz-seg-btn" class:active={permissionMode === pm}
|
||||
onclick={() => permissionMode = pm}>{pm}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="wz-label">{t('wizard.step3.systemPrompt' as any)}</label>
|
||||
<textarea class="wz-textarea" bind:value={systemPrompt} rows="3"
|
||||
placeholder="Optional system instructions..."></textarea>
|
||||
|
||||
<label class="wz-toggle-row">
|
||||
<input type="checkbox" bind:checked={autoStart} />
|
||||
<span>{t('wizard.step3.autoStart' as any)}</span>
|
||||
</label>
|
||||
|
||||
<h3 class="wz-step-title">Agent</h3>
|
||||
<WizardStep3 {provider} {model} {permissionMode} {systemPrompt} {autoStart} {detectedProviders} {providerModels} {modelsLoading} onUpdate={handleUpdate} />
|
||||
<div class="wz-footer">
|
||||
<button class="wz-btn secondary" onclick={goBack}>{t('wizard.back' as any)}</button>
|
||||
<button class="wz-btn secondary" onclick={goBack}>Back</button>
|
||||
<div class="wz-footer-right">
|
||||
<button class="wz-btn ghost" onclick={createProject}>{t('wizard.skip' as any)}</button>
|
||||
<button class="wz-btn primary" onclick={createProject}>{t('wizard.create' as any)}</button>
|
||||
<button class="wz-btn ghost" onclick={createProject}>Skip</button>
|
||||
<button class="wz-btn primary" onclick={createProject}>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -482,241 +331,27 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.wizard-overlay {
|
||||
position: fixed; inset: 0; z-index: 100;
|
||||
background: color-mix(in srgb, var(--ctp-crust) 70%, transparent);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
.wizard-panel {
|
||||
width: min(32rem, 90vw);
|
||||
max-height: 85vh;
|
||||
background: var(--ctp-base);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.75rem;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1rem 3rem color-mix(in srgb, var(--ctp-crust) 50%, transparent);
|
||||
}
|
||||
|
||||
.wz-header {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--ctp-surface0);
|
||||
}
|
||||
|
||||
.wizard-overlay { position: fixed; inset: 0; z-index: 100; background: color-mix(in srgb, var(--ctp-crust) 70%, transparent); display: flex; align-items: center; justify-content: center; }
|
||||
.wizard-panel { width: min(34rem, 90vw); max-height: 85vh; background: var(--ctp-base); border: 1px solid var(--ctp-surface1); border-radius: 0.75rem; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 1rem 3rem color-mix(in srgb, var(--ctp-crust) 50%, transparent); }
|
||||
.wz-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--ctp-surface0); }
|
||||
.wz-title { font-size: 1rem; font-weight: 700; color: var(--ctp-text); margin: 0; flex: 1; }
|
||||
|
||||
.wz-steps { display: flex; gap: 0.375rem; }
|
||||
|
||||
.wz-step-dot {
|
||||
width: 1.5rem; height: 1.5rem; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.6875rem; font-weight: 600;
|
||||
background: var(--ctp-surface0); color: var(--ctp-subtext0);
|
||||
border: 1.5px solid var(--ctp-surface1);
|
||||
}
|
||||
.wz-step-dot { width: 1.5rem; height: 1.5rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.6875rem; font-weight: 600; background: var(--ctp-surface0); color: var(--ctp-subtext0); border: 1.5px solid var(--ctp-surface1); }
|
||||
.wz-step-dot.active { background: color-mix(in srgb, var(--ctp-blue) 20%, transparent); border-color: var(--ctp-blue); color: var(--ctp-blue); }
|
||||
.wz-step-dot.done { background: color-mix(in srgb, var(--ctp-green) 20%, transparent); border-color: var(--ctp-green); color: var(--ctp-green); }
|
||||
|
||||
.wz-close {
|
||||
background: none; border: none; color: var(--ctp-overlay1); cursor: pointer;
|
||||
font-size: 0.875rem; padding: 0.25rem; border-radius: 0.25rem;
|
||||
}
|
||||
.wz-close { background: none; border: none; color: var(--ctp-overlay1); cursor: pointer; font-size: 0.875rem; padding: 0.25rem; border-radius: 0.25rem; }
|
||||
.wz-close:hover { color: var(--ctp-text); background: var(--ctp-surface0); }
|
||||
|
||||
.wz-body {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
flex-direction: column; gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.wz-body { flex: 1; min-height: 0; overflow-y: auto; flex-direction: column; gap: 0.5rem; padding: 1rem; }
|
||||
.wz-step-title { font-size: 0.875rem; font-weight: 600; color: var(--ctp-text); margin: 0 0 0.25rem; }
|
||||
|
||||
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); margin-top: 0.25rem; }
|
||||
|
||||
.wz-input, .wz-select, .wz-textarea {
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; color: var(--ctp-text);
|
||||
font-size: 0.8125rem; font-family: var(--ui-font-family);
|
||||
width: 100%;
|
||||
}
|
||||
.wz-input:focus, .wz-select:focus, .wz-textarea:focus { outline: none; border-color: var(--ctp-blue); }
|
||||
.wz-input::placeholder, .wz-textarea::placeholder { color: var(--ctp-overlay0); }
|
||||
.wz-textarea { resize: vertical; min-height: 3rem; }
|
||||
/* Custom themed dropdown */
|
||||
.wz-dd { width: 100%; }
|
||||
.wz-dd-btn {
|
||||
width: 100%; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.375rem 0.5rem; background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
|
||||
color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family);
|
||||
cursor: pointer; text-align: left;
|
||||
}
|
||||
.wz-dd-btn:hover { border-color: var(--ctp-surface2); }
|
||||
.wz-dd-chev { color: var(--ctp-overlay1); font-size: 0.75rem; }
|
||||
.wz-dd-menu {
|
||||
position: absolute; top: 100%; left: 0; right: 0; z-index: 20;
|
||||
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; margin-top: 0.125rem;
|
||||
max-height: 10rem; overflow-y: auto;
|
||||
box-shadow: 0 0.25rem 0.75rem color-mix(in srgb, var(--ctp-crust) 50%, transparent);
|
||||
}
|
||||
.wz-dd-item {
|
||||
width: 100%; padding: 0.3125rem 0.5rem; background: none; border: none;
|
||||
color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family);
|
||||
cursor: pointer; text-align: left;
|
||||
}
|
||||
.wz-dd-item:hover { background: var(--ctp-surface0); }
|
||||
.wz-dd-item.active { color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); }
|
||||
|
||||
/* Themed checkboxes */
|
||||
input[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 1rem; height: 1rem;
|
||||
border: 1px solid var(--ctp-surface2);
|
||||
border-radius: 0.1875rem;
|
||||
background: var(--ctp-surface0);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
input[type="checkbox"]:checked {
|
||||
background: var(--ctp-blue);
|
||||
border-color: var(--ctp-blue);
|
||||
}
|
||||
input[type="checkbox"]:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.25rem; top: 0.0625rem;
|
||||
width: 0.3125rem; height: 0.5625rem;
|
||||
border: solid var(--ctp-base);
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
input[type="checkbox"]:focus-visible {
|
||||
outline: 2px solid var(--ctp-blue);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.wz-radios { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
||||
|
||||
.wz-radio {
|
||||
display: flex; align-items: center; gap: 0.375rem;
|
||||
padding: 0.375rem 0.625rem; border-radius: 0.375rem;
|
||||
border: 1px solid var(--ctp-surface1); cursor: pointer;
|
||||
font-size: 0.75rem; color: var(--ctp-subtext0);
|
||||
transition: border-color 0.12s, background 0.12s;
|
||||
}
|
||||
.wz-radio:hover { border-color: var(--ctp-overlay1); color: var(--ctp-text); }
|
||||
.wz-radio.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); color: var(--ctp-text); }
|
||||
.wz-radio input[type="radio"] { display: none; }
|
||||
.wz-radio-icon { font-size: 1rem; }
|
||||
|
||||
.wz-path-row { display: flex; gap: 0.375rem; align-items: center; }
|
||||
.wz-path-row .wz-input { flex: 1; }
|
||||
|
||||
.wz-browse-btn {
|
||||
padding: 0.375rem; background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
|
||||
cursor: pointer; font-size: 0.875rem; flex-shrink: 0;
|
||||
}
|
||||
.wz-browse-btn:hover { background: var(--ctp-surface1); }
|
||||
|
||||
.wz-validation { font-size: 0.875rem; font-weight: 700; flex-shrink: 0; }
|
||||
|
||||
.wz-badge {
|
||||
display: inline-flex; align-items: center; gap: 0.25rem;
|
||||
padding: 0.125rem 0.5rem; border-radius: 0.25rem;
|
||||
font-size: 0.6875rem; width: fit-content;
|
||||
}
|
||||
.git-badge {
|
||||
background: color-mix(in srgb, var(--ctp-green) 15%, transparent);
|
||||
color: var(--ctp-green); border: 1px solid color-mix(in srgb, var(--ctp-green) 30%, transparent);
|
||||
}
|
||||
|
||||
.wz-hint { font-size: 0.6875rem; }
|
||||
.wz-hint.error { color: var(--ctp-red); }
|
||||
.wz-hint.warn { color: var(--ctp-peach); }
|
||||
|
||||
.wz-source-fields { display: flex; flex-direction: column; gap: 0.375rem; margin-top: 0.25rem; }
|
||||
|
||||
.wz-template-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.375rem; }
|
||||
|
||||
.wz-template-card {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
|
||||
padding: 0.75rem 0.5rem; border-radius: 0.375rem;
|
||||
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
|
||||
cursor: pointer; text-align: center;
|
||||
}
|
||||
.wz-template-card:hover { border-color: var(--ctp-overlay1); }
|
||||
.wz-template-card.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); }
|
||||
.wz-template-icon { font-size: 1.5rem; }
|
||||
.wz-template-name { font-size: 0.75rem; font-weight: 600; color: var(--ctp-text); }
|
||||
.wz-template-desc { font-size: 0.625rem; color: var(--ctp-subtext0); }
|
||||
|
||||
.wz-emoji-grid { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
|
||||
.wz-emoji {
|
||||
width: 2rem; height: 2rem; border-radius: 0.25rem;
|
||||
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
|
||||
cursor: pointer; font-size: 1rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.wz-emoji:hover { border-color: var(--ctp-overlay1); }
|
||||
.wz-emoji.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 15%, transparent); }
|
||||
|
||||
.wz-toggle-row {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
font-size: 0.8125rem; color: var(--ctp-text); cursor: pointer;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.wz-segmented { display: flex; gap: 0; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); }
|
||||
|
||||
.wz-seg-btn {
|
||||
flex: 1; padding: 0.375rem 0.5rem; font-size: 0.75rem;
|
||||
background: var(--ctp-surface0); border: none; color: var(--ctp-subtext0);
|
||||
cursor: pointer; font-family: var(--ui-font-family);
|
||||
border-right: 1px solid var(--ctp-surface1);
|
||||
}
|
||||
.wz-seg-btn:last-child { border-right: none; }
|
||||
.wz-seg-btn:hover { color: var(--ctp-text); }
|
||||
.wz-seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, transparent); color: var(--ctp-blue); }
|
||||
|
||||
.wz-footer {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding-top: 0.75rem; margin-top: auto;
|
||||
border-top: 1px solid var(--ctp-surface0);
|
||||
}
|
||||
|
||||
.wz-footer { display: flex; align-items: center; justify-content: space-between; padding-top: 0.75rem; margin-top: auto; border-top: 1px solid var(--ctp-surface0); }
|
||||
.wz-footer-right { display: flex; gap: 0.375rem; align-items: center; }
|
||||
|
||||
.wz-cloning { font-size: 0.75rem; color: var(--ctp-blue); font-style: italic; }
|
||||
|
||||
.wz-btn {
|
||||
padding: 0.375rem 0.75rem; border-radius: 0.25rem;
|
||||
font-size: 0.8125rem; font-weight: 500; cursor: pointer;
|
||||
font-family: var(--ui-font-family); border: 1px solid transparent;
|
||||
}
|
||||
.wz-btn { padding: 0.375rem 0.75rem; border-radius: 0.25rem; font-size: 0.8125rem; font-weight: 500; cursor: pointer; font-family: var(--ui-font-family); border: 1px solid transparent; }
|
||||
.wz-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.wz-btn.primary {
|
||||
background: color-mix(in srgb, var(--ctp-blue) 20%, transparent);
|
||||
border-color: var(--ctp-blue); color: var(--ctp-blue);
|
||||
}
|
||||
.wz-btn.primary { background: color-mix(in srgb, var(--ctp-blue) 20%, transparent); border-color: var(--ctp-blue); color: var(--ctp-blue); }
|
||||
.wz-btn.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--ctp-blue) 35%, transparent); }
|
||||
|
||||
.wz-btn.secondary {
|
||||
background: transparent; border-color: var(--ctp-surface1); color: var(--ctp-subtext0);
|
||||
}
|
||||
.wz-btn.secondary { background: transparent; border-color: var(--ctp-surface1); color: var(--ctp-subtext0); }
|
||||
.wz-btn.secondary:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
|
||||
|
||||
.wz-btn.ghost {
|
||||
background: transparent; border: none; color: var(--ctp-subtext0);
|
||||
}
|
||||
.wz-btn.ghost { background: transparent; border: none; color: var(--ctp-subtext0); }
|
||||
.wz-btn.ghost:hover { color: var(--ctp-text); }
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue