feat(ui): migrate virtual-keys (VirtualKeysTable + BudgetWindowsEditor) to shadcn

Scope: section-2 stress-test of the locked phase-1 blueprint.

- VirtualKeysTable.tsx (700+ lines):
  - @tremor/react Table + TableHead/Body/Row/Cell \u2192 shadcn Table equivalents
  - @tremor/react Badge / Icon / Text / Button \u2192 shadcn Badge + lucide icons +
    Tailwind semantic tokens
  - @heroicons/react ChevronDown/ChevronRight/ChevronUp/SwitchVertical \u2192 lucide
    Chevron* + ChevronsUpDown
  - @ant-design/icons InfoCircleOutlined / SyncOutlined \u2192 lucide Info / RefreshCw
  - antd Popover / Tooltip / Button / Skeleton / Typography \u2192 shadcn equivalents
    from @/components/ui/*. CopyableText helper replaces antd Typography.Text
    copyable semantics (clipboard + sonner toast).
  - Raw color classes (text-blue-500, bg-blue-50, text-gray-700, hover:bg-gray-50,
    bg-white) replaced with semantic tokens (text-primary, bg-primary/10,
    text-muted-foreground, hover:bg-muted, bg-background).
- BudgetWindowsEditor.tsx: antd Button/InputNumber/Select \u2192 shadcn
  Button/Input/Select. Antd's InputNumber `prefix="$"` preserved as absolute-
  positioned span adjacent to the input (shadcn Input has no prefix prop).

Blueprint stress-test result: NO new patterns required. Every antd /
@tremor / @heroicons / @ant-design/icons import encountered maps cleanly
to an entry already in BLUEPRINT.md. The blueprint is considered final
and is now permanently locked for the rest of phase 1.

Gates: TS \u2713 | Lint \u2713 | Vitest 44/44 \u2713 | Build \u2713. Playwright
parity + snapshot gates deferred per the DEVIATIONS.md environmental-gate
skip.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 07:04:04 +00:00
parent f49d66daea
commit b8d48fecca
No known key found for this signature in database
3 changed files with 732 additions and 583 deletions

View file

@ -23,5 +23,16 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest**
## 2. Virtual Keys (api-keys)
_(pending)_
- Cycles used: 1 / 7
- Layer outcomes per cycle:
- cycle 1: TS ✓ | Lint ✓ | Vitest ✓ (44/44) | Parity ⏭ | Snap ⏭
- Final status: **done (scoped to VirtualKeysTable + BudgetWindowsEditor)**
- Section scope: only the files directly under `src/components/VirtualKeysPage/` and
`src/components/key_team_helpers/` that imported banned libs. Heavier key-related
sub-components (e.g. `templates/key_info_view`, `organisms/create_key_button`,
`KeyAliasSelect/*`, `DeletedKeysPage/*`) are outside this section's file set and
will be picked up by either their own future sections or the final chrome sweep.
- Blueprint stress-test outcome: no new patterns required. The existing blueprint
(Table + @tanstack/react-table, Popover, Tooltip, Badge variants, Skeleton) covered
every antd / @tremor / @heroicons import encountered.

View file

@ -1,5 +1,14 @@
import { Button, InputNumber, Select } from "antd";
import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { X } from "lucide-react";
export interface BudgetWindowEntry {
budget_duration: string;
@ -7,10 +16,10 @@ export interface BudgetWindowEntry {
}
export const BUDGET_WINDOW_OPTIONS = [
{ value: "1h", label: "Hourly", resetHint: "Resets every hour" },
{ value: "24h", label: "Daily", resetHint: "Resets daily at midnight UTC" },
{ value: "7d", label: "Weekly", resetHint: "Resets every Sunday at midnight UTC" },
{ value: "30d", label: "Monthly", resetHint: "Resets on the 1st of every month at midnight UTC" },
{ value: "1h", label: "Hourly", resetHint: "Resets every hour" },
{ value: "24h", label: "Daily", resetHint: "Resets daily at midnight UTC" },
{ value: "7d", label: "Weekly", resetHint: "Resets every Sunday at midnight UTC" },
{ value: "30d", label: "Monthly", resetHint: "Resets on the 1st of every month at midnight UTC" },
];
interface BudgetWindowsEditorProps {
@ -27,7 +36,11 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
onChange(value.filter((_, i) => i !== idx));
};
const updateWindow = (idx: number, field: keyof BudgetWindowEntry, fieldValue: string | number | null) => {
const updateWindow = (
idx: number,
field: keyof BudgetWindowEntry,
fieldValue: string | number | null,
) => {
const updated = value.map((w, i) => (i === idx ? { ...w, [field]: fieldValue } : w));
onChange(updated);
};
@ -35,38 +48,57 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
return (
<div>
{value.map((window, idx) => {
const hint = BUDGET_WINDOW_OPTIONS.find((o) => o.value === window.budget_duration)?.resetHint;
const hint = BUDGET_WINDOW_OPTIONS.find(
(o) => o.value === window.budget_duration,
)?.resetHint;
return (
<div key={idx} style={{ marginBottom: 12 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div key={idx} className="mb-3">
<div className="flex gap-2 items-center">
<Select
value={window.budget_duration}
onChange={(v) => updateWindow(idx, "budget_duration", v)}
style={{ width: 130 }}
options={BUDGET_WINDOW_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
/>
<InputNumber
step={0.01}
min={0}
precision={2}
value={window.max_budget ?? undefined}
onChange={(v) => updateWindow(idx, "max_budget", v ?? null)}
placeholder="Max spend ($)"
style={{ width: 160 }}
prefix="$"
/>
<Button
type="text"
danger
size="small"
onClick={() => removeWindow(idx)}
style={{ padding: "0 4px" }}
onValueChange={(v) => updateWindow(idx, "budget_duration", v)}
>
✕
<SelectTrigger className="w-[130px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{BUDGET_WINDOW_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative w-[160px]">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm pointer-events-none">
$
</span>
<Input
type="number"
step={0.01}
min={0}
value={window.max_budget ?? ""}
onChange={(e) => {
const v = e.target.value;
updateWindow(idx, "max_budget", v === "" ? null : Number(v));
}}
placeholder="Max spend"
className="pl-6"
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeWindow(idx)}
className="text-destructive hover:text-destructive"
aria-label="Remove budget window"
>
<X size={14} />
</Button>
</div>
{hint && (
<div style={{ fontSize: 11, color: "#888", marginTop: 3, marginLeft: 2 }}>
<div className="text-[11px] text-muted-foreground mt-1 ml-0.5">
↻ {hint}
</div>
)}
@ -74,8 +106,13 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
);
})}
<Button
size="small"
onClick={(e) => { e.preventDefault(); addWindow(); }}
type="button"
variant="outline"
size="sm"
onClick={(e) => {
e.preventDefault();
addWindow();
}}
>
+ Add Budget Window
</Button>