fix(electrobun): wizard 7 fixes — validation, GitLab, SSHFS, icons, model dropdown, keyboard nav
- Git Platform: validates repo via git.probe before enabling Next, supports GitHub + GitLab + any git URL - Template dir configurable in Advanced Settings (template_dir key) - SSHFS: checks sshfs availability, mountpoint selector when enabled - CustomDropdown: flip-up when insufficient space below - 50 Lucide icons (was 24) with categories (AI, Data, DevOps, Security, Media, Comms) - Model: CustomDropdown from live API, max_tokens as slider, effort only with adaptive thinking - Keyboard: Escape closes wizard, Tab navigation with :focus-visible rings, source cards navigable
This commit is contained in:
parent
41b8d46a19
commit
021feba3ed
11 changed files with 368 additions and 614 deletions
|
|
@ -1,113 +1,79 @@
|
|||
<script lang="ts">
|
||||
import { t } from './i18n.svelte.ts';
|
||||
import { onMount } from 'svelte';
|
||||
import { appRpc } from './rpc.ts';
|
||||
import { sanitize, sanitizeUrl, sanitizePath, isValidGitUrl, isValidGithubRepo } from './sanitize.ts';
|
||||
import PathBrowser from './PathBrowser.svelte';
|
||||
import CustomCheckbox from './ui/CustomCheckbox.svelte';
|
||||
|
||||
type SourceType = 'local' | 'git-clone' | 'github' | 'template' | 'remote';
|
||||
type AuthMethod = 'password' | 'key' | 'agent' | 'config';
|
||||
type PathState = 'idle' | 'checking' | 'valid' | 'invalid' | 'not-dir';
|
||||
|
||||
interface Props {
|
||||
sourceType: SourceType;
|
||||
localPath: string;
|
||||
repoUrl: string;
|
||||
cloneTarget: string;
|
||||
githubRepo: string;
|
||||
selectedTemplate: string;
|
||||
remoteHost: string;
|
||||
remoteUser: string;
|
||||
remotePath: string;
|
||||
remoteAuthMethod: AuthMethod;
|
||||
remotePassword: string;
|
||||
remoteKeyPath: string;
|
||||
remoteSshfs: boolean;
|
||||
pathValid: PathState;
|
||||
isGitRepo: boolean;
|
||||
gitBranch: string;
|
||||
gitProbeStatus: 'idle' | 'probing' | 'ok' | 'error';
|
||||
gitProbeBranches: string[];
|
||||
sourceType: SourceType; localPath: string; repoUrl: string; cloneTarget: string;
|
||||
githubRepo: string; selectedTemplate: string; remoteHost: string; remoteUser: string;
|
||||
remotePath: string; remoteAuthMethod: AuthMethod; remotePassword: string;
|
||||
remoteKeyPath: string; remoteSshfs: boolean; remoteSshfsMountpoint: string;
|
||||
pathValid: PathState; isGitRepo: boolean; gitBranch: string;
|
||||
gitProbeStatus: 'idle' | 'probing' | 'ok' | 'error'; gitProbeBranches: string[];
|
||||
githubInfo: { stars: number; description: string; defaultBranch: string } | null;
|
||||
githubLoading: boolean;
|
||||
cloning: boolean;
|
||||
templates: Array<{ id: string; name: string; description: string; icon: string }>;
|
||||
templateTargetDir: string;
|
||||
githubProbeStatus: 'idle' | 'probing' | 'ok' | 'error'; githubLoading: boolean;
|
||||
cloning: boolean; templates: Array<{ id: string; name: string; description: string; icon: string }>;
|
||||
templateTargetDir: string; templateOriginDir: string;
|
||||
onUpdate: (field: string, value: unknown) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
sourceType, localPath, repoUrl, cloneTarget, githubRepo,
|
||||
selectedTemplate, remoteHost, remoteUser, remotePath,
|
||||
remoteAuthMethod, remotePassword, remoteKeyPath, remoteSshfs,
|
||||
pathValid, isGitRepo, gitBranch,
|
||||
gitProbeStatus, gitProbeBranches, githubInfo, githubLoading,
|
||||
cloning, templates, templateTargetDir, onUpdate,
|
||||
sourceType, localPath, repoUrl, cloneTarget, githubRepo, selectedTemplate,
|
||||
remoteHost, remoteUser, remotePath, remoteAuthMethod, remotePassword,
|
||||
remoteKeyPath, remoteSshfs, remoteSshfsMountpoint = '', pathValid, isGitRepo,
|
||||
gitBranch, gitProbeStatus, gitProbeBranches, githubInfo,
|
||||
githubProbeStatus = 'idle', githubLoading, cloning, templates,
|
||||
templateTargetDir, templateOriginDir = '', onUpdate,
|
||||
}: Props = $props();
|
||||
|
||||
let showBrowser = $state(false);
|
||||
let showCloneBrowser = $state(false);
|
||||
let showTemplateBrowser = $state(false);
|
||||
let showKeyBrowser = $state(false);
|
||||
let showMountBrowser = $state(false);
|
||||
let sshfsInstalled = $state<boolean | null>(null);
|
||||
let firstInput = $state<HTMLInputElement | null>(null);
|
||||
|
||||
const SOURCE_TYPES: Array<{ value: SourceType; label: string; icon: string }> = [
|
||||
{ value: 'local', label: 'Local Folder', icon: 'folder' },
|
||||
{ value: 'git-clone', label: 'Git Clone', icon: 'git-branch' },
|
||||
{ value: 'github', label: 'GitHub Repo', icon: 'github' },
|
||||
{ value: 'template', label: 'Template', icon: 'layout-template' },
|
||||
{ value: 'remote', label: 'SSH Remote', icon: 'monitor' },
|
||||
const SOURCE_TYPES: Array<{ value: SourceType; label: string }> = [
|
||||
{ value: 'local', label: 'Local Folder' }, { value: 'git-clone', label: 'Git Clone' },
|
||||
{ value: 'github', label: 'Git Platform' }, { value: 'template', label: 'Template' },
|
||||
{ value: 'remote', label: 'SSH Remote' },
|
||||
];
|
||||
|
||||
const AUTH_METHODS: Array<{ value: AuthMethod; label: string }> = [
|
||||
{ value: 'password', label: 'Password' },
|
||||
{ value: 'key', label: 'SSH Key' },
|
||||
{ value: 'agent', label: 'SSH Agent' },
|
||||
{ value: 'config', label: 'SSH Config' },
|
||||
{ value: 'password', label: 'Password' }, { value: 'key', label: 'SSH Key' },
|
||||
{ value: 'agent', label: 'SSH Agent' }, { value: 'config', label: 'SSH Config' },
|
||||
];
|
||||
|
||||
function validationIcon(state: PathState): string {
|
||||
switch (state) {
|
||||
case 'valid': return '\u2713';
|
||||
case 'invalid': return '\u2717';
|
||||
case 'not-dir': return '\u26A0';
|
||||
case 'checking': return '\u2026';
|
||||
default: return '';
|
||||
}
|
||||
export function focusFirst() { firstInput?.focus(); }
|
||||
|
||||
onMount(async () => {
|
||||
try { const r = await appRpc.request['ssh.checkSshfs']({}); sshfsInstalled = r?.installed ?? false; }
|
||||
catch { sshfsInstalled = false; }
|
||||
});
|
||||
|
||||
function vIcon(s: PathState) { return s === 'valid' ? '\u2713' : s === 'invalid' ? '\u2717' : s === 'not-dir' ? '\u26A0' : s === 'checking' ? '\u2026' : ''; }
|
||||
function vColor(s: PathState) { return s === 'valid' ? 'var(--ctp-green)' : s === 'invalid' ? 'var(--ctp-red)' : s === 'not-dir' ? 'var(--ctp-peach)' : 'var(--ctp-overlay0)'; }
|
||||
|
||||
async function browse(target: string) {
|
||||
const starts: Record<string, string> = { local: localPath, clone: cloneTarget, template: templateTargetDir, key: remoteKeyPath, mount: remoteSshfsMountpoint };
|
||||
const fields: Record<string, string> = { local: 'localPath', clone: 'cloneTarget', template: 'templateTargetDir', key: 'remoteKeyPath', mount: 'remoteSshfsMountpoint' };
|
||||
try { const r = await appRpc.request['files.pickDirectory']({ startingFolder: starts[target] || '~/' }); if (r?.path) onUpdate(fields[target], r.path); } catch { /* no-op */ }
|
||||
}
|
||||
|
||||
function validationColor(state: PathState): 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)';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNativeBrowse(target: 'local' | 'clone' | 'template' | 'key') {
|
||||
try {
|
||||
const start = target === 'local' ? localPath : target === 'clone' ? cloneTarget : target === 'template' ? templateTargetDir : remoteKeyPath;
|
||||
const result = await appRpc.request['files.pickDirectory']({ startingFolder: start || '~/' });
|
||||
if (result?.path) {
|
||||
const field = target === 'local' ? 'localPath' : target === 'clone' ? 'cloneTarget' : target === 'template' ? 'templateTargetDir' : 'remoteKeyPath';
|
||||
onUpdate(field, result.path);
|
||||
}
|
||||
} catch { /* native dialog not available */ }
|
||||
}
|
||||
|
||||
function handleBrowserSelect(field: string, path: string) {
|
||||
onUpdate(field, path);
|
||||
showBrowser = false;
|
||||
showCloneBrowser = false;
|
||||
showTemplateBrowser = false;
|
||||
showKeyBrowser = false;
|
||||
}
|
||||
function closeBrowsers() { showBrowser = false; showMountBrowser = false; }
|
||||
function selectBrowser(field: string, path: string) { onUpdate(field, path); closeBrowsers(); }
|
||||
function srcKey(e: KeyboardEvent, v: SourceType) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onUpdate('sourceType', v); } }
|
||||
</script>
|
||||
|
||||
<div class="wz-radios">
|
||||
{#each SOURCE_TYPES as opt}
|
||||
<label class="wz-radio" class:selected={sourceType === opt.value}>
|
||||
<input type="radio" name="source" value={opt.value}
|
||||
checked={sourceType === opt.value}
|
||||
<label class="wz-radio" class:selected={sourceType === opt.value}
|
||||
tabindex={0} role="radio" aria-checked={sourceType === opt.value}
|
||||
onkeydown={(e) => srcKey(e, opt.value)}>
|
||||
<input type="radio" name="source" value={opt.value} checked={sourceType === opt.value}
|
||||
onchange={() => onUpdate('sourceType', opt.value)} />
|
||||
<span>{opt.label}</span>
|
||||
</label>
|
||||
|
|
@ -115,29 +81,23 @@
|
|||
</div>
|
||||
|
||||
<div class="wz-source-fields">
|
||||
<!-- Local Folder -->
|
||||
<div style:display={sourceType === 'local' ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">Project directory</label>
|
||||
<div class="wz-path-row">
|
||||
<input class="wz-input" type="text" placeholder="/home/user/project"
|
||||
<input class="wz-input" type="text" placeholder="/home/user/project" bind:this={firstInput}
|
||||
value={localPath} oninput={(e) => onUpdate('localPath', (e.target as HTMLInputElement).value)} />
|
||||
<button class="wz-browse-btn" onclick={() => handleNativeBrowse('local')} title="System picker">📂</button>
|
||||
<button class="wz-browse-btn" onclick={() => showBrowser = !showBrowser} title="In-app browser">🔍</button>
|
||||
{#if pathValid !== 'idle'}
|
||||
<span class="wz-validation" style:color={validationColor(pathValid)}>{validationIcon(pathValid)}</span>
|
||||
{/if}
|
||||
<button class="wz-browse-btn" onclick={() => browse('local')}>📂</button>
|
||||
<button class="wz-browse-btn" onclick={() => showBrowser = !showBrowser}>🔍</button>
|
||||
{#if pathValid !== 'idle'}<span class="wz-validation" style:color={vColor(pathValid)}>{vIcon(pathValid)}</span>{/if}
|
||||
</div>
|
||||
<div style:display={showBrowser ? 'block' : 'none'} class="wz-browser-wrap">
|
||||
<PathBrowser onSelect={(p) => handleBrowserSelect('localPath', p)} onClose={() => showBrowser = false} />
|
||||
<PathBrowser onSelect={(p) => selectBrowser('localPath', p)} onClose={() => showBrowser = false} />
|
||||
</div>
|
||||
{#if pathValid === 'valid' && isGitRepo}
|
||||
<span class="wz-badge git-badge">Git repo ({gitBranch})</span>
|
||||
{/if}
|
||||
{#if pathValid === 'valid' && isGitRepo}<span class="wz-badge git-badge">Git repo ({gitBranch})</span>{/if}
|
||||
{#if pathValid === 'invalid'}<span class="wz-hint error">Path does not exist</span>{/if}
|
||||
{#if pathValid === 'not-dir'}<span class="wz-hint warn">Not a directory</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Git Clone -->
|
||||
<div style:display={sourceType === 'git-clone' ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">Repository URL</label>
|
||||
<div class="wz-path-row">
|
||||
|
|
@ -147,23 +107,23 @@
|
|||
{#if gitProbeStatus === 'ok'}<span class="wz-validation" style:color="var(--ctp-green)">✓</span>{/if}
|
||||
{#if gitProbeStatus === 'error'}<span class="wz-validation" style:color="var(--ctp-red)">✗</span>{/if}
|
||||
</div>
|
||||
{#if gitProbeStatus === 'ok' && gitProbeBranches.length > 0}
|
||||
<span class="wz-hint" style="color: var(--ctp-subtext0);">{gitProbeBranches.length} branches found</span>
|
||||
{/if}
|
||||
{#if gitProbeStatus === 'ok' && gitProbeBranches.length > 0}<span class="wz-hint" style="color: var(--ctp-subtext0);">{gitProbeBranches.length} branches found</span>{/if}
|
||||
{#if gitProbeStatus === 'error'}<span class="wz-hint error">Repository not found or inaccessible</span>{/if}
|
||||
<label class="wz-label">Target directory</label>
|
||||
<div class="wz-path-row">
|
||||
<input class="wz-input" type="text" placeholder="~/projects/my-repo"
|
||||
value={cloneTarget} oninput={(e) => onUpdate('cloneTarget', (e.target as HTMLInputElement).value)} />
|
||||
<button class="wz-browse-btn" onclick={() => handleNativeBrowse('clone')}>📂</button>
|
||||
<button class="wz-browse-btn" onclick={() => browse('clone')}>📂</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GitHub -->
|
||||
<div style:display={sourceType === 'github' ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">GitHub repository (owner/repo)</label>
|
||||
<input class="wz-input" type="text" placeholder="owner/repo"
|
||||
<label class="wz-label">Repository (owner/repo or full URL)</label>
|
||||
<input class="wz-input" type="text" placeholder="owner/repo or https://gitlab.com/owner/repo"
|
||||
value={githubRepo} oninput={(e) => onUpdate('githubRepo', (e.target as HTMLInputElement).value)} />
|
||||
{#if githubLoading}<span class="wz-hint" style="color: var(--ctp-blue);">Checking…</span>{/if}
|
||||
{#if githubProbeStatus === 'ok'}<span class="wz-hint valid">Repository verified</span>{/if}
|
||||
{#if githubProbeStatus === 'error'}<span class="wz-hint error">Repository not found or inaccessible</span>{/if}
|
||||
{#if githubInfo}
|
||||
<div class="wz-github-info">
|
||||
<span class="wz-hint" style="color: var(--ctp-yellow);">★ {githubInfo.stars}</span>
|
||||
|
|
@ -173,18 +133,18 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Template -->
|
||||
<div style:display={sourceType === 'template' ? 'flex' : 'none'} class="wz-field-col">
|
||||
{#if templateOriginDir}<span class="wz-hint" style="color: var(--ctp-overlay0);">Templates from: {templateOriginDir}</span>{/if}
|
||||
<label class="wz-label">Target directory</label>
|
||||
<div class="wz-path-row">
|
||||
<input class="wz-input" type="text" placeholder="~/projects"
|
||||
value={templateTargetDir} oninput={(e) => onUpdate('templateTargetDir', (e.target as HTMLInputElement).value)} />
|
||||
<button class="wz-browse-btn" onclick={() => handleNativeBrowse('template')}>📂</button>
|
||||
<button class="wz-browse-btn" onclick={() => browse('template')}>📂</button>
|
||||
</div>
|
||||
<div class="wz-template-grid">
|
||||
{#each templates as tmpl}
|
||||
<button class="wz-template-card" class:selected={selectedTemplate === tmpl.id}
|
||||
onclick={() => onUpdate('selectedTemplate', tmpl.id)}>
|
||||
onclick={() => onUpdate('selectedTemplate', tmpl.id)} tabindex={0}>
|
||||
<span class="wz-template-icon">{tmpl.icon}</span>
|
||||
<span class="wz-template-name">{tmpl.name}</span>
|
||||
<span class="wz-template-desc">{tmpl.description}</span>
|
||||
|
|
@ -193,80 +153,74 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH Remote -->
|
||||
<div style:display={sourceType === 'remote' ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">Host</label>
|
||||
<input class="wz-input" type="text" placeholder="192.168.1.100"
|
||||
value={remoteHost} oninput={(e) => onUpdate('remoteHost', (e.target as HTMLInputElement).value)} />
|
||||
<input class="wz-input" type="text" placeholder="192.168.1.100" value={remoteHost} oninput={(e) => onUpdate('remoteHost', (e.target as HTMLInputElement).value)} />
|
||||
<label class="wz-label">User</label>
|
||||
<input class="wz-input" type="text" placeholder="user"
|
||||
value={remoteUser} oninput={(e) => onUpdate('remoteUser', (e.target as HTMLInputElement).value)} />
|
||||
<input class="wz-input" type="text" placeholder="user" value={remoteUser} oninput={(e) => onUpdate('remoteUser', (e.target as HTMLInputElement).value)} />
|
||||
<label class="wz-label">Remote path</label>
|
||||
<input class="wz-input" type="text" placeholder="/home/user/project"
|
||||
value={remotePath} oninput={(e) => onUpdate('remotePath', (e.target as HTMLInputElement).value)} />
|
||||
|
||||
<input class="wz-input" type="text" placeholder="/home/user/project" value={remotePath} oninput={(e) => onUpdate('remotePath', (e.target as HTMLInputElement).value)} />
|
||||
<label class="wz-label">Auth method</label>
|
||||
<div class="wz-segmented">
|
||||
{#each AUTH_METHODS as m}
|
||||
<button class="wz-seg-btn" class:active={remoteAuthMethod === m.value}
|
||||
onclick={() => onUpdate('remoteAuthMethod', m.value)}>{m.label}</button>
|
||||
{/each}
|
||||
{#each AUTH_METHODS as m}<button class="wz-seg-btn" class:active={remoteAuthMethod === m.value} onclick={() => onUpdate('remoteAuthMethod', m.value)}>{m.label}</button>{/each}
|
||||
</div>
|
||||
|
||||
<div style:display={remoteAuthMethod === 'password' ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">Password</label>
|
||||
<input class="wz-input" type="password" placeholder="••••••"
|
||||
value={remotePassword} oninput={(e) => onUpdate('remotePassword', (e.target as HTMLInputElement).value)} />
|
||||
<input class="wz-input" type="password" placeholder="••••••" value={remotePassword} oninput={(e) => onUpdate('remotePassword', (e.target as HTMLInputElement).value)} />
|
||||
</div>
|
||||
<div style:display={remoteAuthMethod === 'key' ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">Key file path</label>
|
||||
<div class="wz-path-row">
|
||||
<input class="wz-input" type="text" placeholder="~/.ssh/id_ed25519"
|
||||
value={remoteKeyPath} oninput={(e) => onUpdate('remoteKeyPath', (e.target as HTMLInputElement).value)} />
|
||||
<button class="wz-browse-btn" onclick={() => handleNativeBrowse('key')}>📂</button>
|
||||
<input class="wz-input" type="text" placeholder="~/.ssh/id_ed25519" value={remoteKeyPath} oninput={(e) => onUpdate('remoteKeyPath', (e.target as HTMLInputElement).value)} />
|
||||
<button class="wz-browse-btn" onclick={() => browse('key')}>📂</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if sshfsInstalled === false}<span class="wz-hint warn">sshfs not installed -- mount option unavailable</span>{/if}
|
||||
<CustomCheckbox checked={remoteSshfs} label="Mount via SSHFS" disabled={sshfsInstalled === false} onChange={v => onUpdate('remoteSshfs', v)} />
|
||||
<div style:display={remoteSshfs && sshfsInstalled ? 'flex' : 'none'} class="wz-field-col">
|
||||
<label class="wz-label">Local mountpoint</label>
|
||||
<div class="wz-path-row">
|
||||
<input class="wz-input" type="text" placeholder="/mnt/remote-project" value={remoteSshfsMountpoint} oninput={(e) => onUpdate('remoteSshfsMountpoint', (e.target as HTMLInputElement).value)} />
|
||||
<button class="wz-browse-btn" onclick={() => browse('mount')}>📂</button>
|
||||
<button class="wz-browse-btn" onclick={() => showMountBrowser = !showMountBrowser}>🔍</button>
|
||||
</div>
|
||||
<div style:display={showMountBrowser ? 'block' : 'none'} class="wz-browser-wrap">
|
||||
<PathBrowser onSelect={(p) => selectBrowser('remoteSshfsMountpoint', p)} onClose={() => showMountBrowser = false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="wz-toggle-row">
|
||||
<input type="checkbox" checked={remoteSshfs}
|
||||
onchange={(e) => onUpdate('remoteSshfs', (e.target as HTMLInputElement).checked)} />
|
||||
<span>Mount via SSHFS</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.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 { 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:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -1px; }
|
||||
.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-source-fields { display: flex; flex-direction: column; gap: 0.375rem; margin-top: 0.25rem; }
|
||||
.wz-field-col { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); }
|
||||
.wz-input { 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 { outline: none; border-color: var(--ctp-blue); }
|
||||
.wz-input:focus { outline: 2px solid var(--ctp-blue); outline-offset: -1px; border-color: var(--ctp-blue); }
|
||||
.wz-input::placeholder { color: var(--ctp-overlay0); }
|
||||
.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-browse-btn:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -1px; }
|
||||
.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-hint.valid { color: var(--ctp-green); }
|
||||
.wz-browser-wrap { max-height: 16rem; max-width: 32rem; overflow-y: auto; border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; margin-top: 0.5rem; }
|
||||
.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:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -1px; }
|
||||
.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); }
|
||||
|
|
@ -275,10 +229,7 @@
|
|||
.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:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -2px; }
|
||||
.wz-seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, transparent); color: var(--ctp-blue); }
|
||||
.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-github-info { display: flex; flex-direction: column; gap: 0.125rem; margin-top: 0.25rem; }
|
||||
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); }
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue