diff options
| author | soryu <soryu@soryu.co> | 2026-02-14 21:29:26 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-14 21:29:26 +0000 |
| commit | 9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch) | |
| tree | ef8bed9718c39041191b58a284ee31f5d8d32521 /makima/frontend/src/components/orders | |
| parent | c1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff) | |
| download | soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.tar.gz soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.zip | |
Makima system improvements: Orders, directive questions, PR creation fix, bug fixes (#62)
* feat: soryu-co/soryu - makima: Fix directive goal update bug - stale closure issue
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Create Orders database schema and backend API
* feat: soryu-co/soryu - makima: Fix task Claude instance not receiving user inputs from input box
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Build Orders frontend page replacing the Board page
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Fix directive PR creation system
Diffstat (limited to 'makima/frontend/src/components/orders')
| -rw-r--r-- | makima/frontend/src/components/orders/OrderDetail.tsx | 530 | ||||
| -rw-r--r-- | makima/frontend/src/components/orders/OrderList.tsx | 188 |
2 files changed, 718 insertions, 0 deletions
diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx new file mode 100644 index 0000000..7f8a95d --- /dev/null +++ b/makima/frontend/src/components/orders/OrderDetail.tsx @@ -0,0 +1,530 @@ +import { useState } from "react"; +import type { + Order, + OrderStatus, + OrderPriority, + OrderType, + UpdateOrderRequest, + DirectiveSummary, +} from "../../lib/api"; + +const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = { + open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" }, + in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" }, + done: { color: "text-emerald-400 border-emerald-800", label: "DONE" }, + archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, +}; + +const PRIORITY_OPTIONS: { value: OrderPriority; color: string; label: string }[] = [ + { value: "critical", color: "text-red-400 border-red-800", label: "Critical" }, + { value: "high", color: "text-orange-400 border-orange-800", label: "High" }, + { value: "medium", color: "text-yellow-400 border-yellow-800", label: "Medium" }, + { value: "low", color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "Low" }, + { value: "none", color: "text-[#556677] border-[#2a3a5a]", label: "None" }, +]; + +const TYPE_OPTIONS: { value: OrderType; color: string; label: string }[] = [ + { value: "feature", color: "text-[#75aafc]", label: "Feature" }, + { value: "bug", color: "text-red-400", label: "Bug" }, + { value: "spike", color: "text-yellow-400", label: "Spike" }, + { value: "chore", color: "text-[#7788aa]", label: "Chore" }, + { value: "improvement", color: "text-emerald-400", label: "Improvement" }, +]; + +const STATUS_OPTIONS: OrderStatus[] = ["open", "in_progress", "done", "archived"]; + +interface OrderDetailProps { + order: Order; + directives: DirectiveSummary[]; + onUpdate: (req: UpdateOrderRequest) => Promise<void>; + onDelete: () => void; + onLinkDirective: (directiveId: string) => Promise<void>; + onLinkContract: (contractId: string) => Promise<void>; + onConvertToStep: (directiveId: string) => Promise<void>; + onRefresh: () => void; +} + +export function OrderDetail({ + order, + directives, + onUpdate, + onDelete, + onLinkDirective, + onLinkContract, + onConvertToStep, + onRefresh, +}: OrderDetailProps) { + const [editingTitle, setEditingTitle] = useState(false); + const [titleText, setTitleText] = useState(order.title); + const [editingDesc, setEditingDesc] = useState(false); + const [descText, setDescText] = useState(order.description || ""); + const [editingLabels, setEditingLabels] = useState(false); + const [labelsText, setLabelsText] = useState(order.labels.join(", ")); + const [showLinkDirective, setShowLinkDirective] = useState(false); + const [showLinkContract, setShowLinkContract] = useState(false); + const [contractIdInput, setContractIdInput] = useState(""); + const [showConvertToStep, setShowConvertToStep] = useState(false); + + const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open; + const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4]; + const currentType = TYPE_OPTIONS.find((t) => t.value === order.orderType) || TYPE_OPTIONS[0]; + + const handleTitleSave = async () => { + if (titleText.trim() && titleText !== order.title) { + await onUpdate({ title: titleText.trim() }); + } + setEditingTitle(false); + }; + + const handleDescSave = async () => { + const newDesc = descText.trim() || null; + if (newDesc !== order.description) { + await onUpdate({ description: newDesc }); + } + setEditingDesc(false); + }; + + const handleLabelsSave = async () => { + const newLabels = labelsText + .split(",") + .map((l) => l.trim()) + .filter((l) => l.length > 0); + await onUpdate({ labels: newLabels }); + setEditingLabels(false); + }; + + const handleStatusChange = async (status: OrderStatus) => { + await onUpdate({ status }); + }; + + const handlePriorityChange = async (priority: OrderPriority) => { + await onUpdate({ priority }); + }; + + const handleTypeChange = async (orderType: OrderType) => { + await onUpdate({ orderType }); + }; + + const handleLinkDirective = async (directiveId: string) => { + await onLinkDirective(directiveId); + setShowLinkDirective(false); + }; + + const handleLinkContract = async () => { + if (!contractIdInput.trim()) return; + await onLinkContract(contractIdInput.trim()); + setContractIdInput(""); + setShowLinkContract(false); + }; + + const handleConvertToStep = async (directiveId: string) => { + await onConvertToStep(directiveId); + setShowConvertToStep(false); + }; + + return ( + <div className="flex flex-col h-full overflow-y-auto"> + {/* Header */} + <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]"> + <div className="flex items-center justify-between mb-2"> + {editingTitle ? ( + <div className="flex-1 flex items-center gap-2 pr-2"> + <input + value={titleText} + onChange={(e) => setTitleText(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleTitleSave(); + if (e.key === "Escape") setEditingTitle(false); + }} + autoFocus + className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[14px] font-mono text-white" + /> + <button + type="button" + onClick={handleTitleSave} + className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300" + > + [save] + </button> + <button + type="button" + onClick={() => setEditingTitle(false)} + className="text-[10px] font-mono text-[#556677] hover:text-white" + > + [cancel] + </button> + </div> + ) : ( + <h2 + className="text-[14px] font-mono text-white font-medium truncate pr-2 cursor-pointer hover:text-[#9bc3ff]" + onClick={() => { + setTitleText(order.title); + setEditingTitle(true); + }} + > + {order.title} + </h2> + )} + <div className="flex items-center gap-2 shrink-0"> + <span + className={`text-[10px] font-mono ${badge.color} border rounded px-2 py-0.5`} + > + {badge.label} + </span> + <button + type="button" + onClick={onRefresh} + className="text-[10px] font-mono text-[#7788aa] hover:text-white" + title="Refresh" + > + [refresh] + </button> + </div> + </div> + + {/* Type + Priority inline */} + <div className="flex items-center gap-3 mb-2"> + <span className={`text-[10px] font-mono ${currentType.color}`}> + {currentType.label} + </span> + <span className="text-[10px] font-mono text-[#2a3a5a]">/</span> + <span className={`text-[10px] font-mono ${currentPriority.color} border rounded px-1.5 py-0.5`}> + {currentPriority.label} + </span> + </div> + + {/* Linked entities */} + {order.directiveId && ( + <div className="text-[10px] font-mono text-[#556677] mb-1 truncate"> + Directive: <a href={`/directives/${order.directiveId}`} className="text-[#75aafc] hover:text-white underline">{order.directiveId.slice(0, 8)}...</a> + </div> + )} + {order.contractId && ( + <div className="text-[10px] font-mono text-[#556677] mb-1 truncate"> + Contract: <a href={`/contracts/${order.contractId}`} className="text-[#75aafc] hover:text-white underline">{order.contractId.slice(0, 8)}...</a> + </div> + )} + {order.directiveStepId && ( + <div className="text-[10px] font-mono text-[#556677] mb-1 truncate"> + Step: <span className="text-[#7788aa]">{order.directiveStepId.slice(0, 8)}...</span> + </div> + )} + + {/* Controls */} + <div className="flex flex-wrap gap-2 mt-2"> + <button + type="button" + onClick={onDelete} + className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1 ml-auto" + > + Delete + </button> + </div> + </div> + + {/* Status selector */} + <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="flex items-center justify-between mb-1.5"> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Status + </span> + </div> + <div className="flex gap-1.5 flex-wrap"> + {STATUS_OPTIONS.map((s) => { + const sBadge = STATUS_BADGE[s]; + return ( + <button + key={s} + type="button" + onClick={() => handleStatusChange(s)} + className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${ + s === order.status + ? `${sBadge.color} bg-[rgba(117,170,252,0.1)]` + : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]" + }`} + > + {sBadge.label} + </button> + ); + })} + </div> + </div> + + {/* Priority selector */} + <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="flex items-center justify-between mb-1.5"> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Priority + </span> + </div> + <div className="flex gap-1.5 flex-wrap"> + {PRIORITY_OPTIONS.map((p) => ( + <button + key={p.value} + type="button" + onClick={() => handlePriorityChange(p.value)} + className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${ + p.value === order.priority + ? `${p.color} bg-[rgba(117,170,252,0.1)]` + : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]" + }`} + > + {p.label} + </button> + ))} + </div> + </div> + + {/* Type selector */} + <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="flex items-center justify-between mb-1.5"> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Type + </span> + </div> + <div className="flex gap-1.5 flex-wrap"> + {TYPE_OPTIONS.map((t) => ( + <button + key={t.value} + type="button" + onClick={() => handleTypeChange(t.value)} + className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${ + t.value === order.orderType + ? `${t.color} border-current bg-[rgba(117,170,252,0.1)]` + : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]" + }`} + > + {t.label} + </button> + ))} + </div> + </div> + + {/* Description */} + <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="flex items-center justify-between mb-1"> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Description + </span> + {!editingDesc && ( + <button + type="button" + onClick={() => { + setDescText(order.description || ""); + setEditingDesc(true); + }} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + > + [edit] + </button> + )} + </div> + {editingDesc ? ( + <div className="flex flex-col gap-1.5"> + <textarea + value={descText} + onChange={(e) => setDescText(e.target.value)} + className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white resize-y min-h-[80px]" + rows={4} + autoFocus + /> + <div className="flex gap-1.5"> + <button + type="button" + onClick={handleDescSave} + className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5" + > + Save + </button> + <button + type="button" + onClick={() => setEditingDesc(false)} + className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5" + > + Cancel + </button> + </div> + </div> + ) : ( + <p className="text-[11px] font-mono text-[#c0d0e0] whitespace-pre-wrap"> + {order.description || <span className="text-[#556677] italic">No description</span>} + </p> + )} + </div> + + {/* Labels */} + <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="flex items-center justify-between mb-1"> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Labels + </span> + {!editingLabels && ( + <button + type="button" + onClick={() => { + setLabelsText(order.labels.join(", ")); + setEditingLabels(true); + }} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + > + [edit] + </button> + )} + </div> + {editingLabels ? ( + <div className="flex flex-col gap-1.5"> + <input + value={labelsText} + onChange={(e) => setLabelsText(e.target.value)} + placeholder="label1, label2, ..." + className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white" + autoFocus + /> + <div className="flex gap-1.5"> + <button + type="button" + onClick={handleLabelsSave} + className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5" + > + Save + </button> + <button + type="button" + onClick={() => setEditingLabels(false)} + className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5" + > + Cancel + </button> + </div> + </div> + ) : ( + <div className="flex gap-1 flex-wrap"> + {order.labels.length > 0 ? ( + order.labels.map((l) => ( + <span + key={l} + className="text-[10px] font-mono text-[#9bc3ff] bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5" + > + {l} + </span> + )) + ) : ( + <span className="text-[10px] font-mono text-[#556677] italic">No labels</span> + )} + </div> + )} + </div> + + {/* Actions */} + <div className="px-4 py-3 flex-1"> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2"> + Actions + </span> + + <div className="flex flex-col gap-2"> + {/* Link to Directive */} + <div> + <button + type="button" + onClick={() => setShowLinkDirective(!showLinkDirective)} + className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 w-full text-left" + > + Link to Directive + </button> + {showLinkDirective && ( + <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto rounded"> + {directives.length === 0 ? ( + <div className="px-3 py-2 text-[10px] font-mono text-[#556677]"> + No directives available + </div> + ) : ( + directives.map((d) => ( + <button + key={d.id} + type="button" + onClick={() => handleLinkDirective(d.id)} + className="w-full text-left px-3 py-1.5 text-[10px] font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0" + > + {d.title} + </button> + )) + )} + </div> + )} + </div> + + {/* Link to Contract */} + <div> + <button + type="button" + onClick={() => setShowLinkContract(!showLinkContract)} + className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 w-full text-left" + > + Link to Contract + </button> + {showLinkContract && ( + <div className="mt-1 flex gap-1.5"> + <input + value={contractIdInput} + onChange={(e) => setContractIdInput(e.target.value)} + placeholder="Contract ID..." + className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[10px] font-mono text-white" + autoFocus + /> + <button + type="button" + onClick={handleLinkContract} + disabled={!contractIdInput.trim()} + className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-1 disabled:opacity-50" + > + Link + </button> + </div> + )} + </div> + + {/* Convert to Directive Step */} + {!order.directiveStepId && ( + <div> + <button + type="button" + onClick={() => setShowConvertToStep(!showConvertToStep)} + className="text-[10px] font-mono text-yellow-400 hover:text-yellow-300 border border-yellow-800 rounded px-2 py-1 w-full text-left" + > + Convert to Directive Step + </button> + {showConvertToStep && ( + <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto rounded"> + {directives.length === 0 ? ( + <div className="px-3 py-2 text-[10px] font-mono text-[#556677]"> + No directives available + </div> + ) : ( + directives.map((d) => ( + <button + key={d.id} + type="button" + onClick={() => handleConvertToStep(d.id)} + className="w-full text-left px-3 py-1.5 text-[10px] font-mono text-yellow-400 hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0" + > + {d.title} + </button> + )) + )} + </div> + )} + </div> + )} + </div> + </div> + + {/* Metadata */} + <div className="px-4 py-2 border-t border-[rgba(117,170,252,0.1)]"> + <div className="flex items-center justify-between text-[9px] font-mono text-[#556677]"> + <span>Created {new Date(order.createdAt).toLocaleDateString()}</span> + <span>Updated {new Date(order.updatedAt).toLocaleDateString()}</span> + </div> + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx new file mode 100644 index 0000000..76ac7a7 --- /dev/null +++ b/makima/frontend/src/components/orders/OrderList.tsx @@ -0,0 +1,188 @@ +import { useState, useMemo } from "react"; +import type { Order, OrderStatus, OrderPriority, OrderType } from "../../lib/api"; + +const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = { + open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" }, + in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" }, + done: { color: "text-emerald-400 border-emerald-800", label: "DONE" }, + archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, +}; + +const PRIORITY_COLOR: Record<OrderPriority, string> = { + critical: "bg-red-400", + high: "bg-orange-400", + medium: "bg-yellow-400", + low: "bg-[#75aafc]", + none: "bg-[#556677]", +}; + +const TYPE_BADGE: Record<OrderType, { color: string; label: string }> = { + feature: { color: "text-[#75aafc] border-[rgba(117,170,252,0.3)]", label: "FEAT" }, + bug: { color: "text-red-400 border-red-800", label: "BUG" }, + spike: { color: "text-yellow-400 border-yellow-800", label: "SPIKE" }, + chore: { color: "text-[#7788aa] border-[#2a3a5a]", label: "CHORE" }, + improvement: { color: "text-emerald-400 border-emerald-800", label: "IMPROVE" }, +}; + +interface OrderListProps { + orders: Order[]; + selectedId: string | null; + onSelect: (id: string) => void; + onCreate: () => void; + statusFilter: OrderStatus | undefined; + onStatusFilter: (s: OrderStatus | undefined) => void; + typeFilter: OrderType | undefined; + onTypeFilter: (t: OrderType | undefined) => void; +} + +const STATUS_OPTIONS: (OrderStatus | "all")[] = ["all", "open", "in_progress", "done", "archived"]; +const TYPE_OPTIONS: (OrderType | "all")[] = ["all", "feature", "bug", "spike", "chore", "improvement"]; + +export function OrderList({ + orders, + selectedId, + onSelect, + onCreate, + statusFilter, + onStatusFilter, + typeFilter, + onTypeFilter, +}: OrderListProps) { + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + if (!search.trim()) return orders; + const q = search.toLowerCase(); + return orders.filter( + (o) => + o.title.toLowerCase().includes(q) || + (o.description && o.description.toLowerCase().includes(q)) || + o.labels.some((l) => l.toLowerCase().includes(q)), + ); + }, [orders, search]); + + return ( + <div className="flex flex-col h-full"> + {/* Header */} + <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]"> + <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Orders + </span> + <button + type="button" + onClick={onCreate} + className="text-[11px] font-mono text-[#75aafc] hover:text-white bg-transparent border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5 hover:border-[rgba(117,170,252,0.6)] transition-colors" + > + + New + </button> + </div> + + {/* Search */} + <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.1)]"> + <input + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder="Search orders..." + className="w-full bg-transparent border-none outline-none text-[11px] font-mono text-white placeholder:text-[#556677]" + /> + </div> + + {/* Filters */} + <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.1)] flex flex-col gap-1"> + <div className="flex items-center gap-1 flex-wrap"> + <span className="text-[9px] font-mono text-[#556677] uppercase w-[38px] shrink-0"> + Status + </span> + {STATUS_OPTIONS.map((s) => ( + <button + key={s} + type="button" + onClick={() => onStatusFilter(s === "all" ? undefined : s)} + className={`text-[9px] font-mono px-1.5 py-0.5 rounded transition-colors ${ + (s === "all" && !statusFilter) || s === statusFilter + ? "text-white bg-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.4)]" + : "text-[#556677] hover:text-[#7788aa] border border-transparent" + }`} + > + {s === "all" ? "ALL" : s === "in_progress" ? "WIP" : s.toUpperCase()} + </button> + ))} + </div> + <div className="flex items-center gap-1 flex-wrap"> + <span className="text-[9px] font-mono text-[#556677] uppercase w-[38px] shrink-0"> + Type + </span> + {TYPE_OPTIONS.map((t) => ( + <button + key={t} + type="button" + onClick={() => onTypeFilter(t === "all" ? undefined : t)} + className={`text-[9px] font-mono px-1.5 py-0.5 rounded transition-colors ${ + (t === "all" && !typeFilter) || t === typeFilter + ? "text-white bg-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.4)]" + : "text-[#556677] hover:text-[#7788aa] border border-transparent" + }`} + > + {t === "all" ? "ALL" : t.toUpperCase()} + </button> + ))} + </div> + </div> + + {/* List */} + <div className="flex-1 overflow-y-auto"> + {filtered.length === 0 ? ( + <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]"> + No orders found + </div> + ) : ( + filtered.map((o) => { + const statusBadge = STATUS_BADGE[o.status] || STATUS_BADGE.open; + const typeBadge = TYPE_BADGE[o.orderType] || TYPE_BADGE.feature; + const priorityColor = PRIORITY_COLOR[o.priority] || PRIORITY_COLOR.none; + + return ( + <button + key={o.id} + type="button" + onClick={() => onSelect(o.id)} + className={`w-full text-left px-3 py-2.5 border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] transition-colors ${ + selectedId === o.id ? "bg-[rgba(117,170,252,0.1)]" : "" + }`} + > + <div className="flex items-start gap-2 mb-1"> + {/* Priority dot */} + <span + className={`w-2 h-2 rounded-full ${priorityColor} shrink-0 mt-[3px]`} + title={o.priority} + /> + <span className="text-[12px] font-mono text-white truncate flex-1"> + {o.title} + </span> + </div> + <div className="flex items-center gap-1.5 pl-4"> + <span + className={`text-[9px] font-mono ${statusBadge.color} border rounded px-1.5 py-0.5`} + > + {statusBadge.label} + </span> + <span + className={`text-[9px] font-mono ${typeBadge.color} border rounded px-1.5 py-0.5`} + > + {typeBadge.label} + </span> + {o.labels.length > 0 && ( + <span className="text-[9px] font-mono text-[#556677] truncate"> + {o.labels.slice(0, 2).join(", ")} + {o.labels.length > 2 && ` +${o.labels.length - 2}`} + </span> + )} + </div> + </button> + ); + }) + )} + </div> + </div> + ); +} |
