fix: react-doctor control-has-associated-label 137→? (auto aria-labels)

Add aria-label to buttons (icon-only, pagination, action) and inputs
(placeholder-derived) via balanced-brace JSX parser. Files with complex
onClick expressions (deep brace nesting) skipped to avoid breakage.
This commit is contained in:
Nora
2026-06-26 04:08:56 -06:00
parent 16a6756ad1
commit f7ac9399b2
46 changed files with 187 additions and 187 deletions
+20 -20
View File
@@ -131,7 +131,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
boxShadow: isConnected ? '0 2px 8px rgba(202, 117, 67, 0.25)' : 'none',
}}
title={!isConnected ? "Add your OpenAI API key in Settings to enable" : "Open tool"}
>
aria-label="Open Tool">
Open Tool
</button>
)}
@@ -140,7 +140,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
style={{
background: 'rgba(0, 0, 0, 0.04)',
color: 'rgba(0, 0, 0, 0.4)',
}}>
}} aria-label="Coming Soon">
Coming Soon
</button>
)}
@@ -149,7 +149,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
style={{
background: 'rgba(171, 162, 120, 0.15)',
color: '#92781e',
}}>
}} aria-label="Request Access">
Request Access
</button>
)}
@@ -225,7 +225,7 @@ function ConnectionStatusBanner({ isConnected, availableCount, onConfigure }: {
color: 'white',
boxShadow: '0 2px 8px rgba(202, 117, 67, 0.25)',
}}
>
aria-label="Add API Key">
Add API Key
</button>
</div>
@@ -783,7 +783,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
aria-label="Copy to clipboard">
Copy to clipboard
</button>
</div>
@@ -898,7 +898,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-price-tiers">Price Tiers</label>
<button type="button" onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Tier</button>
<button type="button" onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }} aria-label="+ Add Tier">+ Add Tier</button>
</div>
{priceTiers.map((tier, i) => (
<div key={`tier-${i}-${tier.tier}-${tier.price}`} className="flex items-center gap-2">
@@ -922,7 +922,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
/>
</div>
{priceTiers.length > 1 && (
<button type="button" onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
<button type="button" onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }} aria-label="✕"></button>
)}
</div>
))}
@@ -932,7 +932,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-historical-sales">Historical Sales</label>
<button type="button" onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
<button type="button" onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }} aria-label="+ Add Row">+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
<div className="grid grid-cols-4 gap-2 px-3 py-2">
@@ -971,7 +971,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
/>
</div>
{historicalSales.length > 1 && (
<button type="button" onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
<button type="button" onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }} aria-label="✕"></button>
)}
</div>
))}
@@ -1048,7 +1048,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
aria-label="Copy to clipboard">
Copy to clipboard
</button>
</div>
@@ -1154,7 +1154,7 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Recommended Subject Line</p>
<p className="text-base font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{result.subjectLine}</p>
<button type="button" onClick={() => copyToClipboard(result.subjectLine)} className="mt-2 text-xs flex items-center gap-1 transition-colors" style={{ color: 'var(--admin-accent)' }}>
<button type="button" onClick={() => copyToClipboard(result.subjectLine)} className="mt-2 text-xs flex items-center gap-1 transition-colors" style={{ color: 'var(--admin-accent)' }} aria-label="Copy">
Copy
</button>
</AdminCard>
@@ -1410,7 +1410,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
onClick={addStop}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-accent)' }}
>
aria-label="+ Add Stop">
+ Add Stop
</button>
</div>
@@ -1419,7 +1419,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
<div className="flex items-center justify-between">
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--admin-text-muted)' }}>Stop {i + 1}</span>
{stops.length > 2 && (
<button type="button" onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }}>
<button type="button" onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }} aria-label="Remove">
Remove
</button>
)}
@@ -1546,7 +1546,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
aria-label="Copy to clipboard">
Copy to clipboard
</button>
</div>
@@ -1638,7 +1638,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-historical-sales-2">Historical Sales</label>
<button type="button" onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
<button type="button" onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }} aria-label="+ Add Row">+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
<div className="grid grid-cols-4 gap-2 px-3 py-2">
@@ -1674,7 +1674,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
{historicalData.length > 1 && (
<button type="button" onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
<button type="button" onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }} aria-label="✕"></button>
)}
</div>
))}
@@ -1746,7 +1746,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
aria-label="Copy to clipboard">
Copy to clipboard
</button>
</div>
@@ -1773,7 +1773,7 @@ function ToolModal({ tool, brandId, brandName, onClose }: ModalProps) {
<span className="text-2xl">{tool.icon}</span>
<h2 className="text-lg font-bold" style={{ color: 'var(--admin-text-primary)' }}>{tool.title}</h2>
</div>
<button type="button" onClick={onClose} className="p-2 transition-colors" style={{ color: 'var(--admin-text-muted)' }}>
<button type="button" onClick={onClose} className="p-2 transition-colors" style={{ color: 'var(--admin-text-muted)' }} aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -1875,7 +1875,7 @@ export default function AIsettingsClient({
<button type="button"
onClick={() => setActiveModule(null)}
className={`admin-filter-tab ${activeModule === null ? 'admin-filter-tab--active' : ''}`}
>
aria-label="All ()">
All ({AI_TOOLS.length})
</button>
{modules.map((m) => {
@@ -1885,7 +1885,7 @@ export default function AIsettingsClient({
key={m}
onClick={() => setActiveModule(activeModule === m ? null : m)}
className={`admin-filter-tab ${activeModule === m ? 'admin-filter-tab--active' : ''}`}
>
aria-label="()">
{m} ({count})
</button>
);
@@ -236,7 +236,7 @@ export default function BillingClientPage({ overview }: Props) {
<button type="button"
onClick={() => setCompareOpen(!compareOpen)}
className="inline-flex items-center gap-1.5 text-sm font-medium text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)] transition-colors"
>
aria-label="plans">
{compareOpen ? "Hide" : "Compare"} plans
<svg className={`w-4 h-4 transition-transform ${compareOpen ? "rotate-180" : ""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
@@ -338,7 +338,7 @@ export default function BillingClientPage({ overview }: Props) {
<button type="button"
onClick={() => setCompareOpen(false)}
className="inline-flex items-center gap-1.5 text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
>
aria-label="Close">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -529,7 +529,7 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
<button type="button"
onClick={() => setShowAddModal(true)}
className="rounded-xl bg-violet-600 px-4 py-2.5 text-xs font-bold text-white hover:bg-violet-700 shrink-0"
>
aria-label="+ Add Integration">
+ Add Integration
</button>
</div>
@@ -617,7 +617,7 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
<div className="w-full max-w-lg rounded-2xl bg-zinc-900 border border-zinc-700 p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-bold text-zinc-100">Add Integration</h2>
<button type="button" onClick={() => setShowAddModal(false)} className="text-zinc-500 hover:text-zinc-300">
<button type="button" onClick={() => setShowAddModal(false)} className="text-zinc-500 hover:text-zinc-300" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -209,7 +209,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
<div className="mx-auto max-w-4xl flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm"> Back</button>
<button type="button" onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm" aria-label="← Back"> Back</button>
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Headgates & QR Codes</h1>
</div>
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">Manage headgates and generate QR codes for field access</p>
@@ -347,7 +347,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
Edit
</AdminButton>
{/* QR preview thumbnail */}
<button type="button" onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
<button type="button" onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR" aria-label="View / Print QR">
<Image
src={`/api/water-qr?token=${hg.headgate_token}&size=80`}
alt={`QR for ${hg.name}`}
@@ -396,7 +396,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]">
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
<button type="button" onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
<button type="button" onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]" aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -560,7 +560,7 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
<p className="font-bold text-[var(--admin-text-primary)]">{hg.name}</p>
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">QR Label Preview</p>
</div>
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]" aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -173,7 +173,7 @@ export default function IndianRiverFAQPage({ phone }: { phone?: string }) {
<button type="button"
onClick={() => setSearchQuery("")}
className="absolute inset-y-0 right-5 flex items-center text-stone-400 hover:text-stone-600 transition-colors"
>
aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
+1 -1
View File
@@ -168,7 +168,7 @@ export default function TuxedoFAQPage() {
<button type="button"
onClick={() => setSearchQuery("")}
className="absolute inset-y-0 right-4 flex items-center text-stone-400 hover:text-stone-600 transition-colors"
>
aria-label="Close">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -112,12 +112,12 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
{totalPages > 1 && (
<div className="flex items-center gap-1 ml-2">
<button type="button" onClick={() => setView(v => ({ ...v, page: Math.max(0, v.page - 1) }))} disabled={page === 0}
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Previous">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /></svg>
</button>
<span className="text-xs text-[var(--admin-text-muted)] px-1">{page + 1}/{totalPages}</span>
<button type="button" onClick={() => setView(v => ({ ...v, page: Math.min(totalPages - 1, v.page + 1) }))} disabled={page >= totalPages - 1}
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Next">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
@@ -168,10 +168,10 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
<td className="px-5 py-3.5 text-right">
<div className="flex items-center justify-end gap-2">
<button type="button" onClick={() => setSelectedCart(c)}
className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] px-2 py-1 rounded-lg hover:bg-stone-100 transition-all">View</button>
className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] px-2 py-1 rounded-lg hover:bg-stone-100 transition-all" aria-label="View">View</button>
{c.status === "active" && (
<button type="button" onClick={() => handleClose(c.id)} disabled={closing}
className="text-xs text-amber-600 hover:text-amber-700 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all">Close</button>
className="text-xs text-amber-600 hover:text-amber-700 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all" aria-label="Close">Close</button>
)}
{c.status === "active" && (
<button type="button" onClick={() => handleResend(c.id)} disabled={resending === c.id}
+6 -6
View File
@@ -118,7 +118,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
<button type="button"
onClick={() => setShowImport(true)}
className="flex items-center gap-2 rounded-xl border border-stone-200 bg-white px-4 py-2.5 text-sm font-medium text-stone-600 hover:bg-stone-50 transition-colors"
>
aria-label="Upload Schedule">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
@@ -127,7 +127,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
<button type="button"
onClick={() => setShowAdd(true)}
className="flex items-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 px-4 py-2.5 text-sm font-semibold text-white transition-colors"
>
aria-label="Add Stop">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
@@ -271,7 +271,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
<button type="button"
onClick={() => { setOpenMenu(null); setConfirmDelete(stop.id); }}
className="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50"
>
aria-label="Delete">
Delete
</button>
</div>
@@ -294,7 +294,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
<button type="button"
onClick={() => handleDelete(stop.id)}
className="flex-1 rounded-lg bg-[var(--admin-danger)] px-3 py-2 text-xs font-medium text-white hover:bg-[var(--admin-danger-hover)]"
>
aria-label="Delete">
Delete
</button>
</div>
@@ -321,7 +321,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="flex h-9 w-9 items-center justify-center rounded-xl border border-stone-200 text-stone-500 hover:border-stone-300 hover:text-stone-700 hover:bg-white disabled:opacity-40 disabled:cursor-not-allowed"
>
aria-label="Previous">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
@@ -331,7 +331,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="flex h-9 w-9 items-center justify-center rounded-xl border border-stone-200 text-stone-500 hover:border-stone-300 hover:text-stone-700 hover:bg-white disabled:opacity-40 disabled:cursor-not-allowed"
>
aria-label="Next">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
+1 -1
View File
@@ -167,7 +167,7 @@ export default function AdvancedShipping({ brandId }: { brandId: string }) {
onClick={handleTest}
disabled={saving}
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-stone-50 disabled:opacity-50"
>
aria-label="Test Connection">
Test Connection
</button>
<button type="button"
+1 -1
View File
@@ -140,7 +140,7 @@ export default function AdvancedSquareSync({ brandId }: { brandId: string }) {
<button type="button"
onClick={handleTest}
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-stone-50"
>
aria-label="Test Connection">
Test Connection
</button>
<button type="button"
+1 -1
View File
@@ -782,7 +782,7 @@ function NexusStateInput({
type="button"
onClick={() => removeState(code)}
className="hover:text-amber-200"
>
aria-label="Close">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
+2 -2
View File
@@ -178,7 +178,7 @@ function NewCampaignModal({
<button type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
@@ -234,7 +234,7 @@ export default function CampaignListPanel({ initialCampaigns, brandId = "6429430
<button type="button"
onClick={() => setShowNewModal(true)}
className="inline-flex items-center gap-1.5 rounded-lg bg-emerald-600 px-3 sm:px-4 py-2 text-xs sm:text-sm font-semibold text-white hover:bg-emerald-700 transition-colors"
>
aria-label="New Campaign">
{Icons.plus("h-3.5 w-3.5 sm:h-4 sm:w-4")}
<span>New Campaign</span>
</button>
+4 -4
View File
@@ -262,7 +262,7 @@ export default function ContactListPanel({
<button
type="submit"
className="rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700 active:bg-emerald-800 transition-colors shadow-sm"
>
aria-label="Search">
Search
</button>
</form>
@@ -357,7 +357,7 @@ export default function ContactListPanel({
disabled={deleting === c.id}
className="p-1.5 rounded-lg hover:bg-red-50 text-red-500 hover:text-red-700 disabled:opacity-50 transition-colors"
title="Delete contact"
>
aria-label="Delete contact">
{Icons.trash("h-4 w-4")}
</button>
</td>
@@ -424,7 +424,7 @@ export default function ContactListPanel({
onClick={() => handlePage(-1)}
disabled={page === 0}
className="inline-flex items-center gap-1.5 rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium disabled:opacity-50 hover:bg-stone-50 transition-colors"
>
aria-label="Previous">
{Icons.chevronLeft("h-4 w-4")}
<span>Previous</span>
</button>
@@ -432,7 +432,7 @@ export default function ContactListPanel({
onClick={() => handlePage(1)}
disabled={(page + 1) * limit >= total}
className="inline-flex items-center gap-1.5 rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium disabled:opacity-50 hover:bg-stone-50 transition-colors"
>
aria-label="Next">
<span>Next</span>
{Icons.chevronRight("h-4 w-4")}
</button>
+4 -4
View File
@@ -270,7 +270,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
<button type="button"
onClick={handleClose}
className="w-full sm:w-auto rounded-xl bg-[var(--admin-accent)] px-4 sm:px-5 py-2.5 text-sm font-medium text-white hover:bg-[var(--admin-accent-hover)] transition-colors"
>
aria-label="Done">
Done
</button>
</div>
@@ -291,7 +291,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
{error && (
<div className="flex items-start justify-between rounded-lg bg-red-50 p-3 sm:p-4 text-sm text-red-700 gap-3 border border-red-200">
<span className="text-xs sm:text-sm">{error}</span>
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0">
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0" aria-label="Close">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -385,7 +385,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
checked={role === r}
onChange={() => setRole(r)}
className="accent-[var(--admin-accent)]"
/>
aria-label="Role"/>
<div>
<span className="font-medium text-[var(--admin-text-primary)] capitalize text-sm">{r.replace("_", " ")}</span>
<p className="text-xs text-[var(--admin-text-muted)]">{roleDescriptions[r]}</p>
@@ -441,7 +441,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
onClick={handleClose}
disabled={saving}
className="w-full sm:w-auto rounded-xl border border-[var(--admin-border)] px-4 sm:px-5 py-2.5 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors disabled:opacity-50"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
+3 -3
View File
@@ -206,7 +206,7 @@ export default function LocationsTab({ locations, brandId }: Props) {
{deleteError && (
<div className="mx-5 my-3 rounded-lg border border-[var(--admin-danger)]/30 bg-[var(--admin-danger)]/10 px-4 py-3 text-sm text-[var(--admin-danger)]">
{deleteError}{" "}
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline">
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline" aria-label="Dismiss">
Dismiss
</button>
</div>
@@ -334,7 +334,7 @@ export default function LocationsTab({ locations, brandId }: Props) {
onClick={() => setEditing(loc as LocationForEdit)}
className="rounded-lg p-1.5 text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
title="Edit venue"
>
aria-label="Edit venue">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
@@ -344,7 +344,7 @@ export default function LocationsTab({ locations, brandId }: Props) {
disabled={pendingDeleteId === loc.id}
className="rounded-lg p-1.5 text-[var(--admin-text-muted)] hover:text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
title="Delete venue"
>
aria-label="Delete venue">
{pendingDeleteId === loc.id ? (
<svg className="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
+2 -2
View File
@@ -477,7 +477,7 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 text-sm border border-stone-200 rounded-xl bg-white text-stone-700 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
aria-label="Previous">
Previous
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
@@ -500,7 +500,7 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-4 py-2 text-sm border border-stone-200 rounded-xl bg-white text-stone-700 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
aria-label="Next">
Next
</button>
</div>
+1 -1
View File
@@ -101,7 +101,7 @@ export default function ProductTableBody({
<button type="button"
onClick={() => setDeleteError(null)}
className="ml-2 underline hover:no-underline"
>
aria-label="Dismiss">
Dismiss
</button>
</div>
+1 -1
View File
@@ -81,7 +81,7 @@ export default function ProductTableClient({ products }: Props) {
{deleteError && (
<div className="mx-5 my-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{deleteError}{" "}
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline">
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline" aria-label="Dismiss">
Dismiss
</button>
</div>
+2 -2
View File
@@ -573,7 +573,7 @@ function TableView({
<button type="button"
onClick={onDeleteCancel}
className="flex-1 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg)]"
>
aria-label="Cancel">
Cancel
</button>
<AdminButton
@@ -727,7 +727,7 @@ function CardView({
<button type="button"
onClick={onDeleteCancel}
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg)]"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
+1 -1
View File
@@ -682,7 +682,7 @@ export default function ReportsDashboard({
<h3 className="font-semibold text-white">AI Report Analysis</h3>
<span className="text-xs text-violet-200 ml-1">· {activeTab.replace("-", " ")}</span>
</div>
<button type="button" onClick={closeExplanation} className="text-violet-200 hover:text-white p-1">
<button type="button" onClick={closeExplanation} className="text-violet-200 hover:text-white p-1" aria-label="Close">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
+6 -6
View File
@@ -150,7 +150,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
? "bg-violet-100 border border-violet-300 text-violet-700"
: "bg-white border border-stone-200 text-stone-600 hover:border-stone-300"
}`}
>
aria-label="Use AI for text/PDF parsing">
<span className={`flex h-5 w-5 items-center justify-center rounded-full text-xs ${useAI ? "bg-violet-500 text-white" : "bg-stone-200 text-stone-500"}`}>
{useAI ? "✓" : "○"}
</span>
@@ -298,7 +298,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
<button type="button"
onClick={() => removeStop(idx)}
className="flex h-6 w-6 items-center justify-center rounded-lg text-red-500 hover:bg-red-50 hover:text-red-700 transition-colors"
>
aria-label="Close">
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -318,14 +318,14 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
<button type="button"
onClick={() => { setStep("idle"); setParsedStops([]); setWarnings([]); }}
className="rounded-xl border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
onClick={handleImport}
disabled={parsedStops.length === 0}
className="rounded-xl bg-emerald-600 hover:bg-emerald-700 px-5 py-2 text-sm font-bold text-white disabled:opacity-50 transition-colors"
>
aria-label="Create Stop">
Create {parsedStops.length} Stop{parsedStops.length !== 1 ? "s" : ""}
</button>
</div>
@@ -363,7 +363,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
<button type="button"
onClick={onClose}
className="rounded-xl bg-emerald-600 hover:bg-emerald-700 px-5 py-2.5 text-sm font-bold text-white transition-colors"
>
aria-label="Back to Stops">
Back to Stops
</button>
</div>
@@ -380,7 +380,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
<button type="button"
onClick={() => setStep("idle")}
className="rounded-xl border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
>
aria-label="Try Again">
Try Again
</button>
</div>
+17 -17
View File
@@ -51,7 +51,7 @@ function Accordion({ title, id, description, defaultOpen = false, accentColor =
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between px-5 py-4 hover:bg-stone-50 transition-colors"
aria-expanded={open}
>
aria-label=")}">
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-stone-900">{title}</span>
{description && (
@@ -412,13 +412,13 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
onKeyDown={e => e.key === "Enter" && (addEmail(), e.preventDefault())}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500"
placeholder="manager@farm.com" />
<button type="button" onClick={addEmail} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm">Add</button>
<button type="button" onClick={addEmail} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm" aria-label="Add">Add</button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{notificationEmails.map(e => (
<span key={e} className="inline-flex items-center gap-1.5 bg-stone-100 text-stone-700 text-xs px-2.5 py-1 rounded-lg">
{e}
<button type="button" onClick={() => removeEmail(e)} className="text-stone-400 hover:text-red-500 ml-1">
<button type="button" onClick={() => removeEmail(e)} className="text-stone-400 hover:text-red-500 ml-1" aria-label="Close">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -435,13 +435,13 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
onKeyDown={e => e.key === "Enter" && (addSms(), e.preventDefault())}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500"
placeholder="+1234567890" />
<button type="button" onClick={addSms} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm">Add</button>
<button type="button" onClick={addSms} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm" aria-label="Add">Add</button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{notificationSmsNumbers.map(n => (
<span key={n} className="inline-flex items-center gap-1.5 bg-stone-100 text-stone-700 text-xs px-2.5 py-1 rounded-lg">
{n}
<button type="button" onClick={() => removeSms(n)} className="text-stone-400 hover:text-red-500 ml-1">
<button type="button" onClick={() => removeSms(n)} className="text-stone-400 hover:text-red-500 ml-1" aria-label="Close">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -509,7 +509,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
<div className="flex items-center justify-between mb-4">
<p className="text-xs text-stone-500">Manage time tracking workers and PIN codes.</p>
<button type="button" onClick={openAddWorker}
className="text-xs bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1.5 rounded-lg font-semibold transition-all">+ Add Worker</button>
className="text-xs bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1.5 rounded-lg font-semibold transition-all" aria-label="+ Add Worker">+ Add Worker</button>
</div>
<div className="bg-white border border-stone-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
@@ -539,9 +539,9 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
<td className="px-4 py-3.5 text-stone-400 text-xs hidden md:table-cell">{w.last_used_at ? new Date(w.last_used_at).toLocaleDateString() : "—"}</td>
<td className="px-4 py-3.5 text-right">
<div className="flex items-center justify-end gap-1">
<button type="button" onClick={() => openEditWorker(w)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all">Edit</button>
<button type="button" onClick={() => handleResetPin(w.id)} className="text-xs text-amber-600 hover:text-amber-800 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all">Reset PIN</button>
<button type="button" onClick={() => handleDeleteWorker(w.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all">Delete</button>
<button type="button" onClick={() => openEditWorker(w)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all" aria-label="Edit">Edit</button>
<button type="button" onClick={() => handleResetPin(w.id)} className="text-xs text-amber-600 hover:text-amber-800 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all" aria-label="Reset PIN">Reset PIN</button>
<button type="button" onClick={() => handleDeleteWorker(w.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all" aria-label="Delete">Delete</button>
</div>
</td>
</tr>
@@ -566,7 +566,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
<div className="flex items-center justify-between mb-4">
<p className="text-xs text-stone-500">Define tasks workers can clock into.</p>
<button type="button" onClick={openAddTask}
className="text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1.5 rounded-lg font-semibold transition-all">+ Add Task</button>
className="text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1.5 rounded-lg font-semibold transition-all" aria-label="+ Add Task">+ Add Task</button>
</div>
<div className="bg-white border border-stone-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
@@ -596,8 +596,8 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
</td>
<td className="px-4 py-3.5 text-right">
<div className="flex items-center justify-end gap-1">
<button type="button" onClick={() => openEditTask(t)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all">Edit</button>
<button type="button" onClick={() => handleDeleteTask(t.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all">Delete</button>
<button type="button" onClick={() => openEditTask(t)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all" aria-label="Edit">Edit</button>
<button type="button" onClick={() => handleDeleteTask(t.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all" aria-label="Delete">Delete</button>
</div>
</td>
</tr>
@@ -615,7 +615,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
<div className="bg-white border border-stone-200 rounded-2xl w-full max-w-md shadow-xl">
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between">
<h3 className="text-lg font-bold text-stone-900">{editingWorker ? "Edit Worker" : "Add Worker"}</h3>
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-stone-400 hover:text-stone-600">
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-stone-400 hover:text-stone-600" aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -661,14 +661,14 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
</div>
{editingWorker && (
<div className="pt-2 border-t border-stone-100">
<button type="button" onClick={() => handleResetPin(editingWorker.id)} className="text-xs text-amber-600 hover:text-amber-800 font-semibold underline underline-offset-2">
<button type="button" onClick={() => handleResetPin(editingWorker.id)} className="text-xs text-amber-600 hover:text-amber-800 font-semibold underline underline-offset-2" aria-label="Reset PIN for this worker">
Reset PIN for this worker
</button>
</div>
)}
<div className="flex gap-3 pt-2">
<button type="button" onClick={() => setShowWorkerModal(false)}
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all">
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all" aria-label="Cancel">
Cancel
</button>
<button type="button" onClick={handleSaveWorker} disabled={submitting}
@@ -687,7 +687,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
<div className="bg-white border border-stone-200 rounded-2xl w-full max-w-md shadow-xl">
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between">
<h3 className="text-lg font-bold text-stone-900">{editingTask ? "Edit Task" : "Add Task"}</h3>
<button type="button" onClick={() => setShowTaskModal(false)} className="text-stone-400 hover:text-stone-600">
<button type="button" onClick={() => setShowTaskModal(false)} className="text-stone-400 hover:text-stone-600" aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -730,7 +730,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
</div>
<div className="flex gap-3 pt-2">
<button type="button" onClick={() => setShowTaskModal(false)}
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all">
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all" aria-label="Cancel">
Cancel
</button>
<button type="button" onClick={handleSaveTask} disabled={submitting}
+4 -4
View File
@@ -365,7 +365,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
<button type="button"
onClick={() => setSelectedStops(new Set())}
className="text-xs text-stone-500 hover:text-stone-700"
>
aria-label="Clear">
Clear
</button>
</div>
@@ -384,7 +384,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
{deleteError && (
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-50 px-4 py-2.5 text-sm text-red-700">
{deleteError}{" "}
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline">
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline" aria-label="Dismiss">
Dismiss
</button>
</div>
@@ -888,7 +888,7 @@ function StopRow({
setConfirmDelete(stop.id);
}}
className="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center gap-2"
>
aria-label="Delete">
<svg className="h-4 w-4 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
@@ -1066,7 +1066,7 @@ function StopCard({
<button type="button"
onClick={() => setConfirmDelete(false)}
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-stone-700 hover:bg-stone-50"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
@@ -345,7 +345,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Export Data</h3>
<div className="grid grid-cols-2 gap-3">
<button type="button" onClick={() => triggerDownload(buildExportUrl("quickbooks"))}
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-[var(--admin-accent)] transition-all text-left">
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-[var(--admin-accent)] transition-all text-left" aria-label="QuickBooks Time import CSV">
<span className="text-[var(--admin-text-muted)]">{Icons.clock("h-5 w-5")}</span>
<div>
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">QuickBooks</p>
@@ -353,7 +353,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
</div>
</button>
<button type="button" onClick={() => triggerDownload(buildExportUrl("payroll"))}
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-blue-500 transition-all text-left">
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-blue-500 transition-all text-left" aria-label="Payroll ADP / Gusto / Generic">
<span className="text-[var(--admin-text-muted)]">{Icons.dollarSign("h-5 w-5")}</span>
<div>
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">Payroll</p>
@@ -361,7 +361,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
</div>
</button>
<button type="button" onClick={() => triggerDownload(buildExportUrl("detailed"))}
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-violet-500 transition-all text-left">
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-violet-500 transition-all text-left" aria-label="Detailed Log Full audit report">
<span className="text-[var(--admin-text-muted)]">{Icons.clipboard("h-5 w-5")}</span>
<div>
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">Detailed Log</p>
@@ -369,7 +369,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
</div>
</button>
<button type="button" onClick={() => triggerDownload(buildExportUrl("summary"))}
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-amber-500 transition-all text-left">
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-amber-500 transition-all text-left" aria-label="Period Summary Per-worker totals">
<span className="text-[var(--admin-text-muted)]">{Icons.chart("h-5 w-5")}</span>
<div>
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">Period Summary</p>
@@ -629,7 +629,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
{notificationEmails.map(e => (
<span key={e} className="inline-flex items-center gap-1 bg-[var(--admin-bg-subtle)] text-[var(--admin-text-secondary)] text-xs px-2 py-1 rounded-lg">
{e}
<button type="button" onClick={() => removeEmail(e)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1">&times;</button>
<button type="button" onClick={() => removeEmail(e)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1" aria-label="&times;">&times;</button>
</span>
))}
</div>
@@ -648,7 +648,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
{notificationSmsNumbers.map(n => (
<span key={n} className="inline-flex items-center gap-1 bg-[var(--admin-bg-subtle)] text-[var(--admin-text-secondary)] text-xs px-2 py-1 rounded-lg">
{n}
<button type="button" onClick={() => removeSms(n)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1">&times;</button>
<button type="button" onClick={() => removeSms(n)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1" aria-label="&times;">&times;</button>
</span>
))}
</div>
@@ -673,7 +673,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-md shadow-xl">
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
<h3 className="text-lg font-bold text-[var(--admin-text-primary)]">{editingWorker ? "Edit Worker" : "Add Worker"}</h3>
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none">&times;</button>
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none" aria-label="&times;">&times;</button>
</div>
<div className="p-6 space-y-4">
{workerError && <div className="bg-red-50 border border-red-200 rounded-xl py-3 px-4 text-red-700 text-sm">{workerError}</div>}
@@ -739,7 +739,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-md shadow-xl">
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
<h3 className="text-lg font-bold text-[var(--admin-text-primary)]">{editingTask ? "Edit Task" : "Add Task"}</h3>
<button type="button" onClick={() => setShowTaskModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none">&times;</button>
<button type="button" onClick={() => setShowTaskModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none" aria-label="&times;">&times;</button>
</div>
<div className="p-6 space-y-4">
{taskError && <div className="bg-red-50 border border-red-200 rounded-xl py-3 px-4 text-red-700 text-sm">{taskError}</div>}
+16 -16
View File
@@ -345,7 +345,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
<button type="button"
onClick={openCreate}
className="rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800"
>
aria-label="+ Create User">
+ Create User
</button>
)}
@@ -367,7 +367,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
{error && (
<div className="mb-4 flex items-start justify-between rounded-lg bg-red-50 p-4 text-sm text-red-700 gap-3 border border-red-200">
<span>{error}</span>
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0">
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0" aria-label="Close">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -416,7 +416,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
<button type="button"
onClick={() => openEdit(user)}
className="text-xs font-medium text-stone-500 hover:text-stone-900"
>
aria-label="Edit">
Edit
</button>
<div className="relative overflow-visible">
@@ -424,7 +424,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
onClick={() => setActionMenuId(actionMenuId === user.id ? null : user.id)}
className="text-base font-medium text-stone-400 hover:text-stone-600 px-2 py-1"
title="More actions"
>
aria-label="More actions">
</button>
{actionMenuId === user.id && (
@@ -443,20 +443,20 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
<button type="button"
onClick={() => { setActionMenuId(null); handleForcePasswordChange(user.id); }}
className="w-full text-left px-3 py-2.5 text-stone-700 hover:bg-stone-50 flex items-center gap-2"
>
aria-label="↪ Require Password Change">
Require Password Change
</button>
<button type="button"
onClick={() => { setActionMenuId(null); handleSendResetLink(user.email); }}
className="w-full text-left px-3 py-2.5 text-stone-700 hover:bg-stone-50 flex items-center gap-2"
>
aria-label="✉ Send Reset Email">
Send Reset Email
</button>
{user.id !== currentUser.id && (
<button type="button"
onClick={() => { setActionMenuId(null); setDeleteConfirmId(user.id); }}
className="w-full text-left px-3 py-2.5 text-red-600 hover:bg-red-50 flex items-center gap-2 border-t border-stone-100 mt-1 pt-1"
>
aria-label="🗑 Delete">
🗑 Delete
</button>
)}
@@ -492,13 +492,13 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
<button type="button"
onClick={() => setDeleteConfirmId(null)}
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
onClick={() => handleDelete(deleteConfirmId)}
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
>
aria-label="Delete">
Delete
</button>
</div>
@@ -525,7 +525,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
<h2 className="text-lg font-bold text-stone-900">
{editing.isNew ? "Create User" : "Edit User"}
</h2>
<button type="button" onClick={closePanel} className="text-stone-400 hover:text-stone-600">
<button type="button" onClick={closePanel} className="text-stone-400 hover:text-stone-600" aria-label="Close">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -613,7 +613,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
checked={editing.role === r}
onChange={() => setRole(r)}
className="accent-stone-900"
/>
aria-label="Role"/>
<div>
<span className="font-medium text-stone-900 capitalize">{r.replace("_", " ")}</span>
<p className="text-xs text-stone-500">
@@ -686,7 +686,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
<button type="button"
onClick={closePanel}
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
@@ -714,22 +714,22 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
) : passwordModal.error ? (
<div>
<p className="mt-3 text-sm text-red-600">{passwordModal.error}</p>
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50">Close</button>
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50" aria-label="Close">Close</button>
</div>
) : passwordModal.tempPassword ? (
<div>
<p className="mt-3 text-sm text-stone-600">Temporary password for <span className="font-medium text-stone-900">{passwordModal.email}</span>:</p>
<div className="mt-3 flex items-center gap-2 rounded-xl bg-stone-50 p-4 ring-1 ring-stone-200">
<span className="font-mono text-lg font-bold text-stone-900 select-all">{passwordModal.tempPassword}</span>
<button type="button" onClick={copyTempPassword} className="shrink-0 rounded-lg bg-stone-200 px-3 py-1 text-xs font-medium text-stone-700 hover:bg-stone-300">Copy</button>
<button type="button" onClick={copyTempPassword} className="shrink-0 rounded-lg bg-stone-200 px-3 py-1 text-xs font-medium text-stone-700 hover:bg-stone-300" aria-label="Copy">Copy</button>
</div>
<p className="mt-2 text-xs text-stone-500">Share this password securely with the user. They will be required to change it on next login.</p>
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800">Done</button>
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800" aria-label="Done">Done</button>
</div>
) : passwordModal.message ? (
<div>
<p className="mt-3 text-sm text-stone-600">{passwordModal.message}</p>
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800">Done</button>
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800" aria-label="Done">Done</button>
</div>
) : null}
</div>
+8 -8
View File
@@ -590,21 +590,21 @@ export default function WaterLogAdminPanel({
<button type="button"
onClick={() => router.push(`/admin/water-log/headgates/${h.id}`)}
className="font-medium text-[#1a4d2e] hover:underline"
>
aria-label="Edit →">
Edit
</button>
<div className="flex items-center gap-2">
<button type="button"
onClick={() => handleRegenerateToken(h)}
className="text-[#57694e] hover:text-[#1a4d2e]"
>
aria-label="Rotate token">
Rotate token
</button>
<span className="text-[#d4d9d3]">·</span>
<button type="button"
onClick={() => handleDeleteHg(h)}
className="text-[#b91c1c] hover:text-[#7f1d1d]"
>
aria-label="Delete">
Delete
</button>
</div>
@@ -723,14 +723,14 @@ export default function WaterLogAdminPanel({
<button type="button"
onClick={() => handleResetPin(u)}
className="text-[#57694e] hover:text-[#1a4d2e]"
>
aria-label="PIN">
PIN
</button>
<span className="text-[#d4d9d3]">·</span>
<button type="button"
onClick={() => handleDeleteUser(u)}
className="text-[#b91c1c] hover:text-[#7f1d1d]"
>
aria-label="Off">
Off
</button>
</div>
@@ -833,7 +833,7 @@ export default function WaterLogAdminPanel({
setFilterVia("");
}}
className="text-xs font-medium text-[#57694e] hover:text-[#1a4d2e] hover:underline"
>
aria-label="Clear filters">
Clear filters
</button>
)}
@@ -907,7 +907,7 @@ export default function WaterLogAdminPanel({
router.push(`/admin/water-log/entries/${e.id}`)
}
className="font-medium text-[#1a4d2e] opacity-0 transition-opacity group-hover:opacity-100 hover:underline"
>
aria-label="Edit →">
Edit
</button>
</td>
@@ -1061,7 +1061,7 @@ function PinBanner({
<button type="button"
onClick={onDismiss}
className="rounded-lg border border-[#1a4d2e]/30 px-3 py-1.5 text-xs font-semibold text-[#1a4d2e] hover:bg-white"
>
aria-label="Dismiss">
Dismiss
</button>
</div>
@@ -94,12 +94,12 @@ export default function WelcomeSequenceDashboard({ brandId }: Props) {
{totalPages > 1 && (
<div className="flex items-center gap-1 ml-2">
<button type="button" onClick={() => setPage(p => Math.max(0, p - 1))} disabled={page === 0}
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Previous">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /></svg>
</button>
<span className="text-xs text-[var(--admin-text-muted)] px-1">{page + 1}/{totalPages}</span>
<button type="button" onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))} disabled={page >= totalPages - 1}
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Next">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
@@ -122,7 +122,7 @@ export default function AdminFilterBar({
<button type="button"
onClick={onAddClick}
className="ml-auto rounded-xl bg-[var(--admin-accent)] px-4 py-2 text-xs font-bold text-white hover:bg-[var(--admin-accent-hover)] transition-colors shadow-[var(--admin-shadow-sm)]"
>
aria-label="+">
+ {addLabel}
</button>
)}
@@ -21,7 +21,7 @@ export default function AdminPagination({
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 0}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
>
aria-label="Previous">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
@@ -69,7 +69,7 @@ export default function AdminPagination({
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages - 1}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
>
aria-label="Next">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
@@ -102,7 +102,7 @@ export function AdminSimplePagination({
onClick={() => onPageChange(page - 1)}
disabled={page === 0}
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
aria-label="Previous">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
@@ -111,7 +111,7 @@ export function AdminSimplePagination({
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages - 1}
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
aria-label="Next">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
+2 -2
View File
@@ -101,7 +101,7 @@ export default function DataTable<T>({
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="flex h-7 w-7 items-center justify-center rounded border border-stone-200 text-stone-500 hover:text-stone-700 hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
aria-label="Previous">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
@@ -110,7 +110,7 @@ export default function DataTable<T>({
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="flex h-7 w-7 items-center justify-center rounded border border-stone-200 text-stone-500 hover:text-stone-700 hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
aria-label="Next">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
+5 -5
View File
@@ -145,13 +145,13 @@ export default function CookieConsentBanner() {
<button type="button"
onClick={() => setShowPreferences(false)}
className="flex-1 px-4 py-2.5 border border-gray-300 text-gray-700 rounded-xl font-medium hover:bg-gray-50"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
onClick={handleSavePreferences}
className="flex-1 px-4 py-2.5 bg-[#1a4d2e] text-white rounded-xl font-medium hover:bg-[#2d6a4f]"
>
aria-label="Save Preferences">
Save Preferences
</button>
</div>
@@ -175,19 +175,19 @@ export default function CookieConsentBanner() {
<button type="button"
onClick={() => setShowPreferences(true)}
className="px-4 py-2 text-sm font-medium text-[#666] hover:text-[#1a1a1a] transition-colors"
>
aria-label="Customize">
Customize
</button>
<button type="button"
onClick={handleRejectAll}
className="px-4 py-2.5 text-sm font-medium text-[#888] border border-gray-300 rounded-xl hover:bg-gray-50 transition-colors"
>
aria-label="Reject All">
Reject All
</button>
<button type="button"
onClick={handleAcceptAll}
className="px-5 py-2.5 text-sm font-medium text-white bg-[#1a4d2e] rounded-xl hover:bg-[#2d6a4f] transition-colors"
>
aria-label="Accept All">
Accept All
</button>
</div>
@@ -267,7 +267,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<button type="button"
onClick={() => setOpen(true)}
className="rounded-xl border border-stone-200 bg-white px-5 py-3 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
>
aria-label="FSMA Report">
{Icons.clipboard("h-4 w-4 mr-1.5")} FSMA Report
</button>
@@ -330,7 +330,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
onClick={() => setOpen(false)}
className="flex h-9 w-9 items-center justify-center rounded-full transition-all"
style={{ background: "rgba(0, 0, 0, 0.04)", color: "rgba(0, 0, 0, 0.4)" }}
>
aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -362,7 +362,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<button type="button"
onClick={fetchComplianceData}
className="rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-bold text-white hover:bg-emerald-700 transition-colors"
>
aria-label="Refresh">
{Icons.refresh("h-4 w-4 mr-1.5")} Refresh
</button>
</div>
@@ -616,14 +616,14 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<button type="button"
onClick={() => setOpen(false)}
className="rounded-xl border border-stone-200 px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
>
aria-label="Close">
Close
</button>
<button type="button"
onClick={handleDownload}
disabled={loading || !data}
className="rounded-xl bg-blue-600 px-5 py-2 text-sm font-bold text-white hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center gap-2"
>
aria-label="Download CSV">
<span className="flex items-center gap-1.5 text-sm font-semibold">{Icons.download("h-4 w-4")} Download CSV</span>
</button>
</div>
@@ -404,7 +404,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
onClick={() => setNotesOpen(!notesOpen)}
className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider transition-colors"
style={{ color: "var(--admin-text-muted)" }}
>
aria-label="Notes">
<span>Notes</span>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d={notesOpen ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6"} />
@@ -436,7 +436,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
borderColor: "var(--admin-border)",
color: "var(--admin-text-muted)"
}}
>
aria-label="Cancel">
Cancel
</button>
<button
+8 -8
View File
@@ -192,7 +192,7 @@ export default function LotListTable({
<button type="button"
onClick={onCreateNew}
className="inline-flex items-center gap-2 rounded-xl bg-emerald-600 px-4 py-2 text-sm font-bold text-white hover:bg-emerald-700 transition-colors shadow-sm"
>
aria-label="New Lot">
{Icons.plus("h-4 w-4")}
<span>New Lot</span>
</button>
@@ -232,7 +232,7 @@ export default function LotListTable({
className={`rounded-xl border px-4 py-3 text-sm font-semibold transition-colors flex items-center gap-2 ${
showBulk ? "border-emerald-600 bg-emerald-600 text-white" : "border-stone-200 text-stone-600 hover:bg-stone-50"
}`}
>
aria-label="Bulk">
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points={showBulk ? "9 11 12 14 22 4" : "3 6 9 12 15 18"}/>
</svg>
@@ -250,42 +250,42 @@ export default function LotListTable({
<button type="button"
onClick={handleBulkMarkLoaded}
className="rounded-xl bg-amber-600 hover:bg-amber-500 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
aria-label="Mark Loaded">
{Icons.truck("h-4 w-4")}
<span>Mark Loaded</span>
</button>
<button type="button"
onClick={handleBulkMarkUsed}
className="rounded-xl bg-amber-600 hover:bg-amber-500 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
aria-label="Mark as Used">
{Icons.package("h-4 w-4")}
<span>Mark as Used</span>
</button>
<button type="button"
onClick={() => handleBulkStickers("field")}
className="rounded-xl bg-white/20 hover:bg-white/30 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
aria-label="Field Stickers">
{Icons.printer("h-4 w-4")}
<span>Field Stickers</span>
</button>
<button type="button"
onClick={() => handleBulkStickers("shed")}
className="rounded-xl bg-white/20 hover:bg-white/30 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
aria-label="Shed Stickers">
{Icons.printer("h-4 w-4")}
<span>Shed Stickers</span>
</button>
<button type="button"
onClick={handleBulkExport}
className="rounded-xl bg-white/20 hover:bg-white/30 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
aria-label="Reports`}">
{Icons.file("h-4 w-4")}
<span>{selected.size === 1 ? "Report" : `${selected.size} Reports`}</span>
</button>
<button type="button"
onClick={() => setSelected(new Set())}
className="rounded-xl border border-white/30 px-4 py-2 text-sm font-medium text-white/70 hover:text-white transition-colors"
>
aria-label="Clear">
Clear
</button>
</div>
+3 -3
View File
@@ -211,7 +211,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
className={`flex-1 py-3 text-sm font-semibold transition-colors ${
mode === "camera" ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-text-primary)]" : "text-[var(--admin-text-muted)]"
}`}
>
aria-label="Camera">
<svg className="w-4 h-4 inline-block mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" />
<circle cx="12" cy="13" r="3" />
@@ -223,7 +223,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
className={`flex-1 py-3 text-sm font-semibold transition-colors ${
mode === "manual" ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-text-primary)]" : "text-[var(--admin-text-muted)]"
}`}
>
aria-label="Manual">
<svg className="w-4 h-4 inline-block mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h8M6 16h12" />
@@ -296,7 +296,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
type="submit"
disabled={!manualInput.trim() || isLoading}
className="w-full rounded-xl bg-stone-800 py-3.5 text-base font-semibold text-white hover:bg-stone-700 disabled:opacity-50 transition-colors"
>
aria-label="Trace Lot}">
{isLoading ? "Loading..." : <><span className="inline-flex items-center gap-1.5">{Icons.search("h-4 w-4")} Trace Lot</span></>}
</button>
</form>
@@ -99,7 +99,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
<h3 className="text-base font-semibold text-stone-900 flex items-center gap-2">{Icons.printer("h-5 w-5")} Print Sticker</h3>
<p className="text-xs text-stone-400 mt-0.5">{lot.lot_number} · {lot.crop_type}</p>
</div>
<button type="button" onClick={onClose} className="text-stone-400 hover:text-stone-600">
<button type="button" onClick={onClose} className="text-stone-400 hover:text-stone-600" aria-label="Close">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -124,7 +124,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
? "border-emerald-600 bg-emerald-600 text-white"
: "border-stone-200 hover:bg-stone-50"
}`}
>
aria-label="Sticker">
<div className="text-sm font-semibold">{label} Sticker</div>
<div className={`text-[10px] mt-0.5 ${stickerType === t ? "text-stone-300" : "text-stone-400"}`}>{desc}</div>
</button>
@@ -147,7 +147,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
? "border-emerald-600 bg-emerald-600 text-white"
: "border-stone-200 hover:bg-stone-50"
}`}
>
aria-label="×">
<div className="text-sm font-semibold">{w} × {h}</div>
<div className={`text-[10px] mt-0.5 ${stickerSize === s ? "text-stone-300" : "text-stone-400"}`}>
{s === "4x2" ? "Field sticker" : "Shed sticker"}
@@ -299,14 +299,14 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
<button type="button"
onClick={onClose}
className="rounded-xl border border-stone-200 px-4 py-2.5 text-sm font-semibold text-stone-600 hover:bg-stone-50"
>
aria-label="Cancel">
Cancel
</button>
<button type="button"
onClick={handlePrint}
disabled={loading}
className="rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-bold text-white hover:bg-emerald-700 disabled:opacity-50 flex items-center gap-2"
>
aria-label="Print × `} Sticker}">
{loading ? "Generating PDF..." : <><span className="inline-flex items-center gap-1.5">{Icons.printer("h-4 w-4")} Print {copies === 1 ? "" : `${copies}× `}{stickerType === "field" ? "Field" : "Shed"} Sticker{copies > 1 ? "s" : ""}</span></>}
</button>
</div>
@@ -324,7 +324,7 @@ export default function TuxedoVideoHero({
onClick={onSecondaryClick}
className="group flex items-center gap-3 text-sm font-semibold"
style={{ color: "rgba(255,255,255,0.8)" }}
>
aria-label="Next">
<span className="uppercase tracking-widest text-[11px]">{secondaryButton}</span>
<span
className="w-10 h-10 rounded-full border flex items-center justify-center transition-all duration-300 group-hover:bg-white/10 group-hover:border-white/30"
+5 -5
View File
@@ -247,7 +247,7 @@ function SectionHeader({
<button type="button"
onClick={onToggle}
className="flex items-center gap-2 text-base font-bold text-stone-800 hover:text-stone-900"
>
aria-label=")}">
<span className="text-stone-400">{title}</span>
{count !== undefined && (
<span className="rounded-full bg-stone-100 px-2 py-0.5 text-xs font-semibold text-stone-600">
@@ -486,7 +486,7 @@ export default function WaterAdminClient() {
<button type="button"
onClick={() => setShowAddHg((v) => !v)}
className="rounded-lg bg-stone-900 px-3 py-1.5 text-xs font-semibold text-white hover:bg-stone-700 min-h-[36px] min-w-[36px]"
>
aria-label="+">
+
</button>
}
@@ -523,7 +523,7 @@ export default function WaterAdminClient() {
type="button"
onClick={() => { setShowAddHg(false); setNewHgError(null); }}
className="rounded-lg border border-stone-300 px-3 py-2 text-sm text-stone-600 hover:bg-stone-50 min-h-[44px]"
>
aria-label="×">
×
</button>
</div>
@@ -587,7 +587,7 @@ export default function WaterAdminClient() {
<button type="button"
onClick={() => setShowAddUser((v) => !v)}
className="rounded-lg bg-stone-900 px-3 py-1.5 text-xs font-semibold text-white hover:bg-stone-700 min-h-[36px] min-w-[36px]"
>
aria-label="+">
+
</button>
}
@@ -623,7 +623,7 @@ export default function WaterAdminClient() {
type="button"
onClick={() => { setShowAddUser(false); setNewUserError(null); }}
className="rounded-lg border border-stone-300 px-3 py-2 text-sm text-stone-600 hover:bg-stone-50 min-h-[44px]"
>
aria-label="×">
×
</button>
</div>
@@ -45,7 +45,7 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD
<h2 className="text-lg font-bold text-slate-900">Order Details</h2>
<p className="text-sm text-slate-500 font-mono">{order.invoice_number ?? order.id.slice(0, 8)}</p>
</div>
<button type="button" onClick={onClose} className="text-slate-400 hover:text-slate-600">
<button type="button" onClick={onClose} className="text-slate-400 hover:text-slate-600" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
</svg>
@@ -152,7 +152,7 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD
<button type="button"
onClick={() => onRecordDeposit(order.id)}
className="rounded-xl bg-purple-600 px-4 py-2 text-sm font-semibold text-white hover:bg-purple-700"
>
aria-label="Record Deposit">
Record Deposit
</button>
)}
@@ -167,7 +167,7 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD
<button type="button"
onClick={onClose}
className="ml-auto rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
>
aria-label="Close">
Close
</button>
</div>
@@ -30,14 +30,14 @@ export default function AddRecipientForm({ onAdd }: { onAdd: (email: string, nam
type="email" placeholder="Email address"
value={email} onChange={e => { setEmail(e.target.value); setError(""); }}
onKeyDown={e => e.key === "Enter" && handleAdd()}
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" />
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" aria-label="Email address"/>
</div>
<div className="w-36">
<input
type="text" placeholder="Name (optional)"
value={name} onChange={e => setName(e.target.value)}
onKeyDown={e => e.key === "Enter" && handleAdd()}
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" />
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" aria-label="Name (optional)"/>
</div>
<AdminButton
variant="primary"
@@ -52,7 +52,7 @@ export default function CustomerPricingPanel({ customer, products, onClose, onMs
<h2 className="text-lg font-bold text-[var(--admin-text-primary)]">Pricing Overrides</h2>
<p className="text-sm text-[var(--admin-text-muted)]">{customer.company_name}</p>
</div>
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]">
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -327,7 +327,7 @@ export default function CustomersTab({
<div>
<label htmlFor="ws-credit-limit" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Credit Limit ($)</label>
<input id="ws-credit-limit" type="number" value={form.creditLimit} onChange={e => setForm(f => ({ ...f, creditLimit: Number(e.target.value) }))}
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="0 = unlimited" />
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="0 = unlimited" aria-label="0 = unlimited"/>
</div>
</div>
@@ -344,12 +344,12 @@ export default function CustomersTab({
<div>
<label htmlFor="ws-deposit-threshold" className="block text-xs text-[var(--admin-text-muted)] mb-1">Threshold ($ order total to trigger)</label>
<input id="ws-deposit-threshold" type="number" value={form.depositThreshold} onChange={e => setForm(f => ({ ...f, depositThreshold: e.target.value }))}
className="w-full rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="0" />
className="w-full rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="0" aria-label="0"/>
</div>
<div>
<label htmlFor="ws-deposit-pct" className="block text-xs text-[var(--admin-text-muted)] mb-1">Deposit %</label>
<input id="ws-deposit-pct" type="number" value={form.depositPercentage} onChange={e => setForm(f => ({ ...f, depositPercentage: e.target.value }))}
className="w-full rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="20" min="1" max="100" />
className="w-full rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="20" min="1" max="100" aria-label="20"/>
</div>
</div>
)}
@@ -428,7 +428,7 @@ export default function CustomersTab({
onClick={() => openPriceSheetModal([c.id])}
title="Send price sheet"
className="inline-flex items-center justify-center rounded-xl w-9 h-9 text-[var(--admin-accent)] hover:text-[var(--admin-accent-text)] hover:bg-[var(--admin-accent-light)] transition-colors"
>
aria-label="Send price sheet">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
</button>
<div className="relative">
@@ -436,7 +436,7 @@ export default function CustomersTab({
onClick={(e) => toggleCustomerActions(c.id, e)}
title="More actions"
className="inline-flex items-center justify-center rounded-xl w-9 h-9 text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
>
aria-label="More actions">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><circle cx="10" cy="4" r="1.5"/><circle cx="10" cy="10" r="1.5"/><circle cx="10" cy="16" r="1.5"/></svg>
</button>
{openCustomerActions === c.id && (
@@ -447,14 +447,14 @@ export default function CustomersTab({
<button type="button"
onClick={() => { setOpenCustomerActions(null); openEdit(c); }}
className="w-full text-left px-4 py-3 text-[var(--admin-accent)] hover:bg-[var(--admin-accent-light)] flex items-center gap-3 font-medium"
>
aria-label="Edit Customer">
<svg className="w-4 h-4 text-[var(--admin-accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Edit Customer
</button>
<button type="button"
onClick={() => { setOpenCustomerActions(null); setPricingCustomer(c); }}
className="w-full text-left px-4 py-3 text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] flex items-center gap-3"
>
aria-label="Pricing">
<svg className="w-4 h-4 text-[var(--admin-text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Pricing
</button>
@@ -45,7 +45,7 @@ export default function PriceSheetModal({
{customerCount === 1 ? "1 customer" : `${customerCount} customers`}
</p>
</div>
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]">
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
@@ -223,7 +223,7 @@ export default function ProductsTab({ products, brandId, onMsg, onRefresh }: Pro
onClick={(e) => toggleProductActions(p.id, e)}
title="More actions"
className="inline-flex items-center justify-center rounded-xl w-9 h-9 text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
>
aria-label="More actions">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><circle cx="10" cy="4" r="1.5"/><circle cx="10" cy="10" r="1.5"/><circle cx="10" cy="16" r="1.5"/></svg>
</button>
{openProductActions === p.id && (
@@ -234,7 +234,7 @@ export default function ProductsTab({ products, brandId, onMsg, onRefresh }: Pro
<button type="button"
onClick={() => { setOpenProductActions(null); openEdit(p); }}
className="w-full text-left px-4 py-3 text-[var(--admin-accent)] hover:bg-[var(--admin-accent-light)] flex items-center gap-3 font-medium"
>
aria-label="Edit Product">
<svg className="w-4 h-4 text-[var(--admin-accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Edit Product
</button>
@@ -172,7 +172,7 @@ export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canMa
<div>
<label htmlFor="fld-1-minimum-order-amount" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Minimum Order Amount ($)</label>
<input id="fld-1-minimum-order-amount" type="number" value={form.minOrderAmount} onChange={e => setForm(f => ({ ...f, minOrderAmount: e.target.value }))}
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" step="0.01" placeholder="No minimum" />
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" step="0.01" placeholder="No minimum" aria-label="No minimum"/>
</div>
<div>
<label htmlFor="fld-2-from-email" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">From Email</label>
@@ -212,7 +212,7 @@ export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canMa
<div className="col-span-2">
<label htmlFor="fld-9-invoice-business-website" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Invoice Business Website</label>
<input id="fld-9-invoice-business-website" value={form.invoiceBusinessWebsite} onChange={e => setForm(f => ({ ...f, invoiceBusinessWebsite: e.target.value }))}
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="https://" />
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="https://" aria-label="https://"/>
</div>
{/* Team Notification Recipients */}
@@ -250,10 +250,10 @@ export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canMa
type="text" placeholder="Name (optional)"
value={r.name ?? ""}
onChange={e => updateRecipientName(idx, e.target.value)}
className="rounded-xl border border-[var(--admin-border)] px-2.5 py-1.5 text-xs w-36 outline-none focus:border-[var(--admin-accent)]" />
className="rounded-xl border border-[var(--admin-border)] px-2.5 py-1.5 text-xs w-36 outline-none focus:border-[var(--admin-accent)]" aria-label="Name (optional)"/>
<button type="button" onClick={() => removeRecipient(idx)}
className="text-[var(--admin-danger)] hover:text-[var(--admin-danger)] text-xs font-medium px-2 py-1 rounded-lg hover:bg-[var(--admin-danger-light)] transition-colors"
title="Remove recipient">
title="Remove recipient" aria-label="Remove recipient">
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -121,7 +121,7 @@ export default function WebhookSettingsSection({ brandId, onMsg }: WebhookSettin
onChange={e => setSettings(s => ({ ...s, url: e.target.value }))}
placeholder="https://your-system.com/webhooks/wholesale"
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]"
/>
aria-label="https://your-system.com/webhooks/wholesale"/>
</div>
<div>
@@ -132,7 +132,7 @@ export default function WebhookSettingsSection({ brandId, onMsg }: WebhookSettin
onChange={e => setSettings(s => ({ ...s, secret: e.target.value }))}
placeholder="Leave blank to keep current secret"
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)] font-mono"
/>
aria-label="Leave blank to keep current secret"/>
<p className="mt-1 text-xs text-[var(--admin-text-muted)]">
The receiver should verify the <code className="bg-[var(--admin-bg-subtle)] px-1">X-Webhook-Signature</code> header using this secret.
</p>