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
@@ -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>