初始化项目启动器代码

This commit is contained in:
xintaofei
2026-03-27 13:05:27 +08:00
parent 77204e2295
commit 7c89e150f9
25 changed files with 1434 additions and 15 deletions

View File

@@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "push-*", "settings"],
"windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "push-*", "settings", "project-boot"],
"permissions": [
"core:default",
"core:window:default",

View File

@@ -9,7 +9,8 @@
"welcome",
"folder-*",
"commit-*",
"settings"
"settings",
"project-boot"
],
"permissions": [
"window-state:default"

View File

@@ -3,6 +3,7 @@ pub mod conversations;
pub mod folder_commands;
pub mod folders;
pub mod mcp;
pub mod project_boot;
pub mod system_settings;
pub mod terminal;
pub mod version_control;

View File

@@ -0,0 +1,92 @@
use std::path::PathBuf;
use crate::app_error::AppCommandError;
#[tauri::command]
pub async fn create_shadcn_project(
project_name: String,
template: String,
preset_code: String,
package_manager: String,
target_dir: String,
) -> Result<String, AppCommandError> {
let project_name = project_name.trim().to_string();
let template = template.trim().to_string();
let preset_code = preset_code.trim().to_string();
let package_manager = package_manager.trim().to_string();
let target_dir = target_dir.trim().to_string();
if project_name.is_empty() {
return Err(AppCommandError::invalid_input("Project name is required"));
}
if template.is_empty() {
return Err(AppCommandError::invalid_input("Template is required"));
}
if target_dir.is_empty() {
return Err(AppCommandError::invalid_input("Target directory is required"));
}
let full_path = PathBuf::from(&target_dir).join(&project_name);
let full_path_str = full_path.to_string_lossy().to_string();
// Check if directory already exists and is non-empty
if full_path.exists() {
let is_empty = full_path
.read_dir()
.map(|mut entries| entries.next().is_none())
.unwrap_or(false);
if !is_empty {
return Err(AppCommandError::already_exists(
"Target directory already exists and is not empty",
));
}
}
// Determine the command based on package manager
let (program, prefix_args): (&str, Vec<&str>) = match package_manager.as_str() {
"pnpm" => ("pnpm", vec!["dlx"]),
"yarn" => ("yarn", vec!["dlx"]),
"bun" => ("bunx", vec![]),
_ => ("npx", vec![]),
};
let mut cmd = crate::process::tokio_command(program);
cmd.args(&prefix_args);
cmd.args([
"shadcn@latest",
"init",
"-n",
&project_name,
"-t",
&template,
"-p",
&preset_code,
"-y",
]);
cmd.current_dir(&target_dir);
let output = cmd.output().await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
AppCommandError::dependency_missing(format!(
"{program} is not installed. Please install Node.js first."
))
} else {
AppCommandError::external_command(
"Failed to execute project creation command",
e.to_string(),
)
}
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let detail = if stderr.is_empty() { stdout } else { stderr };
return Err(AppCommandError::external_command(
"Project creation command failed",
detail,
));
}
Ok(full_path_str)
}

View File

@@ -604,3 +604,30 @@ pub async fn open_push_window(
Ok(())
}
#[tauri::command]
pub async fn open_project_boot_window(app: AppHandle) -> Result<(), AppCommandError> {
if let Some(existing) = app.get_webview_window("project-boot") {
ensure_windows_undecorated(&existing);
let _ = existing.unminimize();
existing.set_focus().map_err(|e| {
AppCommandError::window("Failed to focus project boot window", e.to_string())
})?;
return Ok(());
}
let url = WebviewUrl::App("project-boot".into());
let builder = WebviewWindowBuilder::new(&app, "project-boot", url)
.title("Project Boot")
.inner_size(1400.0, 900.0)
.min_inner_size(1100.0, 700.0)
.center();
let window = apply_platform_window_style(builder)
.build()
.map_err(|e| {
AppCommandError::window("Failed to open project boot window", e.to_string())
})?;
ensure_windows_undecorated(&window);
Ok(())
}

View File

@@ -16,7 +16,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
use acp::manager::ConnectionManager;
use commands::{
acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands,
notification, system_settings, terminal as terminal_commands, version_control, windows,
notification, project_boot, system_settings, terminal as terminal_commands, version_control,
windows,
};
use tauri::Manager;
use terminal::manager::TerminalManager;
@@ -268,6 +269,8 @@ pub fn run() {
windows::open_merge_window,
windows::open_stash_window,
windows::open_push_window,
windows::open_project_boot_window,
project_boot::create_shadcn_project,
system_settings::get_system_proxy_settings,
system_settings::update_system_proxy_settings,
system_settings::get_system_language_settings,

View File

@@ -0,0 +1,41 @@
"use client"
import { Suspense, useEffect } from "react"
import { useTranslations } from "next-intl"
import { AppTitleBar } from "@/components/layout/app-title-bar"
import { AppToaster } from "@/components/ui/app-toaster"
import { ProjectBootWorkspace } from "@/components/project-boot/project-boot-workspace"
function ProjectBootPageInner() {
const t = useTranslations("ProjectBoot")
useEffect(() => {
document.title = `${t("title")} - codeg`
}, [t])
return (
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
<AppTitleBar
center={
<div className="text-sm font-semibold tracking-tight">
{t("title")}
</div>
}
/>
<main className="min-h-0 flex-1">
<ProjectBootWorkspace />
</main>
<AppToaster position="bottom-right" duration={6000} closeButton />
</div>
)
}
export default function ProjectBootPage() {
return (
<Suspense>
<ProjectBootPageInner />
</Suspense>
)
}

View File

@@ -0,0 +1,28 @@
"use client"
import { useTranslations } from "next-intl"
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/tabs"
import { ShadcnLauncher } from "./shadcn/shadcn-launcher"
export function ProjectBootWorkspace() {
const t = useTranslations("ProjectBoot")
return (
<Tabs defaultValue="shadcn" className="flex h-full flex-col">
<div className="shrink-0 border-b px-4 py-2">
<TabsList>
<TabsTrigger value="shadcn">{t("tabs.shadcn")}</TabsTrigger>
</TabsList>
</div>
<TabsContent value="shadcn" className="min-h-0 flex-1">
<ShadcnLauncher />
</TabsContent>
</Tabs>
)
}

View File

@@ -0,0 +1,286 @@
// ── Preset encoding/decoding (matches shadcn v2 format) ─────────────
const PRESET_STYLES = [
"nova",
"vega",
"maia",
"lyra",
"mira",
] as const
const PRESET_BASE_COLORS = [
"neutral",
"stone",
"zinc",
"gray",
"mauve",
"olive",
"mist",
"taupe",
] as const
const PRESET_THEMES = [
"neutral",
"stone",
"zinc",
"gray",
"amber",
"blue",
"cyan",
"emerald",
"fuchsia",
"green",
"indigo",
"lime",
"orange",
"pink",
"purple",
"red",
"rose",
"sky",
"teal",
"violet",
"yellow",
"mauve",
"olive",
"mist",
"taupe",
] as const
const PRESET_ICON_LIBRARIES = [
"lucide",
"hugeicons",
"tabler",
"phosphor",
"remixicon",
] as const
const PRESET_FONTS = [
"inter",
"noto-sans",
"nunito-sans",
"figtree",
"roboto",
"raleway",
"dm-sans",
"public-sans",
"outfit",
"jetbrains-mono",
"geist",
"geist-mono",
"lora",
"merriweather",
"playfair-display",
"noto-serif",
"roboto-slab",
"oxanium",
"manrope",
"space-grotesk",
"montserrat",
"ibm-plex-sans",
"source-sans-3",
"instrument-sans",
] as const
const PRESET_FONT_HEADINGS = ["inherit", ...PRESET_FONTS] as const
const PRESET_RADII = [
"default",
"none",
"small",
"medium",
"large",
] as const
const PRESET_MENU_ACCENTS = ["subtle", "bold"] as const
const PRESET_MENU_COLORS = [
"default",
"inverted",
"default-translucent",
"inverted-translucent",
] as const
/** V2 field layout for bit-packing (order must match shadcn exactly) */
const PRESET_FIELDS_V2 = [
{ key: "menuColor", values: PRESET_MENU_COLORS, bits: 3 },
{ key: "menuAccent", values: PRESET_MENU_ACCENTS, bits: 3 },
{ key: "radius", values: PRESET_RADII, bits: 4 },
{ key: "font", values: PRESET_FONTS, bits: 6 },
{ key: "iconLibrary", values: PRESET_ICON_LIBRARIES, bits: 6 },
{ key: "theme", values: PRESET_THEMES, bits: 6 },
{ key: "baseColor", values: PRESET_BASE_COLORS, bits: 6 },
{ key: "style", values: PRESET_STYLES, bits: 6 },
{ key: "chartColor", values: PRESET_THEMES, bits: 6 },
{ key: "fontHeading", values: PRESET_FONT_HEADINGS, bits: 5 },
] as const
const BASE62 =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
function toBase62(num: number): string {
if (num === 0) return "0"
let result = ""
let n = num
while (n > 0) {
result = BASE62[n % 62] + result
n = Math.floor(n / 62)
}
return result
}
/** Encode a preset config into a compact base62 code (v2 format). */
export function encodePreset(config: PresetCodeConfig): string {
const defaults: Record<string, string> = {
menuColor: "default",
menuAccent: "subtle",
radius: "default",
font: "inter",
iconLibrary: "lucide",
theme: "neutral",
baseColor: "neutral",
style: "nova",
chartColor: config.theme ?? "neutral",
fontHeading: "inherit",
}
const merged: Record<string, string> = { ...defaults }
for (const [k, v] of Object.entries(config)) {
if (v) merged[k] = v
}
let bits = 0
let offset = 0
for (const field of PRESET_FIELDS_V2) {
const idx = (field.values as readonly string[]).indexOf(
merged[field.key] ?? ""
)
bits += (idx === -1 ? 0 : idx) * 2 ** offset
offset += field.bits
}
return "b" + toBase62(bits)
}
// ── Config types ────────────────────────────────────────────────────
/** Fields that are encoded into the preset code (sent to CLI & preview). */
export interface PresetCodeConfig {
style: string
baseColor: string
theme: string
chartColor: string
iconLibrary: string
font: string
fontHeading: string
radius: string
menuAccent: string
menuColor: string
}
/** Full UI config (preset fields + non-preset fields like base/template). */
export interface ShadcnPresetConfig extends PresetCodeConfig {
base: string
template: string
}
export const DEFAULT_PRESET_CONFIG: ShadcnPresetConfig = {
base: "radix",
style: "nova",
baseColor: "neutral",
theme: "neutral",
chartColor: "neutral",
iconLibrary: "lucide",
font: "inter",
fontHeading: "inherit",
radius: "default",
menuAccent: "subtle",
menuColor: "default",
template: "start",
}
// ── UI option arrays ────────────────────────────────────────────────
export const BASE_OPTIONS = [
{ value: "radix", label: "Radix" },
{ value: "base", label: "Base" },
]
export const STYLE_OPTIONS = PRESET_STYLES.map((v) => ({
value: v,
label: v.charAt(0).toUpperCase() + v.slice(1),
}))
export const BASE_COLOR_OPTIONS = PRESET_BASE_COLORS.map((v) => ({
value: v,
label: v.charAt(0).toUpperCase() + v.slice(1),
}))
export const THEME_OPTIONS = PRESET_THEMES.map((v) => ({
value: v,
label: v.charAt(0).toUpperCase() + v.slice(1),
}))
export const ICON_LIBRARY_OPTIONS = PRESET_ICON_LIBRARIES.map((v) => ({
value: v,
label: v.charAt(0).toUpperCase() + v.slice(1),
}))
export const FONT_OPTIONS = PRESET_FONTS.map((v) => ({
value: v,
label: v
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" "),
}))
export const FONT_HEADING_OPTIONS = PRESET_FONT_HEADINGS.map((v) => ({
value: v,
label:
v === "inherit"
? "Inherit"
: v
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" "),
}))
export const MENU_ACCENT_OPTIONS = PRESET_MENU_ACCENTS.map((v) => ({
value: v,
label: v.charAt(0).toUpperCase() + v.slice(1),
}))
export const MENU_COLOR_OPTIONS = PRESET_MENU_COLORS.map((v) => ({
value: v,
label: v
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" "),
}))
export const RADIUS_OPTIONS = PRESET_RADII.map((v) => ({
value: v,
label: v.charAt(0).toUpperCase() + v.slice(1),
}))
export const TEMPLATE_OPTIONS = [{ value: "start", label: "Start" }]
export const FRAMEWORK_OPTIONS = [
{ value: "next", label: "Next.js" },
{ value: "vite", label: "Vite" },
{ value: "start", label: "TanStack Start" },
{ value: "react-router", label: "React Router" },
{ value: "laravel", label: "Laravel" },
{ value: "astro", label: "Astro" },
]
export const PACKAGE_MANAGER_OPTIONS = [
{ value: "pnpm", label: "pnpm" },
{ value: "npm", label: "npm" },
{ value: "yarn", label: "yarn" },
{ value: "bun", label: "bun" },
]
// ── URL builders ────────────────────────────────────────────────────
/** Build the preview iframe URL using a preset code. */
export function buildPreviewUrl(base: string, presetCode: string): string {
return `https://ui.shadcn.com/preview/${base}/preview?preset=${presetCode}`
}

View File

@@ -0,0 +1,201 @@
"use client"
import { useState } from "react"
import { useTranslations } from "next-intl"
import { Loader2, FolderOpen } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { openFileDialog } from "@/lib/platform"
import { createShadcnProject, openFolderWindow } from "@/lib/api"
import { FRAMEWORK_OPTIONS, PACKAGE_MANAGER_OPTIONS } from "./constants"
interface CreateProjectDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
presetCode: string
}
export function CreateProjectDialog({
open,
onOpenChange,
presetCode,
}: CreateProjectDialogProps) {
const t = useTranslations("ProjectBoot")
const [projectName, setProjectName] = useState("my-app")
const [framework, setFramework] = useState("next")
const [packageManager, setPackageManager] = useState("pnpm")
const [saveDirectory, setSaveDirectory] = useState("")
const [creating, setCreating] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleBrowse = async () => {
const result = await openFileDialog({ directory: true, multiple: false })
if (!result) return
const selected = Array.isArray(result) ? result[0] : result
setSaveDirectory(selected)
}
const handleCreate = async () => {
setError(null)
setCreating(true)
try {
const projectPath = await createShadcnProject({
projectName,
template: framework,
presetCode,
packageManager,
targetDir: saveDirectory,
})
toast.success(t("toasts.createSuccess"))
onOpenChange(false)
resetForm()
await openFolderWindow(projectPath)
} catch (err) {
const message =
err && typeof err === "object" && "message" in err
? (err as { message: string }).message
: String(err)
setError(message)
toast.error(t("toasts.createFailed"), { description: message })
} finally {
setCreating(false)
}
}
const resetForm = () => {
setProjectName("my-app")
setFramework("next")
setPackageManager("pnpm")
setSaveDirectory("")
setError(null)
}
const canCreate =
projectName.trim().length > 0 && saveDirectory.trim().length > 0
return (
<Dialog
open={open}
onOpenChange={(v) => {
onOpenChange(v)
if (!v) resetForm()
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("createDialog.title")}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-1.5">
<Label>{t("createDialog.projectName")}</Label>
<Input
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
placeholder={t("createDialog.projectNamePlaceholder")}
disabled={creating}
/>
</div>
<div className="space-y-1.5">
<Label>{t("createDialog.frameworkTemplate")}</Label>
<Select
value={framework}
onValueChange={setFramework}
disabled={creating}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FRAMEWORK_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>{t("createDialog.packageManager")}</Label>
<Select
value={packageManager}
onValueChange={setPackageManager}
disabled={creating}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>{t("createDialog.saveDirectory")}</Label>
<div className="flex gap-2">
<Input
value={saveDirectory}
onChange={(e) => setSaveDirectory(e.target.value)}
placeholder={t("createDialog.saveDirectoryPlaceholder")}
disabled={creating}
className="flex-1"
/>
<Button
variant="outline"
size="sm"
onClick={handleBrowse}
disabled={creating}
type="button"
>
<FolderOpen className="h-4 w-4" />
</Button>
</div>
</div>
{error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={creating}
>
{t("createDialog.cancel")}
</Button>
<Button onClick={handleCreate} disabled={!canCreate || creating}>
{creating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{creating ? t("createDialog.creating") : t("createDialog.create")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,128 @@
"use client"
import { useState } from "react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
BASE_OPTIONS,
STYLE_OPTIONS,
BASE_COLOR_OPTIONS,
THEME_OPTIONS,
ICON_LIBRARY_OPTIONS,
FONT_OPTIONS,
FONT_HEADING_OPTIONS,
MENU_ACCENT_OPTIONS,
MENU_COLOR_OPTIONS,
RADIUS_OPTIONS,
TEMPLATE_OPTIONS,
type ShadcnPresetConfig,
} from "./constants"
import { CreateProjectDialog } from "./create-project-dialog"
interface ShadcnConfigPanelProps {
config: ShadcnPresetConfig
onConfigChange: (key: keyof ShadcnPresetConfig, value: string) => void
presetCode: string
}
type ConfigI18nKey =
| "config.base"
| "config.style"
| "config.baseColor"
| "config.theme"
| "config.chartColor"
| "config.iconLibrary"
| "config.font"
| "config.fontHeading"
| "config.menuAccent"
| "config.menuColor"
| "config.radius"
| "config.template"
const CONFIG_FIELDS: {
key: keyof ShadcnPresetConfig
i18nKey: ConfigI18nKey
options: { value: string; label: string }[]
}[] = [
{ key: "base", i18nKey: "config.base", options: BASE_OPTIONS },
{ key: "style", i18nKey: "config.style", options: STYLE_OPTIONS },
{ key: "baseColor", i18nKey: "config.baseColor", options: BASE_COLOR_OPTIONS },
{ key: "theme", i18nKey: "config.theme", options: THEME_OPTIONS },
{ key: "chartColor", i18nKey: "config.chartColor", options: THEME_OPTIONS },
{
key: "iconLibrary",
i18nKey: "config.iconLibrary",
options: ICON_LIBRARY_OPTIONS,
},
{ key: "font", i18nKey: "config.font", options: FONT_OPTIONS },
{
key: "fontHeading",
i18nKey: "config.fontHeading",
options: FONT_HEADING_OPTIONS,
},
{ key: "menuAccent", i18nKey: "config.menuAccent", options: MENU_ACCENT_OPTIONS },
{ key: "menuColor", i18nKey: "config.menuColor", options: MENU_COLOR_OPTIONS },
{ key: "radius", i18nKey: "config.radius", options: RADIUS_OPTIONS },
{ key: "template", i18nKey: "config.template", options: TEMPLATE_OPTIONS },
]
export function ShadcnConfigPanel({
config,
onConfigChange,
presetCode,
}: ShadcnConfigPanelProps) {
const t = useTranslations("ProjectBoot")
const [createOpen, setCreateOpen] = useState(false)
return (
<div className="flex h-full flex-col">
<ScrollArea className="flex-1 px-4 py-3">
<div className="space-y-3">
{CONFIG_FIELDS.map((field) => (
<div key={field.key} className="space-y-1">
<Label className="text-xs text-muted-foreground">
{t(field.i18nKey)}
</Label>
<Select
value={config[field.key]}
onValueChange={(v) => onConfigChange(field.key, v)}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{field.options.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
</ScrollArea>
<div className="shrink-0 border-t px-4 py-3">
<Button className="w-full" onClick={() => setCreateOpen(true)}>
{t("config.createProject")}
</Button>
</div>
<CreateProjectDialog
open={createOpen}
onOpenChange={setCreateOpen}
presetCode={presetCode}
/>
</div>
)
}

View File

@@ -0,0 +1,50 @@
"use client"
import { useMemo, useState } from "react"
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable"
import { ShadcnConfigPanel } from "./shadcn-config-panel"
import { ShadcnPreview } from "./shadcn-preview"
import {
DEFAULT_PRESET_CONFIG,
encodePreset,
buildPreviewUrl,
type ShadcnPresetConfig,
} from "./constants"
export function ShadcnLauncher() {
const [config, setConfig] = useState<ShadcnPresetConfig>(
DEFAULT_PRESET_CONFIG
)
const presetCode = useMemo(() => encodePreset(config), [config])
const previewUrl = useMemo(
() => buildPreviewUrl(config.base, presetCode),
[config.base, presetCode]
)
const updateConfig = (key: keyof ShadcnPresetConfig, value: string) => {
setConfig((prev) => ({ ...prev, [key]: value }))
}
return (
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={40} minSize={30} maxSize={50}>
<ShadcnConfigPanel
config={config}
onConfigChange={updateConfig}
presetCode={presetCode}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={60} minSize={40}>
<ShadcnPreview previewUrl={previewUrl} />
</ResizablePanel>
</ResizablePanelGroup>
)
}

View File

@@ -0,0 +1,51 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { useTranslations } from "next-intl"
import { Loader2 } from "lucide-react"
interface ShadcnPreviewProps {
previewUrl: string
}
export function ShadcnPreview({ previewUrl }: ShadcnPreviewProps) {
const t = useTranslations("ProjectBoot")
const [debouncedUrl, setDebouncedUrl] = useState(previewUrl)
const [loading, setLoading] = useState(true)
const timerRef = useRef<ReturnType<typeof setTimeout>>(null)
useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
timerRef.current = setTimeout(() => {
setDebouncedUrl(previewUrl)
setLoading(true)
}, 500)
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [previewUrl])
return (
<div className="relative h-full w-full">
{loading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">
{t("preview.loading")}
</span>
</div>
)}
<iframe
key={debouncedUrl}
src={debouncedUrl}
className="h-full w-full border-0"
onLoad={() => setLoading(false)}
sandbox="allow-scripts allow-same-origin allow-popups"
/>
</div>
)
}

View File

@@ -1,10 +1,10 @@
"use client"
import { useState } from "react"
import { FolderOpen, GitBranch } from "lucide-react"
import { FolderOpen, GitBranch, Rocket } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { openFolderWindow } from "@/lib/api"
import { openFolderWindow, openProjectBootWindow } from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { Button } from "@/components/ui/button"
import { CloneDialog } from "./clone-dialog"
@@ -51,6 +51,23 @@ export function FolderActions() {
{t("cloneRepository")}
</Button>
<Button
variant="ghost"
className="justify-start gap-2 h-9"
onClick={async () => {
try {
await openProjectBootWindow()
} catch (err) {
console.error("[FolderActions] failed to open project boot:", err)
toast.error(t("toasts.openProjectBootFailed"))
}
}}
type="button"
>
<Rocket className="h-4 w-4" />
{t("projectBoot")}
</Button>
<CloneDialog open={cloneOpen} onOpenChange={setCloneOpen} />
</div>
)

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "إزالة من السجل",
"openFolder": "فتح مجلد",
"cloneRepository": "استنساخ المستودع",
"projectBoot": "مُنشئ المشروع",
"softwareVersion": "الإصدار {version}",
"toasts": {
"loadFolderHistoryFailed": "فشل تحميل سجل المجلدات",
"openFolderFailed": "فشل فتح المجلد",
"removeFromHistoryFailed": "فشل إزالة المجلد من السجل",
"openSettingsFailed": "فشل فتح الإعدادات",
"cloneFailed": "فشل استنساخ المستودع"
"cloneFailed": "فشل استنساخ المستودع",
"openProjectBootFailed": "فشل فتح مُنشئ المشروع"
},
"errors": {
"unknown": "حدث خطأ غير متوقع",
@@ -1571,5 +1573,50 @@
"loadingHunk": "جارٍ تحميل hunk...",
"noDiffData": "لا توجد بيانات diff"
}
},
"ProjectBoot": {
"title": "مُنشئ المشروع",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "الأساس",
"style": "النمط",
"baseColor": "اللون الأساسي",
"theme": "السمة",
"chartColor": "لون المخطط",
"iconLibrary": "مكتبة الأيقونات",
"font": "الخط",
"fontHeading": "خط العنوان",
"menuAccent": "تمييز القائمة",
"menuColor": "لون القائمة",
"radius": "نصف القطر",
"template": "القالب",
"createProject": "إنشاء مشروع"
},
"preview": {
"loading": "جاري تحميل المعاينة..."
},
"createDialog": {
"title": "إنشاء مشروع",
"projectName": "اسم المشروع",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "قالب الإطار",
"packageManager": "مدير الحزم",
"saveDirectory": "دليل الحفظ",
"saveDirectoryPlaceholder": "اختر الدليل...",
"browseDirectory": "تصفح",
"cancel": "إلغاء",
"create": "إنشاء",
"creating": "جاري إنشاء المشروع..."
},
"toasts": {
"createFailed": "فشل إنشاء المشروع",
"createSuccess": "تم إنشاء المشروع بنجاح"
},
"errors": {
"directoryExists": "الدليل موجود بالفعل وليس فارغاً.",
"commandFailed": "فشل أمر إنشاء المشروع."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "Aus Verlauf entfernen",
"openFolder": "Ordner öffnen",
"cloneRepository": "Repository klonen",
"projectBoot": "Projekt-Starter",
"softwareVersion": "Version {version}",
"toasts": {
"loadFolderHistoryFailed": "Ordnerverlauf konnte nicht geladen werden",
"openFolderFailed": "Ordner konnte nicht geöffnet werden",
"removeFromHistoryFailed": "Ordner konnte nicht entfernt werden",
"openSettingsFailed": "Einstellungen konnten nicht geöffnet werden",
"cloneFailed": "Repository konnte nicht geklont werden"
"cloneFailed": "Repository konnte nicht geklont werden",
"openProjectBootFailed": "Projekt-Starter konnte nicht geöffnet werden"
},
"errors": {
"unknown": "Unerwarteter Fehler",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Hunk wird geladen...",
"noDiffData": "Keine Diff-Daten"
}
},
"ProjectBoot": {
"title": "Projekt-Starter",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "Basis",
"style": "Stil",
"baseColor": "Basisfarbe",
"theme": "Thema",
"chartColor": "Diagrammfarbe",
"iconLibrary": "Icon-Bibliothek",
"font": "Schriftart",
"fontHeading": "Überschrift-Schriftart",
"menuAccent": "Menü-Akzent",
"menuColor": "Menü-Farbe",
"radius": "Radius",
"template": "Vorlage",
"createProject": "Projekt erstellen"
},
"preview": {
"loading": "Vorschau wird geladen..."
},
"createDialog": {
"title": "Projekt erstellen",
"projectName": "Projektname",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "Framework-Vorlage",
"packageManager": "Paketmanager",
"saveDirectory": "Speicherverzeichnis",
"saveDirectoryPlaceholder": "Verzeichnis auswählen...",
"browseDirectory": "Durchsuchen",
"cancel": "Abbrechen",
"create": "Erstellen",
"creating": "Projekt wird erstellt..."
},
"toasts": {
"createFailed": "Projekt konnte nicht erstellt werden",
"createSuccess": "Projekt erfolgreich erstellt"
},
"errors": {
"directoryExists": "Verzeichnis existiert bereits und ist nicht leer.",
"commandFailed": "Projekterstellungsbefehl fehlgeschlagen."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "Remove from history",
"openFolder": "Open Folder",
"cloneRepository": "Clone Repository",
"projectBoot": "Project Boot",
"softwareVersion": "version {version}",
"toasts": {
"loadFolderHistoryFailed": "Failed to load folder history",
"openFolderFailed": "Failed to open folder",
"removeFromHistoryFailed": "Failed to remove folder",
"openSettingsFailed": "Failed to open settings",
"cloneFailed": "Failed to clone repository"
"cloneFailed": "Failed to clone repository",
"openProjectBootFailed": "Failed to open Project Boot"
},
"errors": {
"unknown": "Unexpected error",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Loading hunk...",
"noDiffData": "No diff data"
}
},
"ProjectBoot": {
"title": "Project Boot",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "Base",
"style": "Style",
"baseColor": "Base Color",
"theme": "Theme",
"chartColor": "Chart Color",
"iconLibrary": "Icon Library",
"font": "Font",
"fontHeading": "Heading Font",
"menuAccent": "Menu Accent",
"menuColor": "Menu Color",
"radius": "Radius",
"template": "Template",
"createProject": "Create Project"
},
"preview": {
"loading": "Loading preview..."
},
"createDialog": {
"title": "Create Project",
"projectName": "Project Name",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "Framework Template",
"packageManager": "Package Manager",
"saveDirectory": "Save Directory",
"saveDirectoryPlaceholder": "Select directory...",
"browseDirectory": "Browse",
"cancel": "Cancel",
"create": "Create",
"creating": "Creating project..."
},
"toasts": {
"createFailed": "Failed to create project",
"createSuccess": "Project created successfully"
},
"errors": {
"directoryExists": "Directory already exists and is not empty.",
"commandFailed": "Project creation command failed."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "Quitar del historial",
"openFolder": "Abrir carpeta",
"cloneRepository": "Clonar repositorio",
"projectBoot": "Inicio de Proyecto",
"softwareVersion": "versión {version}",
"toasts": {
"loadFolderHistoryFailed": "No se pudo cargar el historial de carpetas",
"openFolderFailed": "No se pudo abrir la carpeta",
"removeFromHistoryFailed": "No se pudo quitar la carpeta",
"openSettingsFailed": "No se pudo abrir la configuración",
"cloneFailed": "No se pudo clonar el repositorio"
"cloneFailed": "No se pudo clonar el repositorio",
"openProjectBootFailed": "Error al abrir el inicio de proyecto"
},
"errors": {
"unknown": "Error inesperado",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Cargando hunk...",
"noDiffData": "Sin datos de diff"
}
},
"ProjectBoot": {
"title": "Inicio de Proyecto",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "Base",
"style": "Estilo",
"baseColor": "Color base",
"theme": "Tema",
"chartColor": "Color del gráfico",
"iconLibrary": "Biblioteca de iconos",
"font": "Fuente",
"fontHeading": "Fuente de título",
"menuAccent": "Acento del menú",
"menuColor": "Color del menú",
"radius": "Radio",
"template": "Plantilla",
"createProject": "Crear proyecto"
},
"preview": {
"loading": "Cargando vista previa..."
},
"createDialog": {
"title": "Crear proyecto",
"projectName": "Nombre del proyecto",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "Plantilla del framework",
"packageManager": "Gestor de paquetes",
"saveDirectory": "Directorio de guardado",
"saveDirectoryPlaceholder": "Seleccionar directorio...",
"browseDirectory": "Explorar",
"cancel": "Cancelar",
"create": "Crear",
"creating": "Creando proyecto..."
},
"toasts": {
"createFailed": "Error al crear el proyecto",
"createSuccess": "Proyecto creado exitosamente"
},
"errors": {
"directoryExists": "El directorio ya existe y no está vacío.",
"commandFailed": "El comando de creación del proyecto falló."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "Retirer de lhistorique",
"openFolder": "Ouvrir un dossier",
"cloneRepository": "Cloner un dépôt",
"projectBoot": "Lanceur de projet",
"softwareVersion": "Version {version}",
"toasts": {
"loadFolderHistoryFailed": "Échec du chargement de lhistorique des dossiers",
"openFolderFailed": "Échec de louverture du dossier",
"removeFromHistoryFailed": "Échec de la suppression du dossier",
"openSettingsFailed": "Échec de louverture des paramètres",
"cloneFailed": "Échec du clonage du dépôt"
"cloneFailed": "Échec du clonage du dépôt",
"openProjectBootFailed": "Impossible d'ouvrir le lanceur de projet"
},
"errors": {
"unknown": "Erreur inattendue",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Chargement du hunk...",
"noDiffData": "Aucune donnée diff"
}
},
"ProjectBoot": {
"title": "Lanceur de projet",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "Base",
"style": "Style",
"baseColor": "Couleur de base",
"theme": "Thème",
"chartColor": "Couleur du graphique",
"iconLibrary": "Bibliothèque d'icônes",
"font": "Police",
"fontHeading": "Police de titre",
"menuAccent": "Accent du menu",
"menuColor": "Couleur du menu",
"radius": "Rayon",
"template": "Modèle",
"createProject": "Créer un projet"
},
"preview": {
"loading": "Chargement de l'aperçu..."
},
"createDialog": {
"title": "Créer un projet",
"projectName": "Nom du projet",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "Modèle de framework",
"packageManager": "Gestionnaire de paquets",
"saveDirectory": "Répertoire de sauvegarde",
"saveDirectoryPlaceholder": "Sélectionner un répertoire...",
"browseDirectory": "Parcourir",
"cancel": "Annuler",
"create": "Créer",
"creating": "Création du projet..."
},
"toasts": {
"createFailed": "Échec de la création du projet",
"createSuccess": "Projet créé avec succès"
},
"errors": {
"directoryExists": "Le répertoire existe déjà et n'est pas vide.",
"commandFailed": "La commande de création du projet a échoué."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "履歴から削除",
"openFolder": "フォルダを開く",
"cloneRepository": "リポジトリをクローン",
"projectBoot": "プロジェクトブート",
"softwareVersion": "バージョン {version}",
"toasts": {
"loadFolderHistoryFailed": "フォルダ履歴の読み込みに失敗しました",
"openFolderFailed": "フォルダを開けませんでした",
"removeFromHistoryFailed": "履歴からの削除に失敗しました",
"openSettingsFailed": "設定を開けませんでした",
"cloneFailed": "リポジトリのクローンに失敗しました"
"cloneFailed": "リポジトリのクローンに失敗しました",
"openProjectBootFailed": "プロジェクトブートを開けませんでした"
},
"errors": {
"unknown": "予期しないエラーが発生しました",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Hunk を読み込み中...",
"noDiffData": "Diff データがありません"
}
},
"ProjectBoot": {
"title": "プロジェクトブート",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "ベース",
"style": "スタイル",
"baseColor": "ベースカラー",
"theme": "テーマ",
"chartColor": "チャートカラー",
"iconLibrary": "アイコンライブラリ",
"font": "フォント",
"fontHeading": "見出しフォント",
"menuAccent": "メニューアクセント",
"menuColor": "メニューカラー",
"radius": "角丸",
"template": "テンプレート",
"createProject": "プロジェクトを作成"
},
"preview": {
"loading": "プレビューを読み込み中..."
},
"createDialog": {
"title": "プロジェクトを作成",
"projectName": "プロジェクト名",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "フレームワークテンプレート",
"packageManager": "パッケージマネージャー",
"saveDirectory": "保存先ディレクトリ",
"saveDirectoryPlaceholder": "ディレクトリを選択...",
"browseDirectory": "参照",
"cancel": "キャンセル",
"create": "作成",
"creating": "プロジェクトを作成中..."
},
"toasts": {
"createFailed": "プロジェクトの作成に失敗しました",
"createSuccess": "プロジェクトが正常に作成されました"
},
"errors": {
"directoryExists": "ディレクトリは既に存在し、空ではありません。",
"commandFailed": "プロジェクト作成コマンドが失敗しました。"
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "기록에서 제거",
"openFolder": "폴더 열기",
"cloneRepository": "저장소 클론",
"projectBoot": "프로젝트 부트",
"softwareVersion": "버전 {version}",
"toasts": {
"loadFolderHistoryFailed": "폴더 기록을 불러오지 못했습니다",
"openFolderFailed": "폴더를 열지 못했습니다",
"removeFromHistoryFailed": "기록에서 제거하지 못했습니다",
"openSettingsFailed": "설정을 열지 못했습니다",
"cloneFailed": "저장소 클론에 실패했습니다"
"cloneFailed": "저장소 클론에 실패했습니다",
"openProjectBootFailed": "프로젝트 부트를 열지 못했습니다"
},
"errors": {
"unknown": "예기치 않은 오류가 발생했습니다",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Hunk 로딩 중...",
"noDiffData": "Diff 데이터 없음"
}
},
"ProjectBoot": {
"title": "프로젝트 부트",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "베이스",
"style": "스타일",
"baseColor": "베이스 색상",
"theme": "테마",
"chartColor": "차트 색상",
"iconLibrary": "아이콘 라이브러리",
"font": "글꼴",
"fontHeading": "제목 글꼴",
"menuAccent": "메뉴 강조",
"menuColor": "메뉴 색상",
"radius": "둥글기",
"template": "템플릿",
"createProject": "프로젝트 만들기"
},
"preview": {
"loading": "미리보기 로딩 중..."
},
"createDialog": {
"title": "프로젝트 만들기",
"projectName": "프로젝트 이름",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "프레임워크 템플릿",
"packageManager": "패키지 매니저",
"saveDirectory": "저장 디렉토리",
"saveDirectoryPlaceholder": "디렉토리 선택...",
"browseDirectory": "찾아보기",
"cancel": "취소",
"create": "만들기",
"creating": "프로젝트 생성 중..."
},
"toasts": {
"createFailed": "프로젝트 생성에 실패했습니다",
"createSuccess": "프로젝트가 성공적으로 생성되었습니다"
},
"errors": {
"directoryExists": "디렉토리가 이미 존재하며 비어 있지 않습니다.",
"commandFailed": "프로젝트 생성 명령이 실패했습니다."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "Remover do histórico",
"openFolder": "Abrir pasta",
"cloneRepository": "Clonar repositório",
"projectBoot": "Inicializador de Projeto",
"softwareVersion": "versão {version}",
"toasts": {
"loadFolderHistoryFailed": "Falha ao carregar o histórico de pastas",
"openFolderFailed": "Falha ao abrir a pasta",
"removeFromHistoryFailed": "Falha ao remover a pasta",
"openSettingsFailed": "Falha ao abrir as configurações",
"cloneFailed": "Falha ao clonar o repositório"
"cloneFailed": "Falha ao clonar o repositório",
"openProjectBootFailed": "Falha ao abrir o inicializador de projeto"
},
"errors": {
"unknown": "Erro inesperado",
@@ -1571,5 +1573,50 @@
"loadingHunk": "Carregando hunk...",
"noDiffData": "Sem dados de diff"
}
},
"ProjectBoot": {
"title": "Inicializador de Projeto",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "Base",
"style": "Estilo",
"baseColor": "Cor base",
"theme": "Tema",
"chartColor": "Cor do gráfico",
"iconLibrary": "Biblioteca de ícones",
"font": "Fonte",
"fontHeading": "Fonte do título",
"menuAccent": "Destaque do menu",
"menuColor": "Cor do menu",
"radius": "Raio",
"template": "Modelo",
"createProject": "Criar projeto"
},
"preview": {
"loading": "Carregando visualização..."
},
"createDialog": {
"title": "Criar projeto",
"projectName": "Nome do projeto",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "Modelo de framework",
"packageManager": "Gerenciador de pacotes",
"saveDirectory": "Diretório de salvamento",
"saveDirectoryPlaceholder": "Selecionar diretório...",
"browseDirectory": "Procurar",
"cancel": "Cancelar",
"create": "Criar",
"creating": "Criando projeto..."
},
"toasts": {
"createFailed": "Falha ao criar o projeto",
"createSuccess": "Projeto criado com sucesso"
},
"errors": {
"directoryExists": "O diretório já existe e não está vazio.",
"commandFailed": "O comando de criação do projeto falhou."
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "从历史中移除",
"openFolder": "打开文件夹",
"cloneRepository": "克隆仓库",
"projectBoot": "项目启动器",
"softwareVersion": "版本 {version}",
"toasts": {
"loadFolderHistoryFailed": "加载文件夹历史失败",
"openFolderFailed": "打开文件夹失败",
"removeFromHistoryFailed": "移除历史记录失败",
"openSettingsFailed": "打开设置失败",
"cloneFailed": "克隆仓库失败"
"cloneFailed": "克隆仓库失败",
"openProjectBootFailed": "打开项目启动器失败"
},
"errors": {
"unknown": "发生未知错误",
@@ -1571,5 +1573,50 @@
"loadingHunk": "正在加载代码块...",
"noDiffData": "无差异数据"
}
},
"ProjectBoot": {
"title": "项目启动器",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "基础库",
"style": "风格",
"baseColor": "基础颜色",
"theme": "主题",
"chartColor": "图表颜色",
"iconLibrary": "图标库",
"font": "字体",
"fontHeading": "标题字体",
"menuAccent": "菜单强调",
"menuColor": "菜单颜色",
"radius": "圆角",
"template": "模板",
"createProject": "创建项目"
},
"preview": {
"loading": "加载预览..."
},
"createDialog": {
"title": "创建项目",
"projectName": "项目名称",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "框架模板",
"packageManager": "包管理器",
"saveDirectory": "保存目录",
"saveDirectoryPlaceholder": "选择目录...",
"browseDirectory": "浏览",
"cancel": "取消",
"create": "创建",
"creating": "正在创建项目..."
},
"toasts": {
"createFailed": "创建项目失败",
"createSuccess": "项目创建成功"
},
"errors": {
"directoryExists": "目录已存在且不为空。",
"commandFailed": "项目创建命令执行失败。"
}
}
}

View File

@@ -21,13 +21,15 @@
"removeFromHistory": "從歷史中移除",
"openFolder": "打開資料夾",
"cloneRepository": "複製倉庫",
"projectBoot": "專案啟動器",
"softwareVersion": "版本 {version}",
"toasts": {
"loadFolderHistoryFailed": "載入資料夾歷史失敗",
"openFolderFailed": "打開資料夾失敗",
"removeFromHistoryFailed": "移除歷史記錄失敗",
"openSettingsFailed": "打開設定失敗",
"cloneFailed": "複製倉庫失敗"
"cloneFailed": "複製倉庫失敗",
"openProjectBootFailed": "開啟專案啟動器失敗"
},
"errors": {
"unknown": "發生未知錯誤",
@@ -1571,5 +1573,50 @@
"loadingHunk": "正在載入區塊...",
"noDiffData": "無差異資料"
}
},
"ProjectBoot": {
"title": "專案啟動器",
"tabs": {
"shadcn": "shadcn"
},
"config": {
"base": "基礎庫",
"style": "風格",
"baseColor": "基礎顏色",
"theme": "主題",
"chartColor": "圖表顏色",
"iconLibrary": "圖示庫",
"font": "字體",
"fontHeading": "標題字體",
"menuAccent": "選單強調",
"menuColor": "選單顏色",
"radius": "圓角",
"template": "模板",
"createProject": "建立專案"
},
"preview": {
"loading": "載入預覽..."
},
"createDialog": {
"title": "建立專案",
"projectName": "專案名稱",
"projectNamePlaceholder": "my-app",
"frameworkTemplate": "框架模板",
"packageManager": "套件管理器",
"saveDirectory": "儲存目錄",
"saveDirectoryPlaceholder": "選擇目錄...",
"browseDirectory": "瀏覽",
"cancel": "取消",
"create": "建立",
"creating": "正在建立專案..."
},
"toasts": {
"createFailed": "建立專案失敗",
"createSuccess": "專案建立成功"
},
"errors": {
"directoryExists": "目錄已存在且不為空。",
"commandFailed": "專案建立命令執行失敗。"
}
}
}

View File

@@ -951,6 +951,29 @@ export async function openSettingsWindow(
window.open(result.path, `settings-${section ?? "general"}`)
}
export async function openProjectBootWindow(): Promise<void> {
if (getTransport().isDesktop()) {
return getTransport().call("open_project_boot_window")
}
window.open("/project-boot", "project-boot")
}
export async function createShadcnProject(params: {
projectName: string
template: string
presetCode: string
packageManager: string
targetDir: string
}): Promise<string> {
return getTransport().call("create_shadcn_project", {
projectName: params.projectName,
template: params.template,
presetCode: params.presetCode,
packageManager: params.packageManager,
targetDir: params.targetDir,
})
}
export async function listOpenFolders(): Promise<FolderHistoryEntry[]> {
return getTransport().call("list_open_folders")
}