feat(admin): design system audit fixes

- Add CSS design tokens for consistency (--admin-danger-hover, --admin-accent-dot, shadow-md warm tone)
- Replace unicode icons with inline SVG (⋮, ✕, ▼, ▶)
- Consolidate duplicate PageHeader/AdminPageHeader components
- Standardize border-radius (rounded-xl → rounded-lg for ViewModeTabs)
- Remove hardcoded brand UUID in products page
- Replace hardcoded bg-red-600 with CSS variables in delete buttons
- Add AdminButton, AdminFilterTabs, AdminSearchInput, PageHeader design system components
- Fix GlassModal and AdminModal close button styling
- Remove duplicate 'New Lot' button on Route Trace dashboard
This commit is contained in:
2026-06-02 03:31:54 +00:00
parent 15e939ad7e
commit 7203cf1ead
65 changed files with 3478 additions and 3176 deletions
@@ -4,6 +4,7 @@ import { useState } from "react";
import { type Campaign, type CampaignType } from "@/actions/harvest-reach/campaigns";
import { type Template } from "@/actions/communications/templates";
import type { Segment, SegmentRuleV2 } from "@/actions/harvest-reach/segments";
import { AdminButton } from "@/components/admin/design-system";
type Props = {
brandId: string;
@@ -124,12 +125,9 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
? "Your campaign has been sent to all matching customers."
: `It will be sent on ${new Date(scheduledAt).toLocaleString()}.`}
</p>
<button
onClick={() => window.location.href = "/admin/communications"}
className="px-6 py-2.5 rounded-lg bg-stone-900 text-white text-sm font-medium hover:bg-stone-800"
>
<AdminButton onClick={() => window.location.href = "/admin/communications"}>
Back to Campaigns
</button>
</AdminButton>
</div>
);
}
@@ -145,13 +143,13 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
<div key={label} className="flex items-center">
<div className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium ${
step === n
? "bg-stone-900 text-white"
? "bg-[var(--admin-text-primary)] text-white"
: done
? "bg-stone-200 text-stone-700"
? "bg-[var(--admin-border)] text-[var(--admin-text-secondary)]"
: "bg-zinc-950 text-slate-400"
}`}>
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-xs border ${
step === n ? "border-white" : done ? "border-stone-400" : "border-zinc-600"
step === n ? "border-white" : done ? "border-[var(--admin-text-muted)]" : "border-zinc-600"
}`}>
{done ? (
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -162,7 +160,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
<span className="hidden sm:inline">{label}</span>
</div>
{i < STEPS.length - 1 && (
<div className={`h-0.5 w-6 ${done ? "bg-stone-300" : "bg-slate-200"} mx-1`} />
<div className={`h-0.5 w-6 ${done ? "bg-[var(--admin-border)]" : "bg-slate-200"} mx-1`} />
)}
</div>
);
@@ -189,23 +187,20 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
placeholder="Subject line"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-slate-900"
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-[var(--admin-accent)]"
/>
<textarea
placeholder="Email body text…"
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
rows={5}
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm resize-none outline-none focus:border-slate-900"
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm resize-none outline-none focus:border-[var(--admin-accent)]"
/>
</div>
<div className="flex justify-end">
<button
onClick={() => setStep(2)}
className="px-5 py-2.5 rounded-lg bg-stone-900 text-white text-sm font-medium hover:bg-stone-800"
>
<AdminButton onClick={() => setStep(2)}>
Continue
</button>
</AdminButton>
</div>
</div>
) : (
@@ -213,7 +208,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{/* Start from scratch */}
<button
onClick={() => setStep(2)}
className="rounded-xl border-2 border-dashed border-zinc-600 p-5 text-left hover:border-stone-400 hover:bg-zinc-950 transition-all group"
className="rounded-xl border-2 border-dashed border-zinc-600 p-5 text-left hover:border-[var(--admin-border)] hover:bg-zinc-950 transition-all group"
>
<span className="text-2xl block mb-2">+</span>
<p className="text-sm font-medium text-zinc-300 group-hover:text-zinc-100">Start from scratch</p>
@@ -227,8 +222,8 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
onClick={() => handleTemplateSelect(t)}
className={`rounded-xl border p-4 text-left transition-all ${
selectedTemplateId === t.id
? "border-stone-900 bg-zinc-950"
: "border-zinc-800 hover:border-slate-400 hover:bg-zinc-800"
? "border-[var(--admin-text-primary)] bg-zinc-950"
: "border-zinc-800 hover:border-[var(--admin-border)] hover:bg-zinc-800"
}`}
>
<div className="flex items-start justify-between gap-2">
@@ -263,7 +258,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. May Sweet Corn Promotion"
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-slate-900"
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-[var(--admin-accent)]"
/>
</div>
@@ -275,7 +270,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
onChange={(e) => {
setSelectedSegmentId(e.target.value);
}}
className="flex-1 border border-zinc-600 rounded-lg px-3 py-2.5 text-sm bg-zinc-900 outline-none focus:border-slate-900"
className="flex-1 border border-zinc-600 rounded-lg px-3 py-2.5 text-sm bg-zinc-900 outline-none focus:border-[var(--admin-accent)]"
>
<option value=""> Choose a saved segment </option>
{segments.map((s) => (
@@ -283,12 +278,9 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
))}
</select>
{selectedSegmentId && (
<button
onClick={() => setSelectedSegmentId("")}
className="px-3 text-xs text-slate-400 hover:text-red-500 border border-zinc-800 rounded-lg hover:border-red-200"
>
<AdminButton variant="secondary" size="sm" onClick={() => setSelectedSegmentId("")}>
Clear
</button>
</AdminButton>
)}
</div>
</div>
@@ -297,20 +289,13 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{error && <p className="text-sm text-red-400">{error}</p>}
<div className="flex justify-between pt-2">
<button
onClick={() => setStep(1)}
className="px-4 py-2.5 text-sm text-zinc-500 hover:text-zinc-300 flex items-center gap-1"
>
<AdminButton variant="ghost" onClick={() => setStep(1)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
Back
</button>
<button
onClick={() => setStep(3)}
disabled={!name}
className="px-5 py-2.5 rounded-lg bg-stone-900 text-white text-sm font-medium hover:bg-stone-800 disabled:opacity-40"
>
</AdminButton>
<AdminButton onClick={() => setStep(3)} disabled={!name}>
Continue to Preview
</button>
</AdminButton>
</div>
</div>
)}
@@ -326,7 +311,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{/* Mock email frame */}
<div className="rounded-xl border border-zinc-800 bg-slate-50 overflow-hidden">
<div className="px-4 py-3 bg-zinc-900 border-b border-zinc-800 flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-stone-300" />
<div className="w-8 h-8 rounded-full bg-[var(--admin-border)]" />
<div>
<p className="text-xs font-medium text-zinc-300">Your Brand</p>
<p className="text-xs text-slate-400">To: {"{{customer_name}}"}</p>
@@ -345,24 +330,18 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-slate-900"
className="w-full border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-[var(--admin-accent)]"
/>
</div>
<div className="flex justify-between pt-2">
<button
onClick={() => setStep(2)}
className="px-4 py-2.5 text-sm text-zinc-500 hover:text-zinc-300 flex items-center gap-1"
>
<AdminButton variant="ghost" onClick={() => setStep(2)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
Back
</button>
<button
onClick={() => setStep(4)}
className="px-5 py-2.5 rounded-lg bg-stone-900 text-white text-sm font-medium hover:bg-stone-800"
>
</AdminButton>
<AdminButton onClick={() => setStep(4)}>
Continue to Schedule
</button>
</AdminButton>
</div>
</div>
)}
@@ -385,12 +364,12 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
onClick={() => setCampaignType(ct.value)}
className={`rounded-xl border py-3 px-4 text-left transition-all ${
campaignType === ct.value
? "border-stone-900 bg-stone-900 text-white"
: "border-zinc-800 hover:border-slate-400 hover:bg-zinc-800"
? "border-[var(--admin-text-primary)] bg-[var(--admin-text-primary)] text-white"
: "border-zinc-800 hover:border-[var(--admin-border)] hover:bg-zinc-800"
}`}
>
<p className={`text-sm font-medium ${campaignType === ct.value ? "text-white" : "text-zinc-300"}`}>{ct.label}</p>
<p className={`text-xs mt-0.5 ${campaignType === ct.value ? "text-stone-300" : "text-slate-400"}`}>{ct.desc}</p>
<p className={`text-xs mt-0.5 ${campaignType === ct.value ? "text-[var(--admin-border)]" : "text-slate-400"}`}>{ct.desc}</p>
</button>
))}
</div>
@@ -405,7 +384,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
type="radio"
checked={sendNow}
onChange={() => setSendNow(true)}
className="accent-stone-900 w-4 h-4"
className="accent-[var(--admin-accent)] w-4 h-4"
/>
<span className="text-sm font-medium text-zinc-300">Send now</span>
</label>
@@ -414,7 +393,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
type="radio"
checked={!sendNow}
onChange={() => setSendNow(false)}
className="accent-stone-900 w-4 h-4"
className="accent-[var(--admin-accent)] w-4 h-4"
/>
<span className="text-sm font-medium text-zinc-300">Schedule for later</span>
</label>
@@ -428,7 +407,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
type="datetime-local"
value={scheduledAt}
onChange={(e) => setScheduledAt(e.target.value)}
className="border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-slate-900"
className="border border-zinc-600 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-[var(--admin-accent)]"
min={new Date().toISOString().slice(0, 16)}
/>
</div>
@@ -452,20 +431,17 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{error && <p className="text-sm text-red-400">{error}</p>}
<div className="flex justify-between pt-2">
<button
onClick={() => setStep(3)}
className="px-4 py-2.5 text-sm text-zinc-500 hover:text-zinc-300 flex items-center gap-1"
>
<AdminButton variant="ghost" onClick={() => setStep(3)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
Back
</button>
<button
</AdminButton>
<AdminButton
onClick={handleCreateOrSchedule}
disabled={saving || (!sendNow && !scheduledAt)}
className="px-5 py-2.5 rounded-lg bg-stone-900 text-white text-sm font-medium hover:bg-stone-800 transition-colors disabled:opacity-50"
isLoading={saving}
disabled={(!sendNow && !scheduledAt)}
>
{saving ? "Processing…" : sendNow ? "Send Campaign" : "Schedule Campaign"}
</button>
</AdminButton>
</div>
</div>
)}