feat: migrate History/Recipes/Install pages to shadcn components
Replace inline styles and old CSS class references with Tailwind utilities and shadcn Card, Badge, Button, Input, and Label components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import React, { useEffect, useReducer } from "react";
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { api } from "../lib/api";
|
||||
import { initialState, reducer } from "../lib/state";
|
||||
import { DiffViewer } from "../components/DiffViewer";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function History() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
@@ -16,54 +19,66 @@ export function History() {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>History</h2>
|
||||
<div className="history-list">
|
||||
<h2 className="text-2xl font-bold text-text-main mb-4">History</h2>
|
||||
<div className="space-y-3">
|
||||
{state.history.map((item) => (
|
||||
<article key={item.id} className="history-item">
|
||||
<p>
|
||||
{item.createdAt} · {item.recipeId || "manual"} · {item.source}
|
||||
{!item.canRollback ? " · not rollbackable" : ""}
|
||||
</p>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const preview = await api.previewRollback(item.id);
|
||||
dispatch({ type: "setPreview", preview });
|
||||
} catch (err) {
|
||||
dispatch({ type: "setMessage", message: String(err) });
|
||||
}
|
||||
}}
|
||||
disabled={!item.canRollback}
|
||||
>
|
||||
Preview rollback
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!item.canRollback) {
|
||||
dispatch({
|
||||
type: "setMessage",
|
||||
message: "This snapshot cannot be rolled back",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.rollback(item.id);
|
||||
dispatch({ type: "setMessage", message: "Rollback completed" });
|
||||
await refreshHistory();
|
||||
} catch (err) {
|
||||
dispatch({ type: "setMessage", message: String(err) });
|
||||
}
|
||||
}}
|
||||
disabled={!item.canRollback}
|
||||
>
|
||||
Rollback
|
||||
</button>
|
||||
</article>
|
||||
<Card key={item.id} className="bg-panel border-border-subtle">
|
||||
<CardContent>
|
||||
<p className="text-sm text-text-main">
|
||||
{item.createdAt} · {item.recipeId || "manual"} · {item.source}
|
||||
{!item.canRollback && (
|
||||
<Badge variant="outline" className="ml-2">not rollbackable</Badge>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const preview = await api.previewRollback(item.id);
|
||||
dispatch({ type: "setPreview", preview });
|
||||
} catch (err) {
|
||||
dispatch({ type: "setMessage", message: String(err) });
|
||||
}
|
||||
}}
|
||||
disabled={!item.canRollback}
|
||||
>
|
||||
Preview rollback
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if (!item.canRollback) {
|
||||
dispatch({
|
||||
type: "setMessage",
|
||||
message: "This snapshot cannot be rolled back",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.rollback(item.id);
|
||||
dispatch({ type: "setMessage", message: "Rollback completed" });
|
||||
await refreshHistory();
|
||||
} catch (err) {
|
||||
dispatch({ type: "setMessage", message: String(err) });
|
||||
}
|
||||
}}
|
||||
disabled={!item.canRollback}
|
||||
>
|
||||
Rollback
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{state.lastPreview && <DiffViewer value={state.lastPreview.diff} />}
|
||||
<button onClick={refreshHistory}>Refresh</button>
|
||||
<p>{state.message}</p>
|
||||
<Button variant="outline" onClick={refreshHistory} className="mt-3">
|
||||
Refresh
|
||||
</Button>
|
||||
<p className="text-sm text-text-main/70 mt-2">{state.message}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useEffect, useReducer, useState } from "react";
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { api } from "../lib/api";
|
||||
import { ParamForm } from "../components/ParamForm";
|
||||
import { DiffViewer } from "../components/DiffViewer";
|
||||
import { initialState, reducer } from "../lib/state";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function Install({
|
||||
recipeId,
|
||||
@@ -33,11 +35,13 @@ export function Install({
|
||||
|
||||
const recipe = state.recipes.find((r) => r.id === recipeId);
|
||||
|
||||
if (!recipe) return <div>Recipe not found</div>;
|
||||
if (!recipe) return <div className="text-text-main">Recipe not found</div>;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Install {recipe.name}</h2>
|
||||
<h2 className="text-2xl font-bold text-text-main mb-4">
|
||||
Install {recipe.name}
|
||||
</h2>
|
||||
<ParamForm
|
||||
recipe={recipe}
|
||||
values={params}
|
||||
@@ -51,41 +55,49 @@ export function Install({
|
||||
}}
|
||||
/>
|
||||
{state.lastPreview && (
|
||||
<section>
|
||||
<h3>Preview</h3>
|
||||
<DiffViewer value={state.lastPreview.diff} />
|
||||
<button
|
||||
disabled={isApplying}
|
||||
onClick={() => {
|
||||
setIsApplying(true);
|
||||
api.applyRecipe(recipe.id, params, recipeSource)
|
||||
.then((result) => {
|
||||
if (!result.ok) {
|
||||
const errors = result.errors.length ? result.errors.join(", ") : "failed";
|
||||
dispatch({ type: "setMessage", message: `Apply failed: ${errors}` });
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: "setMessage",
|
||||
message: result.snapshotId
|
||||
? `Applied successfully. Snapshot: ${result.snapshotId}`
|
||||
: "Applied successfully",
|
||||
});
|
||||
if (onDone) {
|
||||
onDone();
|
||||
}
|
||||
})
|
||||
.catch((err) => dispatch({ type: "setMessage", message: String(err) }))
|
||||
.finally(() => setIsApplying(false));
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
{isPreviewing ? <span> ...previewing</span> : null}
|
||||
{isApplying ? <span> ...applying</span> : null}
|
||||
</section>
|
||||
<Card className="mt-4 bg-panel border-border-subtle">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-text-main mb-2">
|
||||
Preview
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DiffViewer value={state.lastPreview.diff} />
|
||||
<div className="flex items-center mt-3">
|
||||
<Button
|
||||
disabled={isApplying}
|
||||
onClick={() => {
|
||||
setIsApplying(true);
|
||||
api.applyRecipe(recipe.id, params, recipeSource)
|
||||
.then((result) => {
|
||||
if (!result.ok) {
|
||||
const errors = result.errors.length ? result.errors.join(", ") : "failed";
|
||||
dispatch({ type: "setMessage", message: `Apply failed: ${errors}` });
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: "setMessage",
|
||||
message: result.snapshotId
|
||||
? `Applied successfully. Snapshot: ${result.snapshotId}`
|
||||
: "Applied successfully",
|
||||
});
|
||||
if (onDone) {
|
||||
onDone();
|
||||
}
|
||||
})
|
||||
.catch((err) => dispatch({ type: "setMessage", message: String(err) }))
|
||||
.finally(() => setIsApplying(false));
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
{isPreviewing ? <span className="text-sm text-text-main/60 ml-2">...previewing</span> : null}
|
||||
{isApplying ? <span className="text-sm text-text-main/60 ml-2">...applying</span> : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<p>{state.message}</p>
|
||||
<p className="text-sm text-text-main/70 mt-2">{state.message}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React, { useEffect, useReducer, useState } from "react";
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { api } from "../lib/api";
|
||||
import { RecipeCard } from "../components/RecipeCard";
|
||||
import { initialState, reducer } from "../lib/state";
|
||||
import type { Recipe } from "../lib/types";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export function Recipes({
|
||||
onInstall,
|
||||
@@ -31,30 +35,30 @@ export function Recipes({
|
||||
load("");
|
||||
}, []);
|
||||
|
||||
const onLoadSource = (event: React.FormEvent) => {
|
||||
const onLoadSource = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
load(source);
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Recipes</h2>
|
||||
<form onSubmit={onLoadSource} style={{ marginBottom: 8 }}>
|
||||
<label>
|
||||
Recipe source (file path or URL)
|
||||
<input
|
||||
value={source}
|
||||
onChange={(event) => setSource(event.target.value)}
|
||||
placeholder="/path/recipes.json or https://example.com/recipes.json"
|
||||
style={{ marginLeft: 8, width: 380 }}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" style={{ marginLeft: 8 }}>
|
||||
<h2 className="text-2xl font-bold text-text-main mb-4">Recipes</h2>
|
||||
<form onSubmit={onLoadSource} className="mb-2 flex items-center gap-2">
|
||||
<Label>Recipe source (file path or URL)</Label>
|
||||
<Input
|
||||
value={source}
|
||||
onChange={(event) => setSource(event.target.value)}
|
||||
placeholder="/path/recipes.json or https://example.com/recipes.json"
|
||||
className="w-[380px] bg-panel border-border-subtle text-text-main"
|
||||
/>
|
||||
<Button type="submit" className="ml-2">
|
||||
{isLoading ? "Loading..." : "Load"}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
<p style={{ opacity: 0.8, marginTop: 0 }}>Loaded from: {loadedSource || "builtin / clawpal recipes"}</p>
|
||||
<div className="recipe-grid">
|
||||
<p className="text-sm text-text-main/80 mt-0">
|
||||
Loaded from: {loadedSource || "builtin / clawpal recipes"}
|
||||
</p>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3">
|
||||
{state.recipes.map((recipe: Recipe) => (
|
||||
<RecipeCard
|
||||
key={recipe.id}
|
||||
|
||||
Reference in New Issue
Block a user