- Git probe tries GitHub then GitLab for owner/repo shorthand - Shows "Found on GitHub/GitLab" with platform indicator - system.shells RPC detects installed shells (bash/zsh/fish/sh/dash) - CustomDropdown flip logic uses 200px threshold for flip-up - Project creation properly persists all wizard fields + adds card
158 lines
6.3 KiB
Svelte
158 lines
6.3 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { PROJECT_ICONS, ACCENT_COLORS } from './wizard-icons.ts';
|
|
import {
|
|
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
|
Rocket, Bug, Puzzle, Box, Layers, GitBranch, Wifi, Lock,
|
|
FlaskConical, Sparkles, FileCode, Wrench, Folder, Bot, Cloud, HardDrive,
|
|
Brain, BrainCircuit, Wand2, Table, BarChart3, Container, Activity,
|
|
Settings, Cog, Key, Fingerprint, ShieldCheck, Image, Video, Music,
|
|
Camera, Palette, MessageCircle, Mail, Phone, Radio, Send,
|
|
Gamepad2, BookOpen, Blocks, Leaf,
|
|
} from 'lucide-svelte';
|
|
import CustomDropdown from './ui/CustomDropdown.svelte';
|
|
import CustomCheckbox from './ui/CustomCheckbox.svelte';
|
|
import { appRpc } from './rpc.ts';
|
|
|
|
interface Props {
|
|
projectName: string;
|
|
nameError: string;
|
|
selectedBranch: string;
|
|
branches: string[];
|
|
useWorktrees: boolean;
|
|
selectedGroupId: string;
|
|
groups: Array<{ id: string; name: string }>;
|
|
projectIcon: string;
|
|
projectColor: string;
|
|
shellChoice: string;
|
|
isGitRepo: boolean;
|
|
onUpdate: (field: string, value: unknown) => void;
|
|
}
|
|
|
|
let {
|
|
projectName, nameError, selectedBranch, branches, useWorktrees,
|
|
selectedGroupId, groups, projectIcon, projectColor, shellChoice,
|
|
isGitRepo, onUpdate,
|
|
}: Props = $props();
|
|
|
|
let detectedShells = $state<Array<{ path: string; name: string }>>([]);
|
|
const FALLBACK_SHELLS = ['bash', 'zsh', 'fish', 'sh'];
|
|
|
|
onMount(async () => {
|
|
try {
|
|
const r = await appRpc.request['system.shells']({});
|
|
if (r?.shells?.length) {
|
|
detectedShells = r.shells;
|
|
// If current shellChoice is not in detected shells, select the first available
|
|
if (!r.shells.some(s => s.name === shellChoice)) {
|
|
onUpdate('shellChoice', r.shells[0].name);
|
|
}
|
|
}
|
|
} catch {
|
|
// Fallback — use hardcoded list
|
|
}
|
|
});
|
|
|
|
let shellItems = $derived(
|
|
detectedShells.length > 0
|
|
? detectedShells.map(s => ({ value: s.name, label: `${s.name} (${s.path})` }))
|
|
: FALLBACK_SHELLS.map(sh => ({ value: sh, label: sh }))
|
|
);
|
|
|
|
const ICON_MAP: Record<string, typeof Terminal> = {
|
|
Terminal, Server, Globe, Code, Database, Cpu, Zap, Shield,
|
|
Rocket, Bug, Puzzle, Box, Layers, GitBranch, Wifi, Lock,
|
|
FlaskConical, Sparkles, FileCode, Wrench, Folder, Bot, Cloud, HardDrive,
|
|
Brain, BrainCircuit, Wand2, Table, BarChart3, Container, Activity,
|
|
Settings, Cog, Key, Fingerprint, ShieldCheck, Image, Video, Music,
|
|
Camera, Palette, MessageCircle, Mail, Phone, Radio, Send,
|
|
Gamepad2, BookOpen, Blocks, Leaf,
|
|
};
|
|
|
|
let firstInput = $state<HTMLInputElement | null>(null);
|
|
|
|
export function focusFirst() {
|
|
firstInput?.focus();
|
|
}
|
|
</script>
|
|
|
|
<label class="wz-label">Project name</label>
|
|
<input class="wz-input" class:error={!!nameError} type="text"
|
|
bind:this={firstInput}
|
|
value={projectName} oninput={(e) => onUpdate('projectName', (e.target as HTMLInputElement).value)}
|
|
placeholder="my-project" />
|
|
{#if nameError}<span class="wz-hint error">{nameError}</span>{/if}
|
|
|
|
{#if isGitRepo && branches.length > 0}
|
|
<label class="wz-label">Branch</label>
|
|
<CustomDropdown
|
|
items={branches.map(br => ({ value: br, label: br }))}
|
|
selected={selectedBranch}
|
|
placeholder="main"
|
|
onSelect={v => onUpdate('selectedBranch', v)}
|
|
/>
|
|
<CustomCheckbox
|
|
checked={useWorktrees}
|
|
label="Use worktrees"
|
|
onChange={v => onUpdate('useWorktrees', v)}
|
|
/>
|
|
{/if}
|
|
|
|
<label class="wz-label">Group</label>
|
|
<CustomDropdown
|
|
items={groups.map(g => ({ value: g.id, label: g.name }))}
|
|
selected={selectedGroupId}
|
|
placeholder="Select group"
|
|
onSelect={v => onUpdate('selectedGroupId', v)}
|
|
/>
|
|
|
|
<label class="wz-label">Icon</label>
|
|
<div class="wz-icon-grid">
|
|
{#each PROJECT_ICONS as ic}
|
|
<button class="wz-icon-btn" class:selected={projectIcon === ic.name}
|
|
onclick={() => onUpdate('projectIcon', ic.name)} title={ic.label}
|
|
tabindex={0}>
|
|
{#if ICON_MAP[ic.name]}
|
|
<svelte:component this={ICON_MAP[ic.name]} size={16} />
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<label class="wz-label">Color</label>
|
|
<div class="wz-color-grid">
|
|
{#each ACCENT_COLORS as c}
|
|
<button class="wz-color-dot" class:selected={projectColor === `var(${c.var})`}
|
|
style:background={`var(${c.var})`}
|
|
onclick={() => onUpdate('projectColor', `var(${c.var})`)}
|
|
title={c.name}
|
|
tabindex={0}></button>
|
|
{/each}
|
|
</div>
|
|
|
|
<label class="wz-label">Shell</label>
|
|
<CustomDropdown
|
|
items={shellItems}
|
|
selected={shellChoice}
|
|
onSelect={v => onUpdate('shellChoice', v)}
|
|
/>
|
|
|
|
<style>
|
|
.wz-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); margin-top: 0.25rem; }
|
|
.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: 2px solid var(--ctp-blue); outline-offset: -1px; border-color: var(--ctp-blue); }
|
|
.wz-input.error { border-color: var(--ctp-red); }
|
|
.wz-input::placeholder { color: var(--ctp-overlay0); }
|
|
.wz-hint { font-size: 0.6875rem; }
|
|
.wz-hint.error { color: var(--ctp-red); }
|
|
.wz-icon-grid { display: flex; flex-wrap: wrap; gap: 0.25rem; max-height: 8rem; overflow-y: auto; }
|
|
.wz-icon-btn { width: 2rem; height: 2rem; border-radius: 0.25rem; border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--ctp-subtext0); }
|
|
.wz-icon-btn:hover { border-color: var(--ctp-overlay1); color: var(--ctp-text); }
|
|
.wz-icon-btn:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -1px; }
|
|
.wz-icon-btn.selected { border-color: var(--ctp-blue); background: color-mix(in srgb, var(--ctp-blue) 15%, transparent); color: var(--ctp-blue); }
|
|
.wz-color-grid { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
|
.wz-color-dot { width: 1.5rem; height: 1.5rem; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: border-color 0.12s, transform 0.12s; }
|
|
.wz-color-dot:hover { transform: scale(1.15); }
|
|
.wz-color-dot:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: 2px; }
|
|
.wz-color-dot.selected { border-color: var(--ctp-text); box-shadow: 0 0 0 1px var(--ctp-surface0); }
|
|
</style>
|