feat(12-02): StarRating component, API extensions, DesignGuide entry
- Create StarRating component with interactive/readonly modes, amber stars, size sm/md - Add PersonalRating type and taskCount/avgCostUsd/lastUsedAt to SkillListItem - Add getRatings and addRating to skillRegistryApi - Add Rating System section to DesignGuide with all variants - Fix SkillCard fixture and DesignGuide examples to include new SkillListItem fields
This commit is contained in:
parent
b52f5a8adf
commit
bea6144e5a
4 changed files with 143 additions and 0 deletions
|
|
@ -10,6 +10,19 @@ export type SkillListItem = {
|
|||
removedAt: number | null;
|
||||
averageRating: number | null;
|
||||
ratingCount: number | null;
|
||||
taskCount: number | null;
|
||||
avgCostUsd: number | null;
|
||||
lastUsedAt: number | null;
|
||||
};
|
||||
|
||||
export type PersonalRating = {
|
||||
id: string;
|
||||
skillId: string;
|
||||
versionId: string | null;
|
||||
stars: number;
|
||||
note: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
export type SkillVersion = {
|
||||
|
|
@ -43,4 +56,8 @@ export const skillRegistryApi = {
|
|||
api.post(`${skillPath(skillId)}/rollback`, { versionId, agentSkillsDir }),
|
||||
remove: (skillId: string) =>
|
||||
api.delete(skillPath(skillId)),
|
||||
getRatings: (skillId: string) =>
|
||||
api.get<PersonalRating[]>(`${skillPath(skillId)}/ratings`),
|
||||
addRating: (skillId: string, body: { stars: number; versionId?: string; note?: string }) =>
|
||||
api.post(`${skillPath(skillId)}/ratings`, body),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ const mockSkill: SkillListItem = {
|
|||
removedAt: null,
|
||||
averageRating: 4.2,
|
||||
ratingCount: 10,
|
||||
taskCount: null,
|
||||
avgCostUsd: null,
|
||||
lastUsedAt: null,
|
||||
};
|
||||
|
||||
describe("SkillCard", () => {
|
||||
|
|
|
|||
65
ui/src/components/StarRating.tsx
Normal file
65
ui/src/components/StarRating.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { Star } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface StarRatingProps {
|
||||
value: number;
|
||||
onChange?: (v: number) => void;
|
||||
readonly?: boolean;
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
export function StarRating({
|
||||
value,
|
||||
onChange,
|
||||
readonly = false,
|
||||
size = "md",
|
||||
}: StarRatingProps) {
|
||||
const iconClass = size === "sm" ? "h-3.5 w-3.5" : "h-5 w-5";
|
||||
|
||||
const stars = (
|
||||
<span className="flex items-center gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((star) => {
|
||||
const filled = star <= value;
|
||||
return (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
aria-label={`Rate ${star} star${star > 1 ? "s" : ""}`}
|
||||
disabled={readonly}
|
||||
onClick={() => onChange?.(star)}
|
||||
className={cn(
|
||||
"focus-visible:ring-ring focus-visible:ring-[3px] rounded outline-none",
|
||||
"hover:bg-accent/10",
|
||||
readonly ? "cursor-default" : "cursor-pointer",
|
||||
)}
|
||||
>
|
||||
<Star
|
||||
className={cn(
|
||||
iconClass,
|
||||
filled ? "fill-amber-400 text-amber-400" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{stars}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{value} out of 5 stars</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return stars;
|
||||
}
|
||||
|
|
@ -127,6 +127,7 @@ import { PageSkeleton } from "@/components/PageSkeleton";
|
|||
import { Identity } from "@/components/Identity";
|
||||
import { SkillCard } from "@/components/SkillCard";
|
||||
import { GroupBadge } from "@/components/GroupBadge";
|
||||
import { StarRating } from "@/components/StarRating";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Section wrapper */
|
||||
|
|
@ -191,6 +192,7 @@ export function DesignGuide() {
|
|||
{ key: "status", label: "Status", value: "Active" },
|
||||
{ key: "priority", label: "Priority", value: "High" },
|
||||
]);
|
||||
const [starValue, setStarValue] = useState(3);
|
||||
|
||||
return (
|
||||
<div className="space-y-10 max-w-4xl">
|
||||
|
|
@ -1275,6 +1277,9 @@ export function DesignGuide() {
|
|||
removedAt: null,
|
||||
averageRating: 4.7,
|
||||
ratingCount: 42,
|
||||
taskCount: null,
|
||||
avgCostUsd: null,
|
||||
lastUsedAt: null,
|
||||
}}
|
||||
onInstall={() => {}}
|
||||
/>
|
||||
|
|
@ -1290,6 +1295,9 @@ export function DesignGuide() {
|
|||
removedAt: null,
|
||||
averageRating: null,
|
||||
ratingCount: null,
|
||||
taskCount: null,
|
||||
avgCostUsd: null,
|
||||
lastUsedAt: null,
|
||||
}}
|
||||
isInstalled
|
||||
onRollback={() => {}}
|
||||
|
|
@ -1307,6 +1315,9 @@ export function DesignGuide() {
|
|||
removedAt: null,
|
||||
averageRating: 3.9,
|
||||
ratingCount: 15,
|
||||
taskCount: null,
|
||||
avgCostUsd: null,
|
||||
lastUsedAt: null,
|
||||
}}
|
||||
isInstalled
|
||||
hasUpdate
|
||||
|
|
@ -1326,6 +1337,9 @@ export function DesignGuide() {
|
|||
removedAt: null,
|
||||
averageRating: 4.2,
|
||||
ratingCount: 38,
|
||||
taskCount: null,
|
||||
avgCostUsd: null,
|
||||
lastUsedAt: null,
|
||||
}}
|
||||
onInstall={() => {}}
|
||||
isLoading
|
||||
|
|
@ -1342,6 +1356,9 @@ export function DesignGuide() {
|
|||
removedAt: null,
|
||||
averageRating: null,
|
||||
ratingCount: null,
|
||||
taskCount: null,
|
||||
avgCostUsd: null,
|
||||
lastUsedAt: null,
|
||||
}}
|
||||
isInstalled
|
||||
hasUpdate
|
||||
|
|
@ -1447,6 +1464,47 @@ export function DesignGuide() {
|
|||
</SubSection>
|
||||
</Section>
|
||||
|
||||
{/* ============================================================ */}
|
||||
{/* RATING SYSTEM */}
|
||||
{/* ============================================================ */}
|
||||
<Section title="Rating System">
|
||||
<SubSection title="Interactive — no selection">
|
||||
<TooltipProvider>
|
||||
<StarRating value={0} onChange={(v) => console.log("rated", v)} />
|
||||
</TooltipProvider>
|
||||
</SubSection>
|
||||
<SubSection title="Interactive — partial selection (controlled)">
|
||||
<TooltipProvider>
|
||||
<StarRating value={starValue} onChange={setStarValue} />
|
||||
</TooltipProvider>
|
||||
<p className="text-xs text-muted-foreground mt-1">Current value: {starValue}</p>
|
||||
</SubSection>
|
||||
<SubSection title="Interactive — full selection">
|
||||
<TooltipProvider>
|
||||
<StarRating value={5} onChange={(v) => console.log("rated", v)} />
|
||||
</TooltipProvider>
|
||||
</SubSection>
|
||||
<SubSection title="Read-only display (value=4)">
|
||||
<TooltipProvider>
|
||||
<StarRating value={4} readonly />
|
||||
</TooltipProvider>
|
||||
</SubSection>
|
||||
<SubSection title="Size comparison — sm vs md">
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">sm</span>
|
||||
<StarRating value={3} readonly size="sm" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">md</span>
|
||||
<StarRating value={3} readonly size="md" />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SubSection>
|
||||
</Section>
|
||||
|
||||
{/* ============================================================ */}
|
||||
{/* KEYBOARD SHORTCUTS */}
|
||||
{/* ============================================================ */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue