Restyle remainder of app to match new dashboard

This commit is contained in:
2026-07-18 13:16:10 -04:00
parent cde8148196
commit d779228485
75 changed files with 437 additions and 343 deletions
@@ -163,17 +163,17 @@ export function AiDescriptionCompare({
</div>
{/* Right: AI suggestion */}
<div className="bg-purple-50/80 dark:bg-purple-950/30 flex flex-col w-full lg:w-1/2">
<div className="bg-purple-50/80 flex flex-col w-full lg:w-1/2">
{/* Measured header + issues area (height mirrored as spacer on the left) */}
<div ref={aiHeaderRef} className="flex-shrink-0">
{/* Header */}
<div className="w-full flex items-center justify-between px-3 py-2">
<div className="flex items-center gap-2">
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
<span className="text-xs font-medium text-purple-600 dark:text-purple-400">
<span className="text-xs font-medium text-purple-600">
AI Suggestion
</span>
<span className="text-xs text-purple-500 dark:text-purple-400">
<span className="text-xs text-purple-500">
({issues.length} {issues.length === 1 ? "issue" : "issues"})
</span>
</div>
@@ -185,7 +185,7 @@ export function AiDescriptionCompare({
{issues.map((issue, index) => (
<div
key={index}
className="flex items-start gap-1.5 text-xs text-purple-600 dark:text-purple-400"
className="flex items-start gap-1.5 text-xs text-purple-600"
>
<AlertCircle className="h-3 w-3 mt-0.5 flex-shrink-0 text-purple-400" />
<span>{issue}</span>
@@ -199,7 +199,7 @@ export function AiDescriptionCompare({
<div className="px-3 pb-3 flex flex-col flex-1 gap-3">
{/* Editable suggestion */}
<div className="flex flex-col flex-1">
<div className="text-sm text-purple-500 dark:text-purple-400 mb-1 font-medium flex-shrink-0">
<div className="text-sm text-purple-500 mb-1 font-medium flex-shrink-0">
Suggested (editable):
</div>
<Textarea
@@ -209,7 +209,7 @@ export function AiDescriptionCompare({
setEditedSuggestion(e.target.value);
syncTextareaHeights();
}}
className="overflow-y-auto overscroll-contain text-sm bg-white dark:bg-black/20 border-purple-200 dark:border-purple-700 focus-visible:ring-purple-400 resize-y min-h-[120px] max-h-[50vh]"
className="overflow-y-auto overscroll-contain text-sm bg-white border-purple-200 focus-visible:ring-purple-400 resize-y min-h-[120px] max-h-[50vh]"
/>
</div>
@@ -218,7 +218,7 @@ export function AiDescriptionCompare({
<Button
size="sm"
variant="outline"
className="h-7 px-3 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 dark:bg-green-950/30 dark:border-green-700 dark:text-green-400"
className="h-7 px-3 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400"
onClick={() => onAccept(editedSuggestion)}
>
<Check className="h-3 w-3 mr-1" />
@@ -227,7 +227,7 @@ export function AiDescriptionCompare({
<Button
size="sm"
variant="ghost"
className="h-7 px-3 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
className="h-7 px-3 text-xs text-muted-foreground hover:text-foreground"
onClick={onDismiss}
>
Ignore
@@ -236,7 +236,7 @@ export function AiDescriptionCompare({
<Button
size="sm"
variant="ghost"
className="h-7 px-3 text-xs text-purple-500 hover:text-purple-700 dark:text-purple-400"
className="h-7 px-3 text-xs text-purple-500 hover:text-purple-700"
disabled={isRevalidating}
onClick={onRevalidate}
>
@@ -209,7 +209,7 @@ export function BulkEditRow({
{state.result!.issues.map((issue, i) => (
<div
key={i}
className="flex items-start gap-1 text-[11px] text-purple-600 dark:text-purple-400"
className="flex items-start gap-1 text-[11px] text-purple-600"
>
<AlertCircle className="h-3 w-3 mt-0.5 flex-shrink-0 text-purple-400" />
<span>{issue}</span>
@@ -223,12 +223,12 @@ export function BulkEditRow({
<Input
value={state.editedSuggestion ?? state.result!.suggestion!}
onChange={(e) => onEditSuggestion(product.pid, e.target.value)}
className="h-7 text-sm flex-1 border-purple-200 dark:border-purple-700 focus-visible:ring-purple-400 bg-purple-50/50 dark:bg-purple-950/20"
className="h-7 text-sm flex-1 border-purple-200 focus-visible:ring-purple-400 bg-purple-50/50"
/>
<Button
size="sm"
variant="outline"
className="h-7 px-2 text-xs shrink-0 bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 dark:bg-green-950/30 dark:border-green-700 dark:text-green-400"
className="h-7 px-2 text-xs shrink-0 bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400"
onClick={() =>
onAccept(product.pid, state.editedSuggestion ?? state.result!.suggestion!)
}
@@ -239,7 +239,7 @@ export function BulkEditRow({
<Button
size="sm"
variant="ghost"
className="h-7 px-1.5 text-xs text-gray-500 hover:text-gray-700 shrink-0"
className="h-7 px-1.5 text-xs text-muted-foreground hover:text-foreground shrink-0"
onClick={() => onDismiss(product.pid)}
>
<X className="h-3 w-3" />
@@ -254,11 +254,11 @@ export function BulkEditRow({
if (!showSuggestion) return null;
return (
<div className="flex flex-col gap-1.5 min-w-0 flex-1 bg-purple-50/60 dark:bg-purple-950/20 rounded-md p-2">
<div className="flex flex-col gap-1.5 min-w-0 flex-1 bg-purple-50/60 rounded-md p-2">
{/* Header */}
<div className="flex items-center gap-1.5">
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
<span className="text-xs font-medium text-purple-600 dark:text-purple-400">
<span className="text-xs font-medium text-purple-600">
AI Suggestion
</span>
{state.result!.issues.length > 0 && (
@@ -274,7 +274,7 @@ export function BulkEditRow({
{state.result!.issues.map((issue, i) => (
<div
key={i}
className="flex items-start gap-1 text-[11px] text-purple-600 dark:text-purple-400"
className="flex items-start gap-1 text-[11px] text-purple-600"
>
<AlertCircle className="h-3 w-3 mt-0.5 flex-shrink-0 text-purple-400" />
<span>{issue}</span>
@@ -287,7 +287,7 @@ export function BulkEditRow({
<Textarea
value={state.editedSuggestion ?? state.result!.suggestion!}
onChange={(e) => onEditSuggestion(product.pid, e.target.value)}
className="text-sm min-h-[60px] max-h-[120px] resize-y border-purple-200 dark:border-purple-700 focus-visible:ring-purple-400 bg-white dark:bg-black/20"
className="text-sm min-h-[60px] max-h-[120px] resize-y border-purple-200 focus-visible:ring-purple-400 bg-white"
rows={2}
/>
@@ -296,7 +296,7 @@ export function BulkEditRow({
<Button
size="sm"
variant="outline"
className="h-7 px-2 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 dark:bg-green-950/30 dark:border-green-700 dark:text-green-400"
className="h-7 px-2 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400"
onClick={() =>
onAccept(product.pid, state.editedSuggestion ?? state.result!.suggestion!)
}
@@ -307,7 +307,7 @@ export function BulkEditRow({
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-xs text-gray-500 hover:text-gray-700"
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => onDismiss(product.pid)}
>
Dismiss
+4 -3
View File
@@ -210,7 +210,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
case 'd':
return <MessageSquare className="h-4 w-4 text-green-500" />;
default:
return <Hash className="h-4 w-4 text-gray-500" />;
return <Hash className="h-4 w-4 text-muted-foreground" />;
}
};
@@ -284,7 +284,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
return (
<div className="mt-2 space-y-2">
{urls.map((urlData, index) => (
<div key={index} className="border rounded-lg p-3 bg-gray-50">
<div key={index} className="border rounded-lg p-3 bg-surface-subtle">
<div className="flex items-start gap-2">
<ExternalLink className="h-4 w-4 mt-1 text-blue-500 flex-shrink-0" />
<div className="min-w-0 flex-1">
@@ -335,7 +335,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
};
return (
<div key={index} className="border rounded-lg p-3 bg-gray-50">
<div key={index} className="border rounded-lg p-3 bg-surface-subtle">
<div className="flex items-center gap-2">
{isImage ? (
<Image className="h-4 w-4 text-green-500" />
@@ -508,6 +508,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
<div className="flex gap-2 mt-2">
<Input
placeholder="Search messages..."
className="rounded-full px-3.5"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && searchMessages()}
+2 -2
View File
@@ -62,7 +62,7 @@ export function ChatTest({ selectedUserId }: ChatTestProps) {
case 'd':
return <MessageSquare className="h-4 w-4 text-green-500" />;
default:
return <Users className="h-4 w-4 text-gray-500" />;
return <Users className="h-4 w-4 text-muted-foreground" />;
}
};
@@ -139,7 +139,7 @@ export function ChatTest({ selectedUserId }: ChatTestProps) {
{rooms.map((room) => (
<div
key={room.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
className="flex items-center justify-between p-3 border rounded-lg hover:bg-surface-subtle"
>
<div className="flex items-center gap-3">
{getRoomIcon(room.type)}
+4 -4
View File
@@ -193,8 +193,8 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL
);
default:
return (
<div className="h-12 w-12 bg-gray-50 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-gray-500" />
<div className="h-12 w-12 bg-surface-subtle rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-muted-foreground" />
</div>
);
}
@@ -268,7 +268,7 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL
return (
<div key={title} className="space-y-0.5">
<div className="flex items-center gap-2 px-2 py-1 border-b border-gray-100">
<div className="flex items-center gap-2 px-2 py-1 border-b border-muted">
{icon}
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{title} ({rooms.length})
@@ -281,7 +281,7 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL
onClick={() => onRoomSelect(room.id.toString())}
className={`
flex items-center justify-between py-0.5 px-3 rounded-lg cursor-pointer transition-colors
hover:bg-gray-100
hover:bg-muted
${selectedRoomId === room.id.toString() ? 'bg-blue-50 border-l-4 border-blue-500' : ''}
${(room.open === false || room.archived === true) ? 'opacity-60' : ''}
`}
@@ -35,7 +35,7 @@ export function SearchResults({ results, query, onClose, onRoomSelect }: SearchR
case 'd':
return <MessageSquare className="h-3 w-3 text-green-500" />;
default:
return <Hash className="h-3 w-3 text-gray-500" />;
return <Hash className="h-3 w-3 text-muted-foreground" />;
}
};
@@ -85,7 +85,7 @@ export function SearchResults({ results, query, onClose, onRoomSelect }: SearchR
{results.map((result) => (
<div
key={result.id}
className="border rounded-lg p-3 hover:bg-gray-50 cursor-pointer"
className="border rounded-lg p-3 hover:bg-surface-subtle cursor-pointer"
onClick={() => {
onRoomSelect(result.room_id.toString());
onClose();
@@ -50,7 +50,7 @@ export function BusinessRangeSelect({
<SelectTrigger
id={id}
className={cn(
"h-8 w-auto gap-1.5 rounded-full border-[#e7e5e1] bg-white px-3.5 text-xs font-semibold text-[#2b2925] shadow-none hover:bg-[#faf9f7] focus:ring-1 focus:ring-[#e06a4e]/30 focus:ring-offset-0",
"h-8 w-auto gap-1.5 rounded-full border-border bg-card px-3.5 text-xs font-semibold text-foreground shadow-none hover:bg-surface-subtle focus:ring-1 focus:ring-coral/30 focus:ring-offset-0",
className
)}
>
@@ -112,22 +112,22 @@ export const DashboardSectionHeader: React.FC<DashboardSectionHeaderProps> = ({
}) => {
const paddingClass = compact ? "px-4 py-2.5" : "px-4 py-3";
const titleClass = size === "large"
? "text-[14px] font-semibold tracking-tight text-[#2b2925]"
: "text-[12.5px] font-semibold tracking-tight text-[#2b2925]";
? "text-[14px] font-semibold tracking-tight text-foreground"
: "text-[12.5px] font-semibold tracking-tight text-foreground";
void accent; // Studio design has no accent bar; prop kept for call-site compat
// Loading skeleton
if (loading) {
return (
<CardHeader className={cn("border-b border-[#f0eeea]", paddingClass, className)}>
<CardHeader className={cn("border-b border-muted", paddingClass, className)}>
<div className="flex justify-between items-start">
<div className="space-y-1">
<Skeleton className="h-5 w-40 bg-[#f0eeea]" />
{description && <Skeleton className="h-4 w-56 bg-[#f5f3f0]" />}
<Skeleton className="h-5 w-40 bg-muted" />
{description && <Skeleton className="h-4 w-56 bg-muted/60" />}
</div>
<div className="flex items-center gap-2">
{timeSelector && <Skeleton className="h-8 w-[130px] bg-[#f0eeea] rounded-full" />}
{actions && <Skeleton className="h-8 w-20 bg-[#f0eeea] rounded-full" />}
{timeSelector && <Skeleton className="h-8 w-[130px] bg-muted rounded-full" />}
{actions && <Skeleton className="h-8 w-20 bg-muted rounded-full" />}
</div>
</div>
</CardHeader>
@@ -137,14 +137,14 @@ export const DashboardSectionHeader: React.FC<DashboardSectionHeaderProps> = ({
const hasRightContent = timeSelector || actions;
return (
<CardHeader className={cn("border-b border-[#f0eeea]", paddingClass, className)}>
<CardHeader className={cn("border-b border-muted", paddingClass, className)}>
<div className="flex flex-row items-center justify-between gap-3">
{/* Left side: title (+ optional description / updated time inline) */}
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline gap-x-2.5">
<CardTitle className={titleClass}>{title}</CardTitle>
{lastUpdated && !loading && (
<span className="text-[10.5px] text-[#a09b92]">
<span className="text-[10.5px] text-muted-foreground/75">
{lastUpdatedFormat(lastUpdated)}
</span>
)}
@@ -191,15 +191,15 @@ export const DashboardSectionHeaderSkeleton: React.FC<DashboardSectionHeaderSkel
const paddingClass = compact ? "px-4 py-2.5" : "px-4 py-3";
return (
<CardHeader className={cn("border-b border-[#f0eeea]", paddingClass, className)}>
<CardHeader className={cn("border-b border-muted", paddingClass, className)}>
<div className="flex justify-between items-start">
<div className="space-y-2">
<Skeleton className="h-5 w-40 bg-[#f0eeea]" />
{hasDescription && <Skeleton className="h-4 w-56 bg-[#f5f3f0]" />}
<Skeleton className="h-5 w-40 bg-muted" />
{hasDescription && <Skeleton className="h-4 w-56 bg-muted/60" />}
</div>
<div className="flex items-center gap-2">
{hasTimeSelector && <Skeleton className="h-8 w-[130px] bg-[#f0eeea] rounded-full" />}
{hasActions && <Skeleton className="h-8 w-20 bg-[#f0eeea] rounded-full" />}
{hasTimeSelector && <Skeleton className="h-8 w-[130px] bg-muted rounded-full" />}
{hasActions && <Skeleton className="h-8 w-20 bg-muted rounded-full" />}
</div>
</div>
</CardHeader>
@@ -136,9 +136,9 @@ export const DashboardStatCard: React.FC<DashboardStatCardProps> = ({
: "text-[20px]";
const cardClass = cn(
"rounded-[14px] border bg-white px-3.5 py-3",
"rounded-[14px] border bg-card px-3.5 py-3",
onClick &&
"cursor-pointer transition-colors duration-150 hover:bg-[#faf9f7] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"cursor-pointer transition-colors duration-150 hover:bg-surface-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
className
);
const cardStyle = { borderColor: STUDIO_COLORS.border };
@@ -147,9 +147,9 @@ export const DashboardStatCard: React.FC<DashboardStatCardProps> = ({
if (loading) {
return (
<div className={cn(cardClass, "min-h-[92px]")} style={cardStyle}>
<div className="h-3.5 w-20 animate-pulse rounded bg-[#f0eeea]" />
<div className="mt-2.5 h-6 w-28 animate-pulse rounded bg-[#f0eeea]" />
{subtitle && <div className="mt-2 h-3.5 w-24 animate-pulse rounded bg-[#f5f3f0]" />}
<div className="h-3.5 w-20 animate-pulse rounded bg-muted" />
<div className="mt-2.5 h-6 w-28 animate-pulse rounded bg-muted" />
{subtitle && <div className="mt-2 h-3.5 w-24 animate-pulse rounded bg-muted/60" />}
</div>
);
}
@@ -219,12 +219,12 @@ export const DashboardStatCardSkeleton: React.FC<DashboardStatCardSkeletonProps>
className,
}) => (
<div
className={cn("min-h-[92px] rounded-[14px] border bg-white px-3.5 py-3", className)}
className={cn("min-h-[92px] rounded-[14px] border bg-card px-3.5 py-3", className)}
style={{ borderColor: STUDIO_COLORS.border }}
>
<div className="h-3.5 w-20 animate-pulse rounded bg-[#f0eeea]" />
<div className="mt-2.5 h-6 w-28 animate-pulse rounded bg-[#f0eeea]" />
{hasSubtitle && <div className="mt-2 h-3.5 w-24 animate-pulse rounded bg-[#f5f3f0]" />}
<div className="h-3.5 w-20 animate-pulse rounded bg-muted" />
<div className="mt-2.5 h-6 w-28 animate-pulse rounded bg-muted" />
{hasSubtitle && <div className="mt-2 h-3.5 w-24 animate-pulse rounded bg-muted/60" />}
</div>
);
@@ -12,10 +12,10 @@ import React from "react";
import { cn } from "@/lib/utils";
export const PILL_TRIGGER_CLASS =
"h-8 w-auto gap-1.5 rounded-full border-[#e7e5e1] bg-white px-3.5 text-xs font-semibold text-[#2b2925] shadow-none hover:bg-[#faf9f7] focus:ring-1 focus:ring-[#e06a4e]/30 focus:ring-offset-0";
"h-8 w-auto gap-1.5 rounded-full border-border bg-card px-3.5 text-xs font-semibold text-foreground shadow-none hover:bg-surface-subtle focus:ring-1 focus:ring-coral/30 focus:ring-offset-0";
export const PILL_BUTTON_CLASS =
"h-8 rounded-full border-[#e7e5e1] bg-white px-3.5 text-xs font-semibold text-[#2b2925] shadow-none hover:bg-[#faf9f7]";
"h-8 rounded-full border-border bg-card px-3.5 text-xs font-semibold text-foreground shadow-none hover:bg-surface-subtle";
export interface MetricPillProps {
active: boolean;
@@ -39,8 +39,8 @@ export const MetricPill: React.FC<MetricPillProps> = ({
className={cn(
"flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors",
active
? "bg-[#f0eeea] text-[#2b2925] hover:bg-[#eae7e2]"
: "text-[#7c7870] hover:bg-[#f5f3f0] hover:text-[#2b2925]"
? "bg-muted text-foreground hover:bg-[#eae7e2]"
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground"
)}
>
<span
@@ -63,7 +63,7 @@ export interface LegendChipProps {
/** Static series chip — for charts whose series aren't toggleable */
export const LegendChip: React.FC<LegendChipProps> = ({ color, dashed = false, children }) => (
<span className="flex h-7 shrink-0 items-center gap-1.5 px-2 text-xs font-medium text-[#7c7870]">
<span className="flex h-7 shrink-0 items-center gap-1.5 px-2 text-xs font-medium text-muted-foreground">
<span
className="inline-block h-2 w-2 rounded-full"
style={
@@ -87,7 +87,7 @@ export interface QuietStatProps {
/**
* Deemphasized stat cell. Place inside:
* <div className="grid grid-cols-2 gap-px overflow-hidden rounded-[14px]
* border border-[#e7e5e1] bg-[#f0eeea] sm:grid-cols-3 lg:grid-cols-6">
* border border-border bg-muted sm:grid-cols-3 lg:grid-cols-6">
*/
export const QuietStat: React.FC<QuietStatProps> = ({ label, value, sub, onClick, loading }) => {
const Tag = onClick ? "button" : "div";
@@ -95,20 +95,20 @@ export const QuietStat: React.FC<QuietStatProps> = ({ label, value, sub, onClick
<Tag
{...(onClick ? { type: "button" as const, onClick } : {})}
className={cn(
"bg-white px-3.5 py-2.5 text-left",
"bg-card px-3.5 py-2.5 text-left",
onClick &&
"transition-colors hover:bg-[#faf9f7] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
"transition-colors hover:bg-surface-subtle focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
)}
>
<div className="text-[10.5px] font-semibold text-[#a09b92]">{label}</div>
<div className="text-[10.5px] font-semibold text-muted-foreground/75">{label}</div>
{loading ? (
<div className="mt-1.5 h-4 w-16 animate-pulse rounded bg-[#f0eeea]" />
<div className="mt-1.5 h-4 w-16 animate-pulse rounded bg-muted" />
) : (
<>
<div className="text-[15px] font-bold tabular-nums tracking-tight text-[#2b2925]">
<div className="text-[15px] font-bold tabular-nums tracking-tight text-foreground">
{value}
</div>
{sub && <div className="text-[10.5px] text-[#7c7870]">{sub}</div>}
{sub && <div className="text-[10.5px] text-muted-foreground">{sub}</div>}
</>
)}
</Tag>
@@ -116,4 +116,4 @@ export const QuietStat: React.FC<QuietStatProps> = ({ label, value, sub, onClick
};
export const QUIET_STRIP_CLASS =
"grid gap-px overflow-hidden rounded-[14px] border border-[#e7e5e1] bg-[#f0eeea]";
"grid gap-px overflow-hidden rounded-[14px] border border-border bg-muted";
@@ -232,7 +232,7 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
className={cn(
"sticky left-0 z-10 border-r px-3 py-2",
groupStyle.rowClass,
isImportant ? "font-semibold text-emerald-800" : "font-medium text-slate-700"
isImportant ? "font-semibold text-emerald-800" : "font-medium text-foreground/90"
)}
>
<div className="flex items-center gap-2">
@@ -243,7 +243,7 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
<span
className={cn(
"text-sm leading-tight pr-2",
isImportant ? "font-semibold text-emerald-900" : "font-medium text-slate-800"
isImportant ? "font-semibold text-emerald-900" : "font-medium text-foreground"
)}
>
{row.label}
@@ -275,7 +275,7 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
<TableCell
key={`${row.key}-${bucket.key}`}
className={cn(
"text-center whitespace-nowrap px-1 py-1 text-slate-700",
"text-center whitespace-nowrap px-1 py-1 text-foreground/90",
groupStyle.rowClass,
isImportant && "font-semibold text-emerald-700"
)}
@@ -294,7 +294,7 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
<TableCell
key={`${row.key}-${bucket.key}`}
className={cn(
"text-center whitespace-nowrap px-1 py-1 text-slate-700",
"text-center whitespace-nowrap px-1 py-1 text-foreground/90",
groupStyle.rowClass,
isImportant && "font-semibold text-emerald-700"
)}
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
@@ -62,17 +63,18 @@ export function BestSellers() {
return (
<>
<Tabs defaultValue="products">
<CardHeader>
<div className="flex flex-row items-center justify-between">
<CardTitle className="text-lg font-medium">Best Sellers</CardTitle>
<SectionHeader
title="Best Sellers"
size="large"
actions={
<TabsList>
<TabsTrigger value="products">Products</TabsTrigger>
<TabsTrigger value="brands">Brands</TabsTrigger>
<TabsTrigger value="categories">Categories</TabsTrigger>
</TabsList>
</div>
</CardHeader>
<CardContent>
}
/>
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load best sellers</p>
) : isLoading ? (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts"
import { useState } from "react"
import config from "@/config"
@@ -87,9 +88,10 @@ export function ForecastMetrics() {
return (
<>
<CardHeader className="flex flex-row items-center justify-between pr-5">
<CardTitle className="text-xl font-medium">Forecast</CardTitle>
<div className="flex items-center gap-2">
<SectionHeader
title="Forecast"
size="large"
actions={
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
@@ -100,6 +102,8 @@ export function ForecastMetrics() {
<ForecastAccuracy />
</PopoverContent>
</Popover>
}
timeSelector={
<Tabs value={String(period)} onValueChange={(v) => setPeriod(v === 'year' ? 'year' : Number(v) as Period)}>
<TabsList>
<TabsTrigger value="30">30D</TabsTrigger>
@@ -107,9 +111,9 @@ export function ForecastMetrics() {
<TabsTrigger value="year">EOY</TabsTrigger>
</TabsList>
</Tabs>
</div>
</CardHeader>
<CardContent className="py-0 -mb-2">
}
/>
<CardContent className="pt-3 pb-0 -mb-2">
{error ? (
<div className="text-sm text-red-500">Error: {error.message}</div>
) : (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import config from "@/config"
import { formatCurrency } from "@/utils/formatCurrency"
import { AlertTriangle, Layers, DollarSign, Tag } from "lucide-react"
@@ -47,10 +48,8 @@ export function OverstockMetrics() {
return (
<>
<CardHeader>
<CardTitle className="text-xl font-medium">Overstock</CardTitle>
</CardHeader>
<CardContent>
<SectionHeader title="Overstock" size="large" />
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load overstock metrics</p>
) : (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts"
import config from "@/config"
import { formatCurrency } from "@/utils/formatCurrency"
@@ -84,10 +85,8 @@ export function PurchaseMetrics() {
return (
<>
<CardHeader>
<CardTitle className="text-xl font-medium">Purchases</CardTitle>
</CardHeader>
<CardContent>
<SectionHeader title="Purchases" size="large" />
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load purchase metrics</p>
) : (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import config from "@/config"
import { formatCurrency } from "@/utils/formatCurrency"
import { PackagePlus, DollarSign, Tag } from "lucide-react"
@@ -49,10 +50,8 @@ export function ReplenishmentMetrics() {
return (
<>
<CardHeader>
<CardTitle className="text-xl font-medium">Replenishment</CardTitle>
</CardHeader>
<CardContent>
<SectionHeader title="Replenishment" size="large" />
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load replenishment metrics</p>
) : (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts"
import { useState } from "react"
import config from "@/config"
@@ -72,11 +73,14 @@ export function SalesMetrics() {
return (
<>
<CardHeader className="flex flex-row items-center justify-between pr-5">
<CardTitle className="text-xl font-medium">Sales</CardTitle>
<SectionHeader
title="Sales"
size="large"
timeSelector={
<HorizonTabs value={period} onChange={(d) => setPeriod(d as Period)} options={[7, 30, 90]} />
</CardHeader>
<CardContent className="py-0 -mb-2">
}
/>
<CardContent className="pt-3 pb-0 -mb-2">
{isError ? (
<p className="text-sm text-destructive">Failed to load sales metrics</p>
) : (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts"
import config from "@/config"
import { formatCurrency } from "@/utils/formatCurrency"
@@ -116,10 +117,8 @@ export function StockMetrics() {
return (
<>
<CardHeader>
<CardTitle className="text-xl font-medium">Stock</CardTitle>
</CardHeader>
<CardContent>
<SectionHeader title="Stock" size="large" />
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load stock metrics</p>
) : (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import config from "@/config"
@@ -38,10 +39,8 @@ export function TopOverstockedProducts() {
return (
<>
<CardHeader>
<CardTitle className="text-xl font-medium">Top Overstocked Products</CardTitle>
</CardHeader>
<CardContent>
<SectionHeader title="Top Overstocked Products" size="large" />
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load overstocked products</p>
) : isLoading ? (
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { CardContent } from "@/components/ui/card"
import { SectionHeader } from "@/components/shared"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import config from "@/config"
@@ -38,10 +39,8 @@ export function TopReplenishProducts() {
return (
<>
<CardHeader>
<CardTitle className="text-xl font-medium">Top Products To Replenish</CardTitle>
</CardHeader>
<CardContent>
<SectionHeader title="Top Products To Replenish" size="large" />
<CardContent className="pt-4">
{isError ? (
<p className="text-sm text-destructive">Failed to load replenish products</p>
) : isLoading ? (
@@ -190,7 +190,7 @@ export function EditableMultiSelect({
{/* Selected items pinned to top */}
{value.length > 0 && (
<CommandGroup>
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-green-600 dark:text-green-400 bg-green-50/80 dark:bg-green-950/40 border-b border-green-100 dark:border-green-900">
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-green-600 bg-green-50/80 border-b border-green-100">
<Check className="h-3 w-3" />
<span>Selected ({value.length})</span>
</div>
@@ -204,7 +204,7 @@ export function EditableMultiSelect({
key={`sel-${selectedVal}`}
value={`sel-${opt?.label ?? selectedVal}`}
onSelect={() => handleSelect(selectedVal)}
className="bg-green-50/50 dark:bg-green-950/30"
className="bg-green-50/50"
>
<Check className="mr-2 h-4 w-4 opacity-100 text-green-600" />
{hex && (
@@ -226,7 +226,7 @@ export function EditableMultiSelect({
{/* AI Suggestions section */}
{(suggestions && suggestions.length > 0) || isLoadingSuggestions ? (
<CommandGroup>
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-purple-600 dark:text-purple-400 bg-purple-50/80 dark:bg-purple-950/40 border-b border-purple-100 dark:border-purple-900">
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-purple-600 bg-purple-50/80 border-b border-purple-100">
<Sparkles className="h-3 w-3" />
<span>Suggested</span>
{isLoadingSuggestions && <Loader2 className="h-3 w-3 animate-spin" />}
@@ -242,7 +242,7 @@ export function EditableMultiSelect({
key={`suggestion-${suggestion.id}`}
value={`suggestion-${suggestion.name}`}
onSelect={() => handleSelect(String(suggestion.id))}
className="bg-purple-50/30 dark:bg-purple-950/20"
className="bg-purple-50/30"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<Check className="h-4 w-4 flex-shrink-0 opacity-0" />
@@ -259,7 +259,7 @@ export function EditableMultiSelect({
{showColors ? suggestion.name : (suggestion.fullPath || suggestion.name)}
</span>
</div>
<span className="text-xs text-purple-500 dark:text-purple-400 ml-2 flex-shrink-0">
<span className="text-xs text-purple-500 ml-2 flex-shrink-0">
{similarityPercent}%
</span>
</CommandItem>
@@ -703,7 +703,7 @@ export function ProductEditForm({
<button
type="button"
onClick={() => setDescDialogOpen(true)}
className="flex items-center gap-1 px-1.5 -mr-1.5 py-0.5 rounded bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/50 dark:hover:bg-purple-800/50 text-purple-600 dark:text-purple-400 text-xs transition-colors shrink-0"
className="flex items-center gap-1 px-1.5 -mr-1.5 py-0.5 rounded bg-purple-100 hover:bg-purple-200 text-purple-600 text-xs transition-colors shrink-0"
title="View AI suggestion"
>
<Sparkles className="h-3.5 w-3.5" />
@@ -140,6 +140,7 @@ export function ProductSearch({
<div className="flex gap-3">
<Input
placeholder="Search products…"
className="rounded-full px-3.5"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
@@ -185,11 +186,11 @@ export function ProductSearch({
<Table>
<TableHeader>
<TableRow>
<TableHead className="sticky top-0 bg-background">Name</TableHead>
<TableHead className="sticky top-0 bg-background">Item Number</TableHead>
<TableHead className="sticky top-0 bg-background">Brand</TableHead>
<TableHead className="sticky top-0 bg-background">Line</TableHead>
<TableHead className="sticky top-0 bg-background text-right">
<TableHead className="sticky top-0 bg-card">Name</TableHead>
<TableHead className="sticky top-0 bg-card">Item Number</TableHead>
<TableHead className="sticky top-0 bg-card">Brand</TableHead>
<TableHead className="sticky top-0 bg-card">Line</TableHead>
<TableHead className="sticky top-0 bg-card text-right">
Price
</TableHead>
</TableRow>
@@ -70,7 +70,7 @@ export const GenericDropzone = ({
e.stopPropagation();
onShowUnassigned();
}}
className="mt-2 text-amber-600 dark:text-amber-400"
className="mt-2 text-amber-600"
>
Show {unassignedImages.length} unassigned {unassignedImages.length === 1 ? 'image' : 'images'}
</Button>
@@ -43,8 +43,8 @@ export const CopyButton = ({ text }: CopyButtonProps) => {
className={`ml-1 inline-flex items-center justify-center rounded-full p-1 transition-colors ${
canCopy
? isCopied
? "bg-green-100 text-green-600 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400"
: "text-muted-foreground hover:bg-gray-100 dark:hover:bg-gray-800"
? "bg-green-100 text-green-600 hover:bg-green-200"
: "text-muted-foreground hover:bg-gray-100"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!canCopy}
@@ -201,7 +201,7 @@ export const SortableImage = ({
>
<X className="h-4 w-4" />
</Button>
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black">
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white">
<img
src={getFullImageUrl(image.url || image.imageUrl || '')}
alt={`${displayName} - Image ${imgIndex + 1}`}
@@ -24,11 +24,11 @@ export const UnassignedImagesSection = ({
return (
<div className="mb-4 px-4">
<div className="bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-md p-3">
<div className="bg-amber-50 border border-amber-200 rounded-md p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-amber-500" />
<h3 className="text-sm font-medium text-amber-700 dark:text-amber-400">
<h3 className="text-sm font-medium text-amber-700">
Unassigned Images ({unassignedImages.length})
</h3>
</div>
@@ -43,7 +43,7 @@ export const UnassignedImageItem = ({
<p className="text-white text-xs mb-1 truncate">{image.file.name}</p>
<div className="flex gap-2">
<Select onValueChange={(value) => onAssign(index, parseInt(value))}>
<SelectTrigger className="h-7 text-xs bg-white dark:bg-gray-800 border-0 text-black dark:text-white">
<SelectTrigger className="h-7 text-xs bg-white border-0 text-black">
<SelectValue placeholder="Assign to..." />
</SelectTrigger>
<SelectContent>
@@ -57,7 +57,7 @@ export const UnassignedImageItem = ({
<Button
variant="ghost"
size="icon"
className="h-7 w-7 bg-white dark:bg-gray-800 text-black dark:text-white"
className="h-7 w-7 bg-white text-black"
onClick={(e) => {
e.stopPropagation();
onRemove(index);
@@ -94,7 +94,7 @@ export const UnassignedImageItem = ({
>
<X className="h-4 w-4" />
</Button>
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black">
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white">
<img
src={image.previewUrl}
alt={`Unassigned image: ${image.file.name}`}
@@ -1510,7 +1510,7 @@ const MatchColumnsStepComponent = <T extends string>({
type="checkbox"
checked={!!globalSelections.costIsTotalCost}
onChange={(e) => setGlobalSelections(prev => ({ ...prev, costIsTotalCost: e.target.checked }))}
className="h-3.5 w-3.5 rounded border-gray-300"
className="h-3.5 w-3.5 rounded border-input"
/>
Divide by min qty
</label>
@@ -1561,7 +1561,7 @@ const MatchColumnsStepComponent = <T extends string>({
return (
<React.Fragment key={`unmapped-values-${column.index}`}>
<TableRow className="bg-amber-50 dark:bg-amber-950/20">
<TableRow className="bg-amber-50">
<TableCell className="font-medium">{column.header}</TableCell>
<TableCell className="text-center">
{renderSamplePreview(column.index)}
@@ -1944,7 +1944,7 @@ const MatchColumnsStepComponent = <T extends string>({
) : (
<AlertCircle className="h-4 w-4 text-amber-500 flex-shrink-0" />
)}
<span className={`text-sm ${isAccountedFor ? "text-green-700 dark:text-green-400" : "text-amber-700 dark:text-amber-400"}`}>
<span className={`text-sm ${isAccountedFor ? "text-green-700" : "text-amber-700"}`}>
{getFieldLabel(field.key)}
</span>
<span className="text-xs text-muted-foreground ml-auto">
@@ -77,8 +77,8 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
</Select>
</div>
{isChecked && (
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-green-700 bg-green-300 dark:bg-green-900/20">
<Check className="h-4 w-4 text-green-700 dark:text-green-500" />
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-green-700 bg-green-300">
<Check className="h-4 w-4 text-green-700" />
</div>
)}
</CardHeader>
@@ -66,7 +66,7 @@ export function AiSuggestionBadge({
className={cn(
'flex items-center justify-between gap-1.5 px-2 py-1 rounded-md text-xs',
'bg-purple-50 border border-purple-200',
'dark:bg-purple-950/30 dark:border-purple-800',
'',
className
)}
>
@@ -77,7 +77,7 @@ export function AiSuggestionBadge({
type="text"
value={editedValue}
onChange={(e) => setEditedValue(e.target.value)}
className="flex-1 min-w-0 bg-transparent text-purple-700 dark:text-purple-300 text-xs outline-none border-b border-transparent focus:border-purple-300 dark:focus:border-purple-600 transition-colors"
className="flex-1 min-w-0 bg-transparent text-purple-700 text-xs outline-none border-b border-transparent focus:border-purple-300 transition-colors"
/>
</div>
<div className="flex items-center gap-[0px] flex-shrink-0">
@@ -107,7 +107,7 @@ export function AiSuggestionBadge({
<Button
size="sm"
variant="ghost"
className="h-4 w-4 p-0 [&_svg]:size-3.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
className="h-4 w-4 p-0 [&_svg]:size-3.5 text-muted-foreground/70 hover:text-foreground hover:bg-muted"
onClick={(e) => {
e.stopPropagation();
onDismiss();
@@ -201,14 +201,14 @@ export function AiSuggestionBadge({
className={cn(
'flex items-center gap-1.5 px-2 py-1 rounded-md text-xs',
'bg-purple-50 border border-purple-200 hover:bg-purple-100',
'dark:bg-purple-950/30 dark:border-purple-800 dark:hover:bg-purple-900/40',
'',
'transition-colors cursor-pointer',
className
)}
title="Click to see AI suggestion"
>
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
<span className="text-purple-600 dark:text-purple-400 font-medium">
<span className="text-purple-600 font-medium">
{issues.length} {issues.length === 1 ? 'issue' : 'issues'}
</span>
<ChevronDown className="h-3 w-3 text-purple-400" />
@@ -222,7 +222,7 @@ export function AiSuggestionBadge({
className={cn(
'flex flex-col gap-2 p-3 rounded-md',
'bg-purple-50 border border-purple-200',
'dark:bg-purple-950/30 dark:border-purple-800',
'',
className
)}
>
@@ -230,11 +230,11 @@ export function AiSuggestionBadge({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="h-3.5 w-3.5 text-purple-500 flex-shrink-0" />
<span className="text-xs font-medium text-purple-600 dark:text-purple-400">
<span className="text-xs font-medium text-purple-600">
AI Suggestion
</span>
{issues.length > 0 && (
<span className="text-xs text-purple-500 dark:text-purple-400">
<span className="text-xs text-purple-500">
({issues.length} {issues.length === 1 ? 'issue' : 'issues'})
</span>
)}
@@ -261,7 +261,7 @@ export function AiSuggestionBadge({
{issues.map((issue, index) => (
<div
key={index}
className="flex items-start gap-1.5 text-purple-600 dark:text-purple-400"
className="flex items-start gap-1.5 text-purple-600"
>
<AlertCircle className="h-3 w-3 mt-0.5 flex-shrink-0 text-purple-400" />
<span>{issue}</span>
@@ -272,10 +272,10 @@ export function AiSuggestionBadge({
{/* Suggested description */}
<div className="mt-1">
<div className="text-xs text-purple-500 dark:text-purple-400 mb-1 font-medium">
<div className="text-xs text-purple-500 mb-1 font-medium">
Suggested:
</div>
<div className="text-sm text-purple-700 dark:text-purple-300 leading-relaxed bg-white/50 dark:bg-black/20 rounded p-2 border border-purple-100 dark:border-purple-800">
<div className="text-sm text-purple-700 leading-relaxed bg-white/50 rounded p-2 border border-purple-100">
{suggestion}
</div>
</div>
@@ -285,7 +285,7 @@ export function AiSuggestionBadge({
<Button
size="sm"
variant="outline"
className="h-7 px-3 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 dark:bg-green-950/30 dark:border-green-700 dark:text-green-400"
className="h-7 px-3 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400"
onClick={(e) => {
e.stopPropagation();
onAccept(suggestion);
@@ -297,7 +297,7 @@ export function AiSuggestionBadge({
<Button
size="sm"
variant="ghost"
className="h-7 px-3 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
className="h-7 px-3 text-xs text-muted-foreground hover:text-foreground"
onClick={(e) => {
e.stopPropagation();
onDismiss();
@@ -319,12 +319,12 @@ export function AiValidationLoading({ className }: { className?: string }) {
className={cn(
'flex items-center gap-2 px-2 py-1 rounded-md text-xs',
'bg-purple-50 border border-purple-200',
'dark:bg-purple-950/30 dark:border-purple-800',
'',
className
)}
>
<div className="h-3 w-3 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
<span className="text-purple-600 dark:text-purple-400">
<span className="text-purple-600">
Validating with AI...
</span>
</div>
@@ -114,21 +114,21 @@ export const CopyDownBanner = memo(() => {
}}
>
<div ref={bannerRef} className="pointer-events-auto">
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-full shadow-lg pl-3 pr-1 py-1 flex items-center gap-2 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="bg-blue-50 border border-blue-200 rounded-full shadow-lg pl-3 pr-1 py-1 flex items-center gap-2 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
<span className="text-xs font-medium text-blue-700 dark:text-blue-300 whitespace-nowrap">
<span className="text-xs font-medium text-blue-700 whitespace-nowrap">
Click row to copy to
</span>
{rowsBelow > 0 && (
<>
<div className="h-4 w-px bg-blue-200 dark:bg-blue-800" />
<div className="h-4 w-px bg-blue-200" />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleApplyToAll}
className="h-6 px-2 text-xs font-medium text-blue-700 hover:text-blue-900 hover:bg-blue-100 dark:text-blue-300 dark:hover:bg-blue-900 dark:hover:text-blue-100"
className="h-6 px-2 text-xs font-medium text-blue-700 hover:text-blue-900 hover:bg-blue-100"
>
<ArrowDownToLine className="h-3 w-3 mr-1" />
All
@@ -149,7 +149,7 @@ export const CopyDownBanner = memo(() => {
variant="ghost"
size="sm"
onClick={handleCancel}
className="h-6 w-6 p-0 text-blue-600 hover:text-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900"
className="h-6 w-6 p-0 text-blue-600 hover:text-blue-800 hover:bg-blue-100"
>
<X className="h-3.5 w-3.5" />
</Button>
@@ -62,7 +62,7 @@ export function SuggestionBadges({
<div className={cn('flex flex-wrap items-center gap-1.5', className)}>
{/* Label */}
<div className={cn(
'flex items-center gap-1 text-purple-600 dark:text-purple-400',
'flex items-center gap-1 text-purple-600',
compact ? 'text-[10px]' : 'text-xs'
)}>
<Sparkles className={compact ? 'h-2.5 w-2.5' : 'h-3 w-3'} />
@@ -71,7 +71,7 @@ export function SuggestionBadges({
{/* Loading state */}
{isLoading && (
<div className="flex items-center gap-1 text-gray-400">
<div className="flex items-center gap-1 text-muted-foreground/70">
<Loader2 className={cn('animate-spin', compact ? 'h-2.5 w-2.5' : 'h-3 w-3')} />
{!compact && <span className="text-xs">Loading...</span>}
</div>
@@ -92,8 +92,8 @@ export function SuggestionBadges({
'inline-flex items-center gap-1 rounded-full border transition-colors',
compact ? 'px-1.5 py-0.5 text-[10px]' : 'px-2 py-0.5 text-xs',
selected
? 'border-green-300 bg-green-50 text-green-700 dark:border-green-700 dark:bg-green-950 dark:text-green-400'
: 'border-purple-200 bg-purple-50 text-purple-700 hover:bg-purple-100 dark:border-purple-800 dark:bg-purple-950/50 dark:text-purple-300 dark:hover:bg-purple-900/50'
? 'border-green-300 bg-green-50 text-green-700'
: 'border-purple-200 bg-purple-50 text-purple-700 hover:bg-purple-100'
)}
title={suggestion.fullPath || suggestion.name}
>
@@ -141,8 +141,8 @@ export function InlineSuggestion({
className={cn(
'flex items-center justify-between px-2 py-1.5 cursor-pointer',
isSelected
? 'bg-green-50 dark:bg-green-950/30'
: 'bg-purple-50/50 hover:bg-purple-100/50 dark:bg-purple-950/20 dark:hover:bg-purple-900/30'
? 'bg-green-50'
: 'bg-purple-50/50 hover:bg-purple-100/50'
)}
onClick={onSelect}
>
@@ -154,7 +154,7 @@ export function InlineSuggestion({
</div>
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
{showScore && (
<span className="text-xs text-purple-500 dark:text-purple-400">
<span className="text-xs text-purple-500">
{similarityPercent}%
</span>
)}
@@ -178,12 +178,12 @@ interface SuggestionSectionHeaderProps {
export function SuggestionSectionHeader({ isLoading, count }: SuggestionSectionHeaderProps) {
return (
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-purple-600 dark:text-purple-400 bg-purple-50/80 dark:bg-purple-950/40 border-b border-purple-100 dark:border-purple-900">
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-purple-600 bg-purple-50/80 border-b border-purple-100">
<Sparkles className="h-3 w-3" />
<span>AI Suggested</span>
{isLoading && <Loader2 className="h-3 w-3 animate-spin" />}
{!isLoading && count !== undefined && (
<span className="text-purple-400 dark:text-purple-500">({count})</span>
<span className="text-purple-400">({count})</span>
)}
</div>
);
@@ -871,8 +871,8 @@ const CellWrapper = memo(({
const cellHighlightClass = cn(
'relative w-full group',
isCopyDownSource && 'ring-2 ring-blue-500 ring-inset rounded',
isInCopyDownRange && 'bg-blue-100 dark:bg-blue-900/30',
isCopyDownTarget && !isInCopyDownRange && 'hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer'
isInCopyDownRange && 'bg-blue-100',
isCopyDownTarget && !isInCopyDownRange && 'hover:bg-blue-50 cursor-pointer'
);
// When in copy-down mode for this field, make cell non-interactive so clicks go to parent
@@ -933,7 +933,7 @@ const CellWrapper = memo(({
className={cn(
'absolute top-1/2 -translate-y-1/2 z-10 p-1 rounded-full',
'bg-blue-50 hover:bg-blue-100 text-blue-500 hover:text-blue-600',
'dark:bg-blue-900/50 dark:hover:bg-blue-900 dark:text-blue-400',
'',
'shadow-sm',
// Position further left if there are errors to avoid overlap
hasErrors ? 'right-7' : 'right-0.5'
@@ -1420,7 +1420,7 @@ const VirtualRow = memo(({
// Use box-shadow for bottom border - renders more consistently with transforms than border-b
'shadow-[inset_0_-1px_0_0_hsl(var(--border))]',
hasErrors && 'bg-destructive/5',
isSelected && 'bg-blue-100 dark:bg-blue-900/40'
isSelected && 'bg-blue-100'
)}
style={{
height: ROW_HEIGHT,
@@ -1534,7 +1534,7 @@ const VirtualRow = memo(({
// Selection (blue) takes priority over errors (red)
shouldBeSticky && (
isSelected
? "lg:[background:#dbeafe] lg:dark:[background:hsl(221_83%_25%/0.4)]"
? "lg:[background:#dbeafe] lg:(221_83%_25%/0.4)]"
: hasErrors
? "lg:[background:linear-gradient(hsl(var(--destructive)/0.05),hsl(var(--destructive)/0.05)),hsl(var(--background))]"
: "lg:[background:hsl(var(--background))]"
@@ -271,7 +271,7 @@ const MultiSelectCellComponent = ({
{/* Selected items section - floats to top of dropdown */}
{selectedValues.length > 0 && (
<CommandGroup>
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-green-600 dark:text-green-400 bg-green-50/80 dark:bg-green-950/40 border-b border-green-100 dark:border-green-900">
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-green-600 bg-green-50/80 border-b border-green-100">
<Check className="h-3 w-3" />
<span>Selected ({selectedValues.length})</span>
</div>
@@ -286,7 +286,7 @@ const MultiSelectCellComponent = ({
key={`selected-${selectedVal}`}
value={`selected-${label}`}
onSelect={() => handleSelect(selectedVal)}
className="bg-green-50/50 dark:bg-green-950/30"
className="bg-green-50/50"
>
<Check className="mr-2 h-4 w-4 opacity-100 text-green-600" />
{field.key === 'colors' && hexColor && (
@@ -308,7 +308,7 @@ const MultiSelectCellComponent = ({
{/* AI Suggestions section - shown below selected items */}
{supportsSuggestions && (fieldSuggestions.length > 0 || suggestions.isLoading) && (
<CommandGroup>
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-purple-600 dark:text-purple-400 bg-purple-50/80 dark:bg-purple-950/40 border-b border-purple-100 dark:border-purple-900">
<div className="flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-purple-600 bg-purple-50/80 border-b border-purple-100">
<Sparkles className="h-3 w-3" />
<span>Suggested</span>
{suggestions.isLoading && <Loader2 className="h-3 w-3 animate-spin" />}
@@ -328,7 +328,7 @@ const MultiSelectCellComponent = ({
key={`suggestion-${suggestion.id}`}
value={`suggestion-${suggestion.name}`}
onSelect={() => handleSelect(String(suggestion.id))}
className="bg-purple-50/30 dark:bg-purple-950/20"
className="bg-purple-50/30"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<Check className="h-4 w-4 flex-shrink-0 opacity-0" />
@@ -347,7 +347,7 @@ const MultiSelectCellComponent = ({
{field.key === 'colors' ? suggestion.name : (suggestion.fullPath || suggestion.name)}
</span>
</div>
<span className="text-xs text-purple-500 dark:text-purple-400 ml-2 flex-shrink-0">
<span className="text-xs text-purple-500 ml-2 flex-shrink-0">
{similarityPercent}%
</span>
</CommandItem>
@@ -313,7 +313,7 @@ const MultilineInputComponent = ({
'overflow-hidden leading-tight h-[65px] top-0',
'border',
hasError ? 'border-destructive bg-destructive/5' : 'border-input',
hasAiSuggestion && !hasError && 'border-purple-300 bg-purple-50/50 dark:border-purple-700 dark:bg-purple-950/20',
hasAiSuggestion && !hasError && 'border-purple-300 bg-purple-50/50',
isValidating && 'opacity-50'
)}
>
@@ -335,7 +335,7 @@ const MultilineInputComponent = ({
setEditValue(initValue);
initialEditValueRef.current = initValue;
}}
className="absolute bottom-1 right-1 flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/50 dark:hover:bg-purple-800/50 text-purple-600 dark:text-purple-400 text-xs transition-colors"
className="absolute bottom-1 right-1 flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-100 hover:bg-purple-200 text-purple-600 text-xs transition-colors"
title="View AI suggestion"
>
<Sparkles className="h-3 w-3" />
@@ -344,7 +344,7 @@ const MultilineInputComponent = ({
)}
{/* AI validating indicator */}
{isAiValidating && (
<div className="absolute bottom-1 right-1 flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-100 dark:bg-purple-900/50">
<div className="absolute bottom-1 right-1 flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-100">
<Loader2 className="h-3 w-3 animate-spin text-purple-500" />
</div>
)}
@@ -67,7 +67,7 @@ const DiffDisplay = ({ original, corrected }: { original: unknown; corrected: un
return (
<span
key={index}
className="bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 rounded px-0.5"
className="bg-green-100 text-green-800 rounded px-0.5"
>
{part.value}
</span>
@@ -77,7 +77,7 @@ const DiffDisplay = ({ original, corrected }: { original: unknown; corrected: un
return (
<span
key={index}
className="bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 line-through rounded px-0.5"
className="bg-red-100 text-red-800 line-through rounded px-0.5"
>
{part.value}
</span>
@@ -153,22 +153,22 @@ const SummaryDisplay = ({
<div className="space-y-2">
{/* Summary */}
{summary && (
<div className="p-3 rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800">
<div className="p-3 rounded-lg bg-blue-50 border border-blue-200">
<div className="flex items-start gap-2">
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-blue-800 dark:text-blue-200">{summary}</div>
<Info className="h-4 w-4 text-blue-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-blue-800">{summary}</div>
</div>
</div>
)}
{/* Warnings */}
{warnings && warnings.length > 0 && (
<div className="p-3 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800">
<div className="p-3 rounded-lg bg-amber-50 border border-amber-200">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<AlertTriangle className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
{warnings.map((warning, index) => (
<div key={index} className="text-sm text-amber-800 dark:text-amber-200">
<div key={index} className="text-sm text-amber-800">
{warning}
</div>
))}
@@ -205,7 +205,7 @@ export function SanityCheckDialog({
{Object.entries(groups).map(([key, group]) => (
<div
key={key}
className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 border border-gray-200 hover:border-gray-300 transition-colors"
className="flex items-start gap-3 p-3 rounded-lg bg-surface-subtle border border-border hover:border-muted-foreground/30 transition-colors"
>
<AlertCircle className="h-4 w-4 text-amber-500 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
@@ -234,12 +234,12 @@ export function SanityCheckDialog({
</Button>
)}
{idx < group.productIndices.length - 1 && (
<span className="text-gray-400 mr-1">,</span>
<span className="text-muted-foreground/70 mr-1">,</span>
)}
</span>
))}
</div>
<p className="text-sm text-gray-600">{group.issue}</p>
<p className="text-sm text-muted-foreground">{group.issue}</p>
{group.suggestion && (
<p className="text-xs text-blue-600 mt-1">
{group.suggestion}
@@ -172,7 +172,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<VaulDrawer.Overlay className="fixed inset-0 bg-black/40 z-40" />
<VaulDrawer.Content className="fixed right-0 top-0 h-dvh w-[90%] max-w-[800px] bg-background p-0 shadow-lg flex flex-col z-50">
{/* Header */}
<div className="flex items-start justify-between p-4 border-b sticky top-0 bg-background z-10">
<div className="flex items-start justify-between p-4 border-b sticky top-0 bg-card z-10">
<div className="flex items-center gap-4 overflow-hidden">
{isLoadingProduct ? (
<Skeleton className="h-16 w-16 rounded-lg" />
@@ -198,15 +198,15 @@ export function ProductTable({
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<div className="border rounded-md relative">
<div className="border rounded-xl bg-card relative overflow-hidden">
{isLoading && (
<div className="absolute inset-0 bg-background/70 flex items-center justify-center z-20">
<div className="absolute inset-0 bg-card/70 flex items-center justify-center z-20">
<Skeleton className="h-8 w-32" />
</div>
)}
<div className="overflow-x-auto">
<Table className={cn(isLoading ? 'opacity-50' : '', "w-max min-w-full")}>
<TableHeader className="sticky top-0 bg-background z-10">
<TableHeader className="sticky top-0 bg-card z-10">
<TableRow>
<SortableContext
items={orderedVisibleColumns}
@@ -86,7 +86,7 @@ export default function FilterControls({
placeholder="Search orders..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="max-w-xs"
className="max-w-xs rounded-full px-3.5"
disabled={loading}
/>
<Select
@@ -106,11 +106,11 @@ export default function PipelineCard() {
</div>
</div>
{data.overdue.count > 0 ? (
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50/50 dark:border-red-900/50 dark:bg-red-950/20 p-2.5">
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50/50 p-2.5">
<AlertTriangle className="h-4 w-4 text-red-500" />
<div>
<p className="text-xs text-red-600 dark:text-red-400">Overdue</p>
<p className="text-sm font-bold text-red-600 dark:text-red-400">
<p className="text-xs text-red-600">Overdue</p>
<p className="text-sm font-bold text-red-600">
{data.overdue.count} POs ({formatCurrency(data.overdue.value)})
</p>
</div>
@@ -60,7 +60,7 @@ export default function PurchaseOrderAccordion({
// Clone the TableRow (children) and add the onClick handler and className
const enhancedRow = React.cloneElement(children as React.ReactElement, {
onClick: () => setIsOpen(!isOpen),
className: `${(children as React.ReactElement).props.className || ""} cursor-pointer ${isOpen ? 'bg-gray-100' : ''} ${rowClassName || ""}`.trim(),
className: `${(children as React.ReactElement).props.className || ""} cursor-pointer ${isOpen ? "bg-muted" : ""} ${rowClassName || ""}`.trim(),
"data-state": isOpen ? "open" : "closed"
});
@@ -116,7 +116,7 @@ export default function PurchaseOrderAccordion({
}
return (
<div className="max-h-[600px] overflow-y-auto bg-gray-50 rounded-md p-2">
<div className="max-h-[600px] overflow-y-auto bg-surface-subtle rounded-md p-2">
<Table className="w-full">
<TableHeader className="bg-white sticky top-0 z-10">
<TableRow>
@@ -151,7 +151,7 @@ export default function PurchaseOrderAccordion({
))
) : (
items.map((item) => (
<TableRow key={item.id} className="hover:bg-gray-100">
<TableRow key={item.id} className="hover:bg-muted">
<TableCell className="">
<a
href={`https://backend.acherryontop.com/product/${item.pid}`}
@@ -228,7 +228,7 @@ export default function PurchaseOrderAccordion({
{isOpen && (
<TableRow className="p-0 border-0">
<TableCell colSpan={12} className="p-0 border-0">
<div className="pt-2 pb-4 px-4 bg-gray-50 border-t border-b">
<div className="pt-2 pb-4 px-4 bg-surface-subtle border-t border-b">
<div className="mb-2 text-sm text-muted-foreground">
{purchaseOrder.total_items} product{purchaseOrder.total_items !== 1 ? "s" : ""} in this {purchaseOrder.record_type === "receiving_only" ? "receiving" : "purchase order"}
</div>
@@ -96,7 +96,7 @@ export default function PurchaseOrdersTable({
return (
<Badge
variant="outline"
className="flex items-center justify-center border-gray-500 text-gray-700 bg-gray-50 px-0 text-xs w-[85px]"
className="flex items-center justify-center border-muted-foreground/40 text-muted-foreground bg-surface-subtle px-0 text-xs w-[85px]"
>
{recordType || "Unknown"}
</Badge>
@@ -323,7 +323,7 @@ export default function PurchaseOrdersTable({
) : purchaseOrders.length > 0 ? (
purchaseOrders.map((po) => {
// Determine row styling based on record type
let rowClassName = "border-l-4 border-l-gray-300"; // Default
let rowClassName = "border-l-4 border-l-[#d8d4cd]"; // Default
if (po.record_type === "po_with_receiving") {
rowClassName = "border-l-4 border-l-green-500";
+12 -12
View File
@@ -307,11 +307,11 @@ function Filters({
function SuccessBadge({ success }: { success: boolean }) {
return success ? (
<Badge variant="outline" className="text-green-600 border-green-300 bg-green-50 dark:bg-green-950 dark:border-green-800 dark:text-green-400">
<Badge variant="outline" className="text-green-600 border-green-300 bg-green-50">
Success
</Badge>
) : (
<Badge variant="outline" className="text-red-600 border-red-300 bg-red-50 dark:bg-red-950 dark:border-red-800 dark:text-red-400">
<Badge variant="outline" className="text-red-600 border-red-300 bg-red-50">
Failed
</Badge>
);
@@ -342,7 +342,7 @@ function EditorTable({ entries, onView }: { entries: AuditEntry[]; onView: (id:
href={`https://backend.acherryontop.com/product/${e.pid}`}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-sm hover:underline text-blue-600 dark:text-blue-400"
className="font-mono text-sm hover:underline text-blue-600"
>
{e.pid}
</a>
@@ -434,7 +434,7 @@ function DetailView({ detail, logType }: { detail: AuditDetail; logType: LogType
href={`https://backend.acherryontop.com/product/${detail.pid}`}
target="_blank"
rel="noopener noreferrer"
className="font-mono hover:underline text-blue-600 dark:text-blue-400"
className="font-mono hover:underline text-blue-600"
>
{detail.pid}
</a>
@@ -478,7 +478,7 @@ function DetailView({ detail, logType }: { detail: AuditDetail; logType: LogType
{detail.error_message && (
<div>
<span className="text-muted-foreground">Error</span>
<p className="text-red-600 dark:text-red-400 mt-1 text-xs bg-red-50 dark:bg-red-950 p-2 rounded-md break-all">
<p className="text-red-600 mt-1 text-xs bg-red-50 p-2 rounded-md break-all">
{detail.error_message}
</p>
</div>
@@ -576,11 +576,11 @@ function JsonValue({ value, depth }: { value: unknown; depth: number }) {
}
if (typeof value === "boolean") {
return <span className={value ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>{String(value)}</span>;
return <span className={value ? "text-green-600" : "text-red-600"}>{String(value)}</span>;
}
if (typeof value === "number") {
return <span className="text-blue-600 dark:text-blue-400">{value}</span>;
return <span className="text-blue-600">{value}</span>;
}
if (typeof value === "string") {
@@ -605,7 +605,7 @@ function JsonString({ value }: { value: string }) {
if (value.length <= STRING_TRUNCATE_LIMIT || expanded) {
return (
<span className="text-amber-700 dark:text-amber-400 break-words whitespace-pre-wrap">
<span className="text-amber-700 break-words whitespace-pre-wrap">
{value}
{expanded && value.length > STRING_TRUNCATE_LIMIT && (
<button onClick={() => setExpanded(false)} className="ml-1 text-muted-foreground hover:text-foreground text-[10px] underline">
@@ -617,7 +617,7 @@ function JsonString({ value }: { value: string }) {
}
return (
<span className="text-amber-700 dark:text-amber-400 break-words whitespace-pre-wrap">
<span className="text-amber-700 break-words whitespace-pre-wrap">
{value.slice(0, STRING_TRUNCATE_LIMIT)}
<span className="text-muted-foreground">... </span>
<button onClick={() => setExpanded(true)} className="text-muted-foreground hover:text-foreground text-[10px] underline">
@@ -642,7 +642,7 @@ function JsonObject({ obj, depth }: { obj: Record<string, unknown>; depth: numbe
{"{ "}
{entries.map(([k, v], i) => (
<span key={k}>
<span className="text-purple-600 dark:text-purple-400">{k}</span>
<span className="text-purple-600">{k}</span>
<span className="text-muted-foreground">: </span>
<JsonValue value={v} depth={depth + 1} />
{i < entries.length - 1 && <span className="text-muted-foreground">, </span>}
@@ -676,7 +676,7 @@ function JsonField({ fieldKey, value, depth }: { fieldKey: string; value: unknow
className="inline-flex items-center gap-1 hover:text-foreground"
>
{open ? <ChevronDown className="h-3 w-3 text-muted-foreground" /> : <ChevronRightIcon className="h-3 w-3 text-muted-foreground" />}
<span className="text-purple-600 dark:text-purple-400 font-medium">{fieldKey}</span>
<span className="text-purple-600 font-medium">{fieldKey}</span>
</button>
{open && (
<div className="ml-4 border-l border-border pl-3 mt-1">
@@ -690,7 +690,7 @@ function JsonField({ fieldKey, value, depth }: { fieldKey: string; value: unknow
// Primitive or small array — render inline
return (
<div>
<span className="text-purple-600 dark:text-purple-400 font-medium">{fieldKey}</span>
<span className="text-purple-600 font-medium">{fieldKey}</span>
<span className="text-muted-foreground">: </span>
<JsonValue value={value} depth={depth + 1} />
</div>
@@ -401,12 +401,12 @@ export function DataManagement() {
if (data.completed_steps && Array.isArray(data.completed_steps)) {
return (
<div className="space-y-2 mt-2">
<div className="text-sm font-semibold text-gray-700">Completed Steps:</div>
<div className="space-y-1 bg-gray-50 p-2 rounded">
<div className="text-sm font-semibold text-foreground">Completed Steps:</div>
<div className="space-y-1 bg-surface-subtle p-2 rounded">
{data.completed_steps.map((step: any, idx: number) => (
<div key={idx} className="flex justify-between text-sm">
<span className="font-medium">{step.name}</span>
<div className="flex gap-4 text-gray-600">
<div className="flex gap-4 text-muted-foreground">
<span>{step.duration}s</span>
{step.rowsAffected !== undefined && (
<span>{formatNumber(step.rowsAffected)} rows</span>
@@ -434,15 +434,15 @@ export function DataManagement() {
if (data.details) {
return (
<div className="space-y-2 mt-2">
<div className="text-sm font-semibold text-gray-700">Import Details:</div>
<div className="space-y-2 bg-gray-50 p-2 rounded">
<div className="text-sm font-semibold text-foreground">Import Details:</div>
<div className="space-y-2 bg-surface-subtle p-2 rounded">
{Object.entries(data.details).map(([table, stats]: [string, any]) => (
<div key={table} className="border-b last:border-0 pb-2 last:pb-0">
<div className="font-medium text-sm capitalize mb-1">{table}:</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
{Object.entries(stats).map(([key, value]) => (
<div key={key} className="flex justify-between">
<span className="text-gray-600">{key}:</span>
<span className="text-muted-foreground">{key}:</span>
<span className="font-mono">{typeof value === 'number' ? formatNumber(value) : String(value)}</span>
</div>
))}
@@ -453,11 +453,11 @@ export function DataManagement() {
{/* Show step timings if present */}
{data.step_timings && (
<div className="mt-2">
<div className="text-sm font-semibold text-gray-700">Step Timings:</div>
<div className="space-y-1 bg-gray-50 p-2 rounded text-sm">
<div className="text-sm font-semibold text-foreground">Step Timings:</div>
<div className="space-y-1 bg-surface-subtle p-2 rounded text-sm">
{Object.entries(data.step_timings).map(([step, duration]) => (
<div key={step} className="flex justify-between">
<span className="text-gray-600">{step}:</span>
<span className="text-muted-foreground">{step}:</span>
<span className="font-mono">{String(duration)}s</span>
</div>
))}
@@ -480,11 +480,11 @@ export function DataManagement() {
);
return (
<div className="space-y-1 mt-2 bg-gray-50 p-2 rounded text-sm font-mono">
<div className="space-y-1 mt-2 bg-surface-subtle p-2 rounded text-sm font-mono">
{Object.entries(data).map(([key, value]) => (
<div key={key} className="flex">
<span
className="text-gray-600 font-bold shrink-0"
className="text-muted-foreground font-bold shrink-0"
style={{ width: `${maxKeyLength + 2}ch` }}
>
{key}:
@@ -920,7 +920,7 @@ export function DataManagement() {
{table.error ? (
<span className="text-red-600 text-sm">{table.error}</span>
) : (
<span className="text-sm text-gray-600">{formatNumber(table.count)}</span>
<span className="text-sm text-muted-foreground">{formatNumber(table.count)}</span>
)}
</div>
))}
@@ -1104,7 +1104,7 @@ export function DataManagement() {
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
>
<span className="font-medium">{table.table_name}</span>
<span className="text-sm text-gray-600">
<span className="text-sm text-muted-foreground">
{formatStatusTime(table.last_sync_timestamp)}
</span>
</div>
@@ -1146,7 +1146,7 @@ export function DataManagement() {
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
>
<span className="font-medium">{module.module_name}</span>
<span className="text-sm text-gray-600">
<span className="text-sm text-muted-foreground">
{formatStatusTime(module.last_calculation_timestamp)}
</span>
</div>
@@ -1197,7 +1197,7 @@ export function DataManagement() {
</span>
</div>
<div className="w-[170px]">
<span className="text-sm text-gray-600">
<span className="text-sm text-muted-foreground">
{formatDate(record.start_time)}
</span>
</div>
@@ -1230,7 +1230,7 @@ export function DataManagement() {
<AccordionContent className="px-4 pb-2">
<div className="space-y-2 pt-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">End Time:</span>
<span className="text-muted-foreground">End Time:</span>
<span>
{record.end_time
? formatDate(record.end_time)
@@ -1239,28 +1239,28 @@ export function DataManagement() {
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Added:</span>
<span className="text-muted-foreground">Added:</span>
<span className="text-green-600 font-medium">{formatNumber(record.records_added)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Updated:</span>
<span className="text-muted-foreground">Updated:</span>
<span className="text-blue-600 font-medium">{formatNumber(record.records_updated)}</span>
</div>
{record.records_deleted !== undefined && (
<div className="flex justify-between">
<span className="text-gray-600">Deleted:</span>
<span className="text-muted-foreground">Deleted:</span>
<span className="text-red-600 font-medium">{formatNumber(record.records_deleted)}</span>
</div>
)}
{record.records_skipped !== undefined && (
<div className="flex justify-between">
<span className="text-gray-600">Skipped:</span>
<span className="text-muted-foreground">Skipped:</span>
<span className="text-yellow-600 font-medium">{formatNumber(record.records_skipped)}</span>
</div>
)}
{record.total_processed !== undefined && (
<div className="flex justify-between col-span-2">
<span className="text-gray-600">Total Processed:</span>
<span className="text-muted-foreground">Total Processed:</span>
<span className="font-medium">{formatNumber(record.total_processed)}</span>
</div>
)}
@@ -1319,7 +1319,7 @@ export function DataManagement() {
</span>
</div>
<div className="w-[170px]">
<span className="text-sm text-gray-600">
<span className="text-sm text-muted-foreground">
{formatDate(record.start_time)}
</span>
</div>
@@ -1352,7 +1352,7 @@ export function DataManagement() {
<AccordionContent className="px-4 pb-2">
<div className="space-y-2 pt-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">End Time:</span>
<span className="text-muted-foreground">End Time:</span>
<span>
{record.end_time
? formatDate(record.end_time)
@@ -1360,15 +1360,15 @@ export function DataManagement() {
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Products:</span>
<span className="text-muted-foreground">Products:</span>
<span>{record.processed_products} of {record.total_products}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Orders:</span>
<span className="text-muted-foreground">Orders:</span>
<span>{record.processed_orders} of {record.total_orders}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Purchase Orders:</span>
<span className="text-muted-foreground">Purchase Orders:</span>
<span>{record.processed_purchase_orders} of {record.total_purchase_orders}</span>
</div>
{record.error_message && (
@@ -180,7 +180,7 @@ export function ProductSettings() {
<Input
type="search"
placeholder="Search products by ID or name..."
className="pl-8"
className="pl-8 rounded-full"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -543,7 +543,7 @@ export function PromptManagement() {
placeholder="Search prompts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="max-w-sm"
className="max-w-sm rounded-full px-3.5"
/>
</div>
@@ -568,7 +568,7 @@ export function PromptManagement() {
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="hover:bg-gray-100">
<TableRow key={row.id} className="hover:bg-muted">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="pl-3 whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -593,7 +593,7 @@ export function ReusableImageManagement() {
placeholder="Search images..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="max-w-sm"
className="max-w-sm rounded-full px-3.5"
/>
</div>
@@ -621,7 +621,7 @@ export function ReusableImageManagement() {
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="hover:bg-gray-100">
<TableRow key={row.id} className="hover:bg-muted">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="pl-6">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -276,7 +276,7 @@ export function TemplateManagement() {
placeholder="Search templates..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="max-w-sm"
className="max-w-sm rounded-full px-3.5"
/>
</div>
@@ -304,7 +304,7 @@ export function TemplateManagement() {
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="hover:bg-gray-100">
<TableRow key={row.id} className="hover:bg-muted">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="pl-6">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -58,7 +58,7 @@ export function UserList({ users, onEdit, onDelete }: UserListProps) {
{user.is_active ? (
<Badge variant="default" className="bg-green-100 text-green-800 hover:bg-green-100">Active</Badge>
) : (
<Badge variant="secondary" className="bg-slate-100">Inactive</Badge>
<Badge variant="secondary" className="bg-muted">Inactive</Badge>
)}
</TableCell>
<TableCell>
@@ -172,7 +172,7 @@ export function VendorSettings() {
<Input
type="search"
placeholder="Search vendors..."
className="pl-8"
className="pl-8 rounded-full"
value={searchInputValue}
onChange={(e) => setSearchInputValue(e.target.value)}
/>
+82
View File
@@ -0,0 +1,82 @@
/**
* Shared app-wide UI components (Sorbet Studio design system).
*
* Neutral import path for the design-system primitives that grew up in the
* dashboard and are now app-wide conventions. The implementation files still
* live in components/dashboard/shared/ (single physical source); import from
* HERE outside the dashboard so pages don't depend on a dashboard path.
*
* @example
* import { SectionHeader, StatCard, DashboardTable, TABLE_STYLES } from "@/components/shared";
*/
export {
DashboardSectionHeader,
DashboardSectionHeader as SectionHeader,
DashboardSectionHeaderSkeleton,
type DashboardSectionHeaderProps,
} from "@/components/dashboard/shared/DashboardSectionHeader";
export {
DashboardEmptyState,
DashboardErrorState,
DashboardLoadingState,
type DashboardEmptyStateProps,
type DashboardErrorStateProps,
} from "@/components/dashboard/shared/DashboardStates";
export {
DashboardBadge,
StatusBadge,
TrendBadge,
type BadgeVariant,
type StatusType,
} from "@/components/dashboard/shared/DashboardBadge";
export {
DashboardTable,
SimpleTable,
type DashboardTableProps,
type TableColumn,
type SimpleTableProps,
} from "@/components/dashboard/shared/DashboardTable";
export {
MetricPill,
LegendChip,
QuietStat,
PILL_TRIGGER_CLASS,
PILL_BUTTON_CLASS,
} from "@/components/dashboard/shared/StudioControls";
export {
DashboardStatCard,
DashboardStatCard as StatCard,
DashboardStatCardSkeleton,
TrendPill,
type DashboardStatCardProps,
} from "@/components/dashboard/shared/DashboardStatCard";
export {
ChartSkeleton,
TableSkeleton,
StatCardSkeleton,
} from "@/components/dashboard/shared/DashboardSkeleton";
export {
CARD_STYLES,
TYPOGRAPHY,
SPACING,
MOTION,
SCROLL_STYLES,
TREND_COLORS,
FINANCIAL_COLORS,
METRIC_COLORS,
METRIC_COLORS_HSL,
STATUS_COLORS,
STAT_ICON_STYLES,
TABLE_STYLES,
BADGE_STYLES,
getTrendColor,
getIconColorVariant,
} from "@/lib/dashboard/designTokens";
@@ -738,7 +738,7 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
</div>
<div className="relative">
<Table>
<TableHeader className="sticky top-0 bg-background z-10 border-b">
<TableHeader className="sticky top-0 bg-card z-10 border-b">
<TableRow>
<SortableTableHead field="title">Name</SortableTableHead>
{searchParams.company === 'all' && (
+1 -1
View File
@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
+1 -1
View File
@@ -10,7 +10,7 @@ const alertVariants = cva(
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
"border-destructive/50 text-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
+1 -1
View File
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
+4 -4
View File
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
@@ -14,7 +14,7 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
"border border-input bg-card shadow-sm hover:bg-surface-subtle hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
@@ -22,8 +22,8 @@ const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "h-9 w-9",
},
},
+2 -2
View File
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"rounded-[14px] border bg-card text-card-foreground shadow-[0_1px_2px_rgba(43,41,37,.04)]",
className
)}
{...props}
@@ -36,7 +36,7 @@ const CardTitle = React.forwardRef<
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
"text-base font-semibold leading-none tracking-tight",
className
)}
{...props}
+1 -1
View File
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
+1 -1
View File
@@ -43,7 +43,7 @@ const DrawerContent = React.forwardRef<
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 gap-4 bg-background",
"fixed z-50 gap-4 bg-card",
{
"inset-x-0 bottom-0 mt-24 rounded-t-[10px] border-t": side === "bottom",
"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-xl": side === "right",
+1 -1
View File
@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-full border border-input bg-card px-3.5 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
+1 -1
View File
@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
"fixed z-50 gap-4 bg-card p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
+2 -2
View File
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
"inline-flex h-9 items-center justify-center rounded-full bg-muted p-1 text-muted-foreground",
className
)}
{...props}
@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
"inline-flex items-center justify-center whitespace-nowrap rounded-full px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
+1 -1
View File
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 rounded-full text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
+46 -42
View File
@@ -4,64 +4,68 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Sorbet Studio palette (2026-07 app-wide restyle).
Semantic slots re-pointed at the dashboard's Studio hues so the whole
app shares one warm system: ground #f8f7f5, ink #2b2925, hairline
#e7e5e1, wells #f0eeea, coral #e06a4e as the working accent. */
--background: 40 17.6% 96.7%; /* #f8f7f5 - Studio ground */
--foreground: 40 7.5% 15.7%; /* #2b2925 - Studio ink */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--card-foreground: 40 7.5% 15.7%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--popover-foreground: 40 7.5% 15.7%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--primary: 40 7.5% 15.7%; /* ink - actions stay neutral; coral is accent-only */
--primary-foreground: 40 23.1% 97.5%; /* #faf9f7 */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--secondary: 40 16.7% 92.9%; /* #f0eeea - Studio well */
--secondary-foreground: 40 7.5% 15.7%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted: 40 16.7% 92.9%; /* #f0eeea */
--muted-foreground: 40 5.1% 46.3%; /* #7c7870 - Studio muted */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--accent: 40 16.7% 92.9%;
--accent-foreground: 40 7.5% 15.7%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--destructive: 8.8 47.9% 47.5%; /* #b3503f - Studio down */
--destructive-foreground: 40 23.1% 97.5%;
--info: 217.2 91.2% 59.8%;
--info-foreground: 222.2 47.4% 11.2%;
--info: 205.4 46.8% 46.5%; /* #3f7fae - slate blue (chart-secondary) */
--info-foreground: 40 7.5% 15.7%;
--success: 142.1 76.2% 36.3%;
--success-foreground: 222.2 47.4% 11.2%;
--success: 149.0 55.4% 30.8%; /* #237a4d - Studio up */
--success-foreground: 40 7.5% 15.7%;
--warning: 45.4 93.4% 47.5%;
--warning-foreground: 222.2 47.4% 11.2%;
--warning: 39.1 64.7% 40.0%; /* #a87a24 - Studio amber */
--warning-foreground: 40 7.5% 15.7%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--border: 40 11.1% 89.4%; /* #e7e5e1 - Studio hairline */
--input: 40 11.1% 89.4%;
--ring: 11.5 70.2% 59.2%; /* #e06a4e - coral focus */
--radius: 0.5rem;
/* Extra Studio surface: subtle wells inside white cards (#faf9f7) */
--surface-subtle: 40 23.1% 97.5%;
--sidebar-background: 0 0% 98%;
/* Studio working accent (#e06a4e) - use sparingly: focus rings, one
accent per section, active highlights. Not for buttons. */
--coral: 11.5 70.2% 59.2%;
--sidebar-foreground: 240 5.3% 26.1%;
--radius: 0.625rem;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-background: 36 17.2% 94.3%; /* #f3f1ee - a step deeper than ground */
--sidebar-foreground: 33.3 5.5% 32.4%; /* #57534e */
--sidebar-primary: 40 7.5% 15.7%;
--sidebar-primary-foreground: 40 23.1% 97.5%;
--sidebar-accent: 37.5 16.0% 90.2%; /* #eae7e2 - active/hover well */
--sidebar-accent-foreground: 40 7.5% 15.7%;
--sidebar-border: 40 11.1% 89.4%;
--sidebar-ring: 11.5 70.2% 59.2%;
/* Dashboard card with glass effect */
--card-glass: 0 0% 100%;
--card-glass-foreground: 222.2 84% 4.9%;
--card-glass-foreground: 40 7.5% 15.7%;
/* Semantic chart colors - Sorbet Studio palette (must match METRIC_COLORS) */
--chart-revenue: 11.5 70.2% 59.2%; /* #e06a4e - Studio coral */
@@ -178,13 +182,13 @@
.dashboard-scroll {
@apply overflow-y-auto;
scrollbar-width: thin;
scrollbar-color: rgb(229 231 235) transparent;
scrollbar-color: #e7e5e1 transparent;
}
.dark .dashboard-scroll {
scrollbar-color: rgb(55 65 81) transparent;
}
.dashboard-scroll:hover {
scrollbar-color: rgb(209 213 219) transparent;
scrollbar-color: #d8d4cd transparent;
}
.dark .dashboard-scroll:hover {
scrollbar-color: rgb(75 85 99) transparent;
@@ -213,11 +217,11 @@
/* Full dashboard controls intentionally stay denser than the rest of the app. */
.dashboard-page [role="tablist"] {
@apply h-8 rounded-md bg-muted/60 p-0.5;
@apply h-8 rounded-full bg-muted/60 p-0.5;
}
.dashboard-page [role="tab"] {
@apply h-7 rounded px-2.5 text-xs;
@apply h-7 rounded-full px-2.5 text-xs;
}
.dashboard-page [role="combobox"] {
+11 -11
View File
@@ -18,17 +18,17 @@
*/
export const CARD_STYLES = {
/** Base card appearance (Sorbet Studio: white panel, warm border, 14px radius) */
base: "bg-white border border-[#e7e5e1] shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
base: "bg-card border border-border shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
/** Elevated card for primary/featured sections (Sales, Financial) */
elevated: "bg-white border border-[#e7e5e1] shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
elevated: "bg-card border border-border shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
/** Subtle card for secondary/supporting content */
subtle: "bg-[#faf9f7] border border-[#efede9] rounded-[12px]",
subtle: "bg-surface-subtle border border-muted rounded-[12px]",
/** Accent card with subtle ring highlight */
accent: "bg-white border border-[#e06a4e]/25 shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
accent: "bg-card border border-coral/25 shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
/** Card with subtle hover effect */
interactive: "bg-white border border-[#e7e5e1] shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px] transition-colors duration-150 hover:border-[#d8d4cd] hover:bg-[#faf9f7]",
interactive: "bg-card border border-border shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px] transition-colors duration-150 hover:border-[#d8d4cd] hover:bg-surface-subtle",
/** Solid card without glass effect (use sparingly) */
solid: "bg-white border border-[#e7e5e1] shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
solid: "bg-card border border-border shadow-[0_1px_2px_rgba(43,41,37,.04)] rounded-[14px]",
/** Card header layout */
header: "flex flex-row items-center justify-between p-3.5 pb-0",
/** Compact header for stat cards */
@@ -372,11 +372,11 @@ export const STAT_ICON_STYLES = {
* Table styling tokens
*/
export const TABLE_STYLES = {
container: "rounded-xl border border-[#efede9] overflow-hidden",
header: "bg-[#faf9f7]",
headerCell: "text-[10.5px] font-semibold text-[#a09b92] uppercase tracking-wide",
row: "border-b border-[#f0eeea] last:border-0",
rowHover: "hover:bg-[#faf9f7] transition-colors",
container: "rounded-xl border border-muted overflow-hidden",
header: "bg-surface-subtle",
headerCell: "text-[10.5px] font-semibold text-muted-foreground/75 uppercase tracking-wide",
row: "border-b border-muted last:border-0",
rowHover: "hover:bg-surface-subtle transition-colors",
cell: "text-xs",
cellNumeric: "text-sm tabular-nums text-right",
} as const;
+5 -5
View File
@@ -807,7 +807,7 @@ export function BlackFridayDashboard() {
<DollarSign className="h-3.5 w-3.5" />
Revenue
</div>
<div className="text-3xl font-bold tracking-tight tabular-nums text-emerald-600 dark:text-emerald-400">
<div className="text-3xl font-bold tracking-tight tabular-nums text-emerald-600">
{formatCurrency(currentYearData.totals.revenue, 0)}
</div>
{yoy !== null && (
@@ -838,7 +838,7 @@ export function BlackFridayDashboard() {
<TrendingUp className="h-3.5 w-3.5" />
Profit
</div>
<div className="text-2xl font-bold tabular-nums text-emerald-600 dark:text-emerald-400">
<div className="text-2xl font-bold tabular-nums text-emerald-600">
{formatCurrency(currentYearData.totals.profit, 0)}
</div>
{lastYearData && (
@@ -913,7 +913,7 @@ export function BlackFridayDashboard() {
<div className="flex items-center justify-between mb-1.5">
<span className={cn(
"text-xs font-semibold",
isToday ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"
isToday ? "text-emerald-600" : "text-muted-foreground"
)}>
{day.label}
</span>
@@ -1306,7 +1306,7 @@ export function BlackFridayDashboard() {
>
<TableCell className={cn(
"text-xs h-9 pl-4",
isCurrentYear ? "font-bold text-emerald-600 dark:text-emerald-400" : "font-semibold"
isCurrentYear ? "font-bold text-emerald-600" : "font-semibold"
)}>
{year}
</TableCell>
@@ -1319,7 +1319,7 @@ export function BlackFridayDashboard() {
<TableCell className="text-xs h-9 text-right tabular-nums text-muted-foreground">
{formatCurrency(entry.totals.avgOrderValue, 0)}
</TableCell>
<TableCell className="text-xs h-9 text-right tabular-nums font-medium text-emerald-600 dark:text-emerald-400">
<TableCell className="text-xs h-9 text-right tabular-nums font-medium text-emerald-600">
{formatCurrency(entry.totals.profit, 0)}
</TableCell>
<TableCell className="text-xs h-9 text-right pr-4">
+2 -2
View File
@@ -529,7 +529,7 @@ function BrandsPanel() {
/>
{/* Table */}
<div className="rounded-md border">
<div className="rounded-xl border bg-card overflow-hidden">
<Table>
<TableHeader>
<TableRow>
@@ -728,7 +728,7 @@ function VendorsPanel() {
/>
{/* Table */}
<div className="rounded-md border">
<div className="rounded-xl border bg-card overflow-hidden">
<Table>
<TableHeader>
<TableRow>
+2 -2
View File
@@ -272,7 +272,7 @@ const TypeBadge = ({
}) => {
const typeLabel = TYPE_LABELS[type] || `Type ${type}`;
const colorClass =
TYPE_COLORS[type] || "bg-gray-100 text-gray-800 hover:bg-gray-200";
TYPE_COLORS[type] || "bg-muted text-foreground hover:bg-muted";
return (
<Badge
@@ -1284,7 +1284,7 @@ export function Categories() {
</div>
{/* Data Table */}
<div className="rounded-md border">
<div className="rounded-xl border bg-card overflow-hidden">
<Table>
<TableHeader>
<TableRow>
+1 -1
View File
@@ -671,7 +671,7 @@ export default function ProductEditor() {
</div>
)}
{queryStatus.unsupported.length > 0 && (
<div className="text-amber-600 dark:text-amber-400">
<div className="text-amber-600">
{queryStatus.unsupported.length} filter type{queryStatus.unsupported.length !== 1 ? "s" : ""} not supported ({queryStatus.unsupported.join(", ")}). Results may be broader than expected.
</div>
)}
+1 -1
View File
@@ -693,7 +693,7 @@ export function ProductLines() {
</div>
{/* Data Table */}
<div className="rounded-md border">
<div className="rounded-xl border bg-card overflow-hidden">
<Table>
<TableHeader>
<TableRow>
+1 -1
View File
@@ -499,7 +499,7 @@ export function Products() {
placeholder='Search products by name... (press "/")'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9"
className="pl-9 pr-8 h-9 rounded-full"
/>
{searchQuery && (
<button
+2
View File
@@ -51,6 +51,8 @@ export default {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
'surface-subtle': 'hsl(var(--surface-subtle))',
coral: 'hsl(var(--coral))',
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
File diff suppressed because one or more lines are too long