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;
|
removedAt: number | null;
|
||||||
averageRating: number | null;
|
averageRating: number | null;
|
||||||
ratingCount: 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 = {
|
export type SkillVersion = {
|
||||||
|
|
@ -43,4 +56,8 @@ export const skillRegistryApi = {
|
||||||
api.post(`${skillPath(skillId)}/rollback`, { versionId, agentSkillsDir }),
|
api.post(`${skillPath(skillId)}/rollback`, { versionId, agentSkillsDir }),
|
||||||
remove: (skillId: string) =>
|
remove: (skillId: string) =>
|
||||||
api.delete(skillPath(skillId)),
|
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,
|
removedAt: null,
|
||||||
averageRating: 4.2,
|
averageRating: 4.2,
|
||||||
ratingCount: 10,
|
ratingCount: 10,
|
||||||
|
taskCount: null,
|
||||||
|
avgCostUsd: null,
|
||||||
|
lastUsedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("SkillCard", () => {
|
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 { Identity } from "@/components/Identity";
|
||||||
import { SkillCard } from "@/components/SkillCard";
|
import { SkillCard } from "@/components/SkillCard";
|
||||||
import { GroupBadge } from "@/components/GroupBadge";
|
import { GroupBadge } from "@/components/GroupBadge";
|
||||||
|
import { StarRating } from "@/components/StarRating";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Section wrapper */
|
/* Section wrapper */
|
||||||
|
|
@ -191,6 +192,7 @@ export function DesignGuide() {
|
||||||
{ key: "status", label: "Status", value: "Active" },
|
{ key: "status", label: "Status", value: "Active" },
|
||||||
{ key: "priority", label: "Priority", value: "High" },
|
{ key: "priority", label: "Priority", value: "High" },
|
||||||
]);
|
]);
|
||||||
|
const [starValue, setStarValue] = useState(3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10 max-w-4xl">
|
<div className="space-y-10 max-w-4xl">
|
||||||
|
|
@ -1275,6 +1277,9 @@ export function DesignGuide() {
|
||||||
removedAt: null,
|
removedAt: null,
|
||||||
averageRating: 4.7,
|
averageRating: 4.7,
|
||||||
ratingCount: 42,
|
ratingCount: 42,
|
||||||
|
taskCount: null,
|
||||||
|
avgCostUsd: null,
|
||||||
|
lastUsedAt: null,
|
||||||
}}
|
}}
|
||||||
onInstall={() => {}}
|
onInstall={() => {}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1290,6 +1295,9 @@ export function DesignGuide() {
|
||||||
removedAt: null,
|
removedAt: null,
|
||||||
averageRating: null,
|
averageRating: null,
|
||||||
ratingCount: null,
|
ratingCount: null,
|
||||||
|
taskCount: null,
|
||||||
|
avgCostUsd: null,
|
||||||
|
lastUsedAt: null,
|
||||||
}}
|
}}
|
||||||
isInstalled
|
isInstalled
|
||||||
onRollback={() => {}}
|
onRollback={() => {}}
|
||||||
|
|
@ -1307,6 +1315,9 @@ export function DesignGuide() {
|
||||||
removedAt: null,
|
removedAt: null,
|
||||||
averageRating: 3.9,
|
averageRating: 3.9,
|
||||||
ratingCount: 15,
|
ratingCount: 15,
|
||||||
|
taskCount: null,
|
||||||
|
avgCostUsd: null,
|
||||||
|
lastUsedAt: null,
|
||||||
}}
|
}}
|
||||||
isInstalled
|
isInstalled
|
||||||
hasUpdate
|
hasUpdate
|
||||||
|
|
@ -1326,6 +1337,9 @@ export function DesignGuide() {
|
||||||
removedAt: null,
|
removedAt: null,
|
||||||
averageRating: 4.2,
|
averageRating: 4.2,
|
||||||
ratingCount: 38,
|
ratingCount: 38,
|
||||||
|
taskCount: null,
|
||||||
|
avgCostUsd: null,
|
||||||
|
lastUsedAt: null,
|
||||||
}}
|
}}
|
||||||
onInstall={() => {}}
|
onInstall={() => {}}
|
||||||
isLoading
|
isLoading
|
||||||
|
|
@ -1342,6 +1356,9 @@ export function DesignGuide() {
|
||||||
removedAt: null,
|
removedAt: null,
|
||||||
averageRating: null,
|
averageRating: null,
|
||||||
ratingCount: null,
|
ratingCount: null,
|
||||||
|
taskCount: null,
|
||||||
|
avgCostUsd: null,
|
||||||
|
lastUsedAt: null,
|
||||||
}}
|
}}
|
||||||
isInstalled
|
isInstalled
|
||||||
hasUpdate
|
hasUpdate
|
||||||
|
|
@ -1447,6 +1464,47 @@ export function DesignGuide() {
|
||||||
</SubSection>
|
</SubSection>
|
||||||
</Section>
|
</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 */}
|
{/* KEYBOARD SHORTCUTS */}
|
||||||
{/* ============================================================ */}
|
{/* ============================================================ */}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue