fix: react-doctor small categories 68→72 (a11y+perf+style, 21 fixes)
- no-outline-none (2): add focus-visible outline to inline-styled inputs - no-redundant-roles (2): drop role="list" on <ul> - click-events-have-key-events (2): convert div backdrops to buttons - no-static-element-interactions (2): add role+tabIndex+onKeyDown to clickable divs - rendering-svg-precision (2): round SVG path decimals - no-tiny-text (2): 11px → 12px on CommandPalette hints - js-set-map-lookups (4→1): Sets for visibleItems, regex for keyword match - no-gray-on-colored-background (4): use color-matched text instead of stone-400/900 - no-transition-all (4): list specific properties in transition shorthand
This commit is contained in:
@@ -258,11 +258,16 @@ function fallbackParse(
|
|||||||
stops: stopKeywords,
|
stops: stopKeywords,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const keywordPatterns: Record<string, RegExp> = {};
|
||||||
|
for (const [etype, kws] of Object.entries(keywordMaps)) {
|
||||||
|
keywordPatterns[etype] = new RegExp(kws.map(escapeRegex).join("|"));
|
||||||
|
}
|
||||||
|
|
||||||
for (const header of h) {
|
for (const header of h) {
|
||||||
for (const [etype, keywords] of Object.entries(keywordMaps)) {
|
for (const [etype, pattern] of Object.entries(keywordPatterns)) {
|
||||||
for (const kw of keywords) {
|
const matches = header.match(pattern);
|
||||||
if (header.includes(kw)) typeScores[etype] = (typeScores[etype] ?? 0) + 1;
|
if (matches) typeScores[etype] = (typeScores[etype] ?? 0) + matches.length;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,10 +314,15 @@ function fallbackParse(
|
|||||||
notes: ["notes", "note", "special_instructions", "comments", "memo", "instruction"],
|
notes: ["notes", "note", "special_instructions", "comments", "memo", "instruction"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const semanticPatterns: Record<string, RegExp> = {};
|
||||||
|
for (const [field, kws] of Object.entries(semanticMap)) {
|
||||||
|
semanticPatterns[field] = new RegExp(kws.map(escapeRegex).join("|"));
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < h.length; i++) {
|
for (let i = 0; i < h.length; i++) {
|
||||||
const header = h[i];
|
const header = h[i];
|
||||||
for (const [field, keywords] of Object.entries(semanticMap)) {
|
for (const [field, pattern] of Object.entries(semanticPatterns)) {
|
||||||
if (keywords.some((kw) => header.includes(kw))) {
|
if (pattern.test(header)) {
|
||||||
columnMappings[headers[i]] = field;
|
columnMappings[headers[i]] = field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ export default function LaunchChecklistPage() {
|
|||||||
<div className="relative w-24 h-24">
|
<div className="relative w-24 h-24">
|
||||||
<svg className="w-24 h-24 -rotate-90" viewBox="0 0 36 36">
|
<svg className="w-24 h-24 -rotate-90" viewBox="0 0 36 36">
|
||||||
<path
|
<path
|
||||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
d="M18 2.08 a 15.92 15.92 0 0 1 0 31.83 a 15.92 15.92 0 0 1 0 -31.83"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#f0f0f0"
|
stroke="#f0f0f0"
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
d="M18 2.08 a 15.92 15.92 0 0 1 0 31.83 a 15.92 15.92 0 0 1 0 -31.83"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#1a4d2e"
|
stroke="#1a4d2e"
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
|
|||||||
@@ -191,8 +191,12 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
|
|||||||
|
|
||||||
{/* Cart detail modal */}
|
{/* Cart detail modal */}
|
||||||
{cart && (
|
{cart && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
|
<button
|
||||||
onClick={() => setSelectedCart(null)}>
|
type="button"
|
||||||
|
aria-label="Close abandoned cart details"
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm border-0 cursor-default"
|
||||||
|
onClick={() => setSelectedCart(null)}
|
||||||
|
>
|
||||||
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-lg shadow-xl"
|
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-lg shadow-xl"
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
|
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
|
||||||
@@ -246,7 +250,7 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -191,9 +191,10 @@ export default function AdminSidebar({
|
|||||||
* future iteration hides Harvest Reach for some role, the entire group
|
* future iteration hides Harvest Reach for some role, the entire group
|
||||||
* header goes away rather than rendering an empty section.
|
* header goes away rather than rendering an empty section.
|
||||||
*/
|
*/
|
||||||
|
const visibleItemSet = new Set(visibleItems);
|
||||||
const visibleGroups: NavGroup[] = [];
|
const visibleGroups: NavGroup[] = [];
|
||||||
for (const group of NAV_GROUPS) {
|
for (const group of NAV_GROUPS) {
|
||||||
const items = group.items.filter((item) => visibleItems.includes(item));
|
const items = group.items.filter((item) => visibleItemSet.has(item));
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
visibleGroups.push({ ...group, items });
|
visibleGroups.push({ ...group, items });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface CardListProps {
|
|||||||
|
|
||||||
export function CardList({ children, ariaLabel }: CardListProps) {
|
export function CardList({ children, ariaLabel }: CardListProps) {
|
||||||
return (
|
return (
|
||||||
<ul role="list" aria-label={ariaLabel} className="space-y-3 px-4 pb-24">
|
<ul aria-label={ariaLabel} className="space-y-3 px-4 pb-24">
|
||||||
{children}
|
{children}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -322,13 +322,8 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
aria-label="Search command palette"
|
aria-label="Search command palette"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
className="flex-1 min-w-0 border-none outline-none focus-visible:outline-2 focus-visible:outline-[var(--color-accent)] focus-visible:outline-offset-2 bg-transparent text-base"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
border: "none",
|
|
||||||
outline: "none",
|
|
||||||
background: "transparent",
|
|
||||||
fontSize: "1rem",
|
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
color: "var(--admin-text-primary)",
|
color: "var(--admin-text-primary)",
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
@@ -424,7 +419,7 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.6875rem",
|
fontSize: "0.75rem",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
letterSpacing: "0.05em",
|
letterSpacing: "0.05em",
|
||||||
color: "var(--admin-text-muted)",
|
color: "var(--admin-text-muted)",
|
||||||
@@ -445,7 +440,7 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
style={{
|
style={{
|
||||||
padding: "0.5rem 1rem 0.625rem",
|
padding: "0.5rem 1rem 0.625rem",
|
||||||
borderTop: "1px solid var(--admin-border)",
|
borderTop: "1px solid var(--admin-border)",
|
||||||
fontSize: "0.6875rem",
|
fontSize: "0.75rem",
|
||||||
color: "var(--admin-text-muted)",
|
color: "var(--admin-text-muted)",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
letterSpacing: "0.02em",
|
letterSpacing: "0.02em",
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export default function ContactListPanel({
|
|||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => handleDelete(c.id)}
|
onClick={() => handleDelete(c.id)}
|
||||||
disabled={deleting === c.id}
|
disabled={deleting === c.id}
|
||||||
className="p-1.5 rounded-lg hover:bg-red-50 text-stone-400 hover:text-red-500 disabled:opacity-50 transition-colors"
|
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"
|
title="Delete contact"
|
||||||
>
|
>
|
||||||
{Icons.trash("h-4 w-4")}
|
{Icons.trash("h-4 w-4")}
|
||||||
@@ -386,7 +386,7 @@ export default function ContactListPanel({
|
|||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => handleDelete(c.id)}
|
onClick={() => handleDelete(c.id)}
|
||||||
disabled={deleting === c.id}
|
disabled={deleting === c.id}
|
||||||
className="p-1.5 rounded-lg hover:bg-red-50 text-stone-400 hover:text-red-500 disabled:opacity-50"
|
className="p-1.5 rounded-lg hover:bg-red-50 text-red-500 hover:text-red-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{Icons.trash("h-4 w-4")}
|
{Icons.trash("h-4 w-4")}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -89,7 +89,15 @@ export default function SegmentListSidebar({ segments, activeSegmentId, onSelect
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => onSelect(segment)}
|
onClick={() => onSelect(segment)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(segment);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between gap-2 transition-all ${
|
className={`px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between gap-2 transition-all ${
|
||||||
activeSegmentId === segment.id
|
activeSegmentId === segment.id
|
||||||
? "bg-[var(--admin-accent-light)] border border-[var(--admin-accent)]"
|
? "bg-[var(--admin-accent-light)] border border-[var(--admin-accent)]"
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
|||||||
<td className="px-2 py-1.5">
|
<td className="px-2 py-1.5">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => removeStop(idx)}
|
onClick={() => removeStop(idx)}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-lg text-stone-400 hover:bg-red-50 hover:text-red-600 transition-colors"
|
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"
|
||||||
>
|
>
|
||||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function SideNavGroup({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="space-y-1" role="list" hidden={collapsed}>
|
<ul className="space-y-1" hidden={collapsed}>
|
||||||
{children}
|
{children}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -55,10 +55,8 @@ export function StopsDatePicker({ value }: StopsDatePickerProps) {
|
|||||||
if (!newDate) return;
|
if (!newDate) return;
|
||||||
router.push(`/admin/v2/stops?date=${newDate}`);
|
router.push(`/admin/v2/stops?date=${newDate}`);
|
||||||
}}
|
}}
|
||||||
|
className="bg-transparent border-0 outline-none focus-visible:outline-2 focus-visible:outline-[var(--color-accent)] focus-visible:outline-offset-2"
|
||||||
style={{
|
style={{
|
||||||
background: "transparent",
|
|
||||||
border: 0,
|
|
||||||
outline: "none",
|
|
||||||
fontSize: "15px",
|
fontSize: "15px",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: "var(--color-text)",
|
color: "var(--color-text)",
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ export default function HeroSection() {
|
|||||||
background: "linear-gradient(135deg, #1a4d2e 0%, #166534 100%)",
|
background: "linear-gradient(135deg, #1a4d2e 0%, #166534 100%)",
|
||||||
color: "white",
|
color: "white",
|
||||||
boxShadow: "0 8px 32px rgba(26, 77, 46, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.15)",
|
boxShadow: "0 8px 32px rgba(26, 77, 46, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.15)",
|
||||||
transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
|
transition: "transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.transform = "translateY(-3px)";
|
e.currentTarget.style.transform = "translateY(-3px)";
|
||||||
@@ -364,7 +364,7 @@ export default function HeroSection() {
|
|||||||
color: "#1a4d2e",
|
color: "#1a4d2e",
|
||||||
borderColor: "#1a4d2e",
|
borderColor: "#1a4d2e",
|
||||||
background: "rgba(26, 77, 46, 0.03)",
|
background: "rgba(26, 77, 46, 0.03)",
|
||||||
transition: "all 0.3s ease",
|
transition: "background 0.3s ease, color 0.3s ease",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = "#1a4d2e";
|
e.currentTarget.style.background = "#1a4d2e";
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ export default function TestimonialsAndCTA() {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
boxShadow: "0 8px 32px rgba(26, 77, 46, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.1)",
|
boxShadow: "0 8px 32px rgba(26, 77, 46, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.1)",
|
||||||
transition: "all 0.3s ease",
|
transition: "background 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease, border-color 0.3s ease",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = "rgba(45, 107, 79, 0.95)";
|
e.currentTarget.style.background = "rgba(45, 107, 79, 0.95)";
|
||||||
@@ -536,7 +536,7 @@ export default function TestimonialsAndCTA() {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5)",
|
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5)",
|
||||||
transition: "all 0.3s ease",
|
transition: "background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = "rgba(255, 255, 255, 0.9)";
|
e.currentTarget.style.background = "rgba(255, 255, 255, 0.9)";
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ function WaterFieldInner() {
|
|||||||
{t.selectHeadgate}
|
{t.selectHeadgate}
|
||||||
</label>
|
</label>
|
||||||
{isHeadgateLocked ? (
|
{isHeadgateLocked ? (
|
||||||
<div className="w-full rounded-xl border-2 border-amber-300 bg-amber-50 px-4 py-4 text-lg font-bold text-stone-900">
|
<div className="w-full rounded-xl border-2 border-amber-300 bg-amber-50 px-4 py-4 text-lg font-bold text-amber-900">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg className="w-5 h-5 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="w-5 h-5 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||||
|
|||||||
+14
-5
@@ -173,6 +173,15 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
|
|||||||
let filtered: unknown[] = [];
|
let filtered: unknown[] = [];
|
||||||
|
|
||||||
for (const filter of this.filters) {
|
for (const filter of this.filters) {
|
||||||
|
const inSet = filter.op === "in" && Array.isArray(filter.value)
|
||||||
|
? new Set(filter.value as unknown[])
|
||||||
|
: null;
|
||||||
|
const likeNeedle = filter.op === "like" && typeof filter.value === "string"
|
||||||
|
? (filter.value as string).replace(/%/g, "")
|
||||||
|
: null;
|
||||||
|
const ilikeNeedle = filter.op === "ilike" && typeof filter.value === "string"
|
||||||
|
? (filter.value as string).replace(/%/g, "").toLowerCase()
|
||||||
|
: null;
|
||||||
filtered = filtered.filter((row) => {
|
filtered = filtered.filter((row) => {
|
||||||
const r = row as Record<string, unknown>;
|
const r = row as Record<string, unknown>;
|
||||||
const rowValue = r[filter.column];
|
const rowValue = r[filter.column];
|
||||||
@@ -196,17 +205,17 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
|
|||||||
case "like":
|
case "like":
|
||||||
return (
|
return (
|
||||||
typeof rowValue === "string" &&
|
typeof rowValue === "string" &&
|
||||||
rowValue.includes((filter.value as string).replace(/%/g, ""))
|
likeNeedle !== null &&
|
||||||
|
rowValue.includes(likeNeedle)
|
||||||
);
|
);
|
||||||
case "ilike":
|
case "ilike":
|
||||||
return (
|
return (
|
||||||
typeof rowValue === "string" &&
|
typeof rowValue === "string" &&
|
||||||
rowValue
|
ilikeNeedle !== null &&
|
||||||
.toLowerCase()
|
rowValue.toLowerCase().includes(ilikeNeedle)
|
||||||
.includes((filter.value as string).replace(/%/g, "").toLowerCase())
|
|
||||||
);
|
);
|
||||||
case "in":
|
case "in":
|
||||||
return Array.isArray(filter.value) && filter.value.includes(rowValue);
|
return inSet !== null && inSet.has(rowValue);
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user