feat(components): add reusable full-screen panel components
Add new full-screen panel components to support the UI refactoring: - FullScreenPanel: Reusable full-screen layout component with header, content area, and optional footer. Provides consistent layout for settings, prompts, and other full-screen views. - PromptFormPanel: Dedicated panel for creating and editing prompts with markdown preview support. Features real-time validation and integrated save/cancel actions. - AgentsPanel: Panel component for managing agent configurations. Provides a consistent interface for agent CRUD operations. - RepoManagerPanel: Full-featured repository manager panel for Skills. Supports repository listing, addition, deletion, and configuration management with integrated validation. These components establish the foundation for the upcoming settings page migration from dialog-based to full-screen layout.
This commit is contained in:
148
src/components/prompts/PromptFormPanel.tsx
Normal file
148
src/components/prompts/PromptFormPanel.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import type { Prompt, AppId } from "@/lib/api";
|
||||
|
||||
interface PromptFormPanelProps {
|
||||
appId: AppId;
|
||||
editingId?: string;
|
||||
initialData?: Prompt;
|
||||
onSave: (id: string, prompt: Prompt) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PromptFormPanel: React.FC<PromptFormPanelProps> = ({
|
||||
appId,
|
||||
editingId,
|
||||
initialData,
|
||||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const appName = t(`apps.${appId}`);
|
||||
const filenameMap: Record<AppId, string> = {
|
||||
claude: "CLAUDE.md",
|
||||
codex: "AGENTS.md",
|
||||
gemini: "GEMINI.md",
|
||||
};
|
||||
const filename = filenameMap[appId];
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setName(initialData.name);
|
||||
setDescription(initialData.description || "");
|
||||
setContent(initialData.content);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const id = editingId || `prompt-${Date.now()}`;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const prompt: Prompt = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
content: content.trim(),
|
||||
enabled: initialData?.enabled || false,
|
||||
createdAt: initialData?.createdAt || timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
await onSave(id, prompt);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
// Error handled by hook
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const title = editingId
|
||||
? t("prompts.editTitle", { appName })
|
||||
: t("prompts.addTitle", { appName });
|
||||
|
||||
return (
|
||||
<FullScreenPanel
|
||||
isOpen={true}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div>
|
||||
<Label htmlFor="name" className="text-foreground">{t("prompts.name")}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("prompts.namePlaceholder")}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description" className="text-foreground">{t("prompts.description")}</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t("prompts.descriptionPlaceholder")}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="content" className="block mb-2 text-foreground">
|
||||
{t("prompts.content")}
|
||||
</Label>
|
||||
<MarkdownEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder={t("prompts.contentPlaceholder", { filename })}
|
||||
darkMode={isDarkMode}
|
||||
minHeight="500px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-6">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={!name.trim() || !content.trim() || saving}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFormPanel;
|
||||
Reference in New Issue
Block a user