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:
zhixian
2026-02-17 01:48:09 +09:00
parent eb6daad1e1
commit 8a88bd0565
3 changed files with 130 additions and 99 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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}