fix(css): replace hardcoded rgba colors with color-mix(var(--token)) for light/dark parity

All error-red and success-green rgba values were using dark-mode hex values directly.
In light mode those tokens shift (error #f85149→#dc2626, success #3fb950→#16a34a),
so the hardcoded tints stayed wrong. Replaced with color-mix() so tints follow the token.

Also:
- Add missing --space-5 (1.25rem) to spacing scale in theme.css
- Add --color-accent (purple) token for csv_import badge; adapts dark/light
- Wire blocklist source badges to use --color-info/accent/success tokens
This commit is contained in:
pyr0ball 2026-04-13 20:56:07 -07:00
parent c93466c037
commit d5651e5fe8
5 changed files with 239 additions and 55 deletions

View file

@ -38,6 +38,7 @@
--color-error: #f85149;
--color-warning: #d29922;
--color-info: #58a6ff;
--color-accent: #a478ff; /* purple — csv import badge, secondary accent */
/* Typography */
--font-display: 'Fraunces', Georgia, serif;
@ -49,6 +50,7 @@
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
@ -109,6 +111,7 @@
--color-error: #dc2626;
--color-warning: #b45309;
--color-info: #2563eb;
--color-accent: #7c3aed; /* purple — deeper for contrast on cream */
/* Shadows — lighter, warm tint */
--shadow-sm: 0 1px 3px rgba(60, 45, 20, 0.12), 0 1px 2px rgba(60, 45, 20, 0.08);

View file

@ -140,11 +140,13 @@ import { ref, computed, onMounted } from 'vue'
const props = defineProps<{ currentView?: string }>()
const apiBase = (import.meta.env.VITE_API_BASE as string) ?? ''
// Probe once on mount hidden until confirmed enabled so button never flashes
const enabled = ref(false)
onMounted(async () => {
try {
const res = await fetch('/api/feedback/status')
const res = await fetch(`${apiBase}/api/feedback/status`)
if (res.ok) {
const data = await res.json()
enabled.value = data.enabled === true
@ -205,7 +207,7 @@ async function submit() {
loading.value = true
submitError.value = ''
try {
const res = await fetch('/api/feedback', {
const res = await fetch(`${apiBase}/api/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -237,18 +239,18 @@ async function submit() {
/* ── Floating action button ─────────────────────────────────────────── */
.feedback-fab {
position: fixed;
right: var(--spacing-md);
bottom: calc(68px + var(--spacing-md)); /* above mobile bottom nav */
right: var(--space-4);
bottom: calc(68px + var(--space-4)); /* above mobile bottom nav */
z-index: 190;
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: 9px var(--spacing-md);
background: var(--color-bg-elevated);
gap: var(--space-2);
padding: 9px var(--space-4);
background: var(--color-surface-raised);
border: 1px solid var(--color-border);
border-radius: 999px;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
font-size: 0.8125rem;
font-family: var(--font-body);
font-weight: 500;
cursor: pointer;
@ -256,9 +258,9 @@ async function submit() {
transition: background 0.15s, color 0.15s, box-shadow 0.15s, border-color 0.15s;
}
.feedback-fab:hover {
background: var(--color-bg-card);
color: var(--color-text-primary);
border-color: var(--color-border-focus);
background: var(--color-surface-2);
color: var(--color-text);
border-color: var(--app-primary);
box-shadow: var(--shadow-lg);
}
.feedback-fab-icon { width: 15px; height: 15px; flex-shrink: 0; }
@ -267,7 +269,7 @@ async function submit() {
/* On desktop, bottom nav is gone — drop to standard corner */
@media (min-width: 769px) {
.feedback-fab {
bottom: var(--spacing-lg);
bottom: var(--space-6);
}
}
@ -286,13 +288,13 @@ async function submit() {
@media (min-width: 500px) {
.feedback-overlay {
align-items: center;
padding: var(--spacing-md);
padding: var(--space-4);
}
}
/* ── Modal ────────────────────────────────────────────────────────────── */
.feedback-modal {
background: var(--color-bg-elevated);
background: var(--color-surface-raised);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
width: 100%;
@ -300,7 +302,7 @@ async function submit() {
overflow-y: auto;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-xl);
box-shadow: var(--shadow-lg);
}
@media (min-width: 500px) {
@ -316,13 +318,13 @@ async function submit() {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm);
padding: var(--space-4) var(--space-4) var(--space-3);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.feedback-title {
font-family: var(--font-display);
font-size: var(--font-size-lg);
font-size: 1.125rem;
font-weight: 600;
margin: 0;
}
@ -337,23 +339,23 @@ async function submit() {
align-items: center;
justify-content: center;
}
.feedback-close:hover { color: var(--color-text-primary); }
.feedback-close:hover { color: var(--color-text); }
.feedback-body {
padding: var(--spacing-md);
padding: var(--space-4);
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
gap: var(--space-4);
}
.feedback-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
@ -362,23 +364,23 @@ async function submit() {
resize: vertical;
min-height: 80px;
font-family: var(--font-body);
font-size: var(--font-size-sm);
font-size: 0.8125rem;
}
.form-required { color: var(--color-error); margin-left: 2px; }
.feedback-error {
color: var(--color-error);
font-size: var(--font-size-sm);
font-size: 0.8125rem;
margin: 0;
}
.feedback-success {
color: var(--color-success);
font-size: var(--font-size-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-success-bg);
border: 1px solid var(--color-success-border);
font-size: 0.8125rem;
padding: var(--space-3) var(--space-4);
background: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent);
border-radius: var(--radius-md);
}
.feedback-link { color: var(--color-success); font-weight: 600; text-decoration: underline; }
@ -387,15 +389,15 @@ async function submit() {
.feedback-summary {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-bg-secondary);
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-surface-2);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
}
.feedback-summary-row {
display: flex;
gap: var(--spacing-md);
gap: var(--space-4);
align-items: flex-start;
}
.feedback-summary-row > :first-child { min-width: 72px; flex-shrink: 0; }
@ -404,8 +406,115 @@ async function submit() {
word-break: break-word;
}
.mt-md { margin-top: var(--spacing-md); }
.mt-xs { margin-top: var(--spacing-xs); }
.mt-md { margin-top: var(--space-4); }
.mt-xs { margin-top: var(--space-2); }
/* ── Form elements ────────────────────────────────────────────────────── */
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.form-label {
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.form-input {
width: 100%;
padding: var(--space-2) var(--space-3);
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text);
font-family: var(--font-body);
font-size: 0.875rem;
line-height: 1.5;
transition: border-color 0.15s;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: var(--app-primary);
}
.form-input::placeholder { color: var(--color-text-muted); opacity: 0.7; }
/* ── Buttons ──────────────────────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-family: var(--font-body);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
white-space: nowrap;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary {
background: var(--app-primary);
color: #fff;
border: 1px solid var(--app-primary);
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
.btn-ghost {
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
}
.btn-ghost:hover:not(:disabled) {
background: var(--color-surface-2);
color: var(--color-text);
border-color: var(--app-primary);
}
/* ── Filter chips ─────────────────────────────────────────────────────── */
.filter-chip-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.btn-chip {
padding: 5px var(--space-3);
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: 999px;
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text-muted);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.btn-chip.active,
.btn-chip:hover {
background: color-mix(in srgb, var(--app-primary) 15%, transparent);
border-color: var(--app-primary);
color: var(--app-primary);
}
/* ── Card ─────────────────────────────────────────────────────────────── */
.card {
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
/* ── Text utilities ───────────────────────────────────────────────────── */
.text-muted { color: var(--color-text-muted); }
.text-sm { font-size: 0.8125rem; line-height: 1.5; }
.font-semibold { font-weight: 600; }
/* Transition */
.modal-fade-enter-active, .modal-fade-leave-active { transition: opacity 0.2s ease; }

View file

@ -5,10 +5,25 @@
'steal-card': isSteal,
'listing-card--auction': isAuction && hoursRemaining !== null && hoursRemaining > 1,
'listing-card--triple-red': tripleRed,
'listing-card--selected': selected,
}"
@click="selectMode ? $emit('toggle') : undefined"
>
<!-- Thumbnail -->
<div class="card__thumb">
<!-- Selection checkbox always in DOM; shown on hover or when in select mode -->
<button
v-show="selectMode || selected"
class="card__select-btn"
:class="{ 'card__select-btn--checked': selected }"
:aria-pressed="selected"
:aria-label="selected ? 'Deselect listing' : 'Select listing'"
@click.stop="$emit('toggle')"
>
<svg v-if="selected" viewBox="0 0 12 12" fill="currentColor" width="10" height="10">
<path d="M1.5 6L4.5 9L10.5 3" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<img
v-if="listing.photo_urls.length"
:src="listing.photo_urls[0]"
@ -25,9 +40,13 @@
<!-- Main info -->
<div class="card__body">
<!-- Title row -->
<a :href="listing.url" target="_blank" rel="noopener noreferrer" class="card__title">
{{ listing.title }}
</a>
<a
:href="listing.url"
target="_blank"
rel="noopener noreferrer"
class="card__title"
@click="selectMode && $event.preventDefault()"
>{{ listing.title }}</a>
<!-- Format + condition badges -->
<div class="card__badges">
@ -77,6 +96,7 @@
v-model="blockReason"
class="card__block-reason"
placeholder="Reason (optional)"
aria-label="Reason for blocking (optional)"
maxlength="200"
@keydown.enter="onBlock"
@keydown.esc="blockingOpen = false"
@ -96,6 +116,8 @@
class="card__trust"
:class="[trustClass, { 'card__trust--partial': trust?.score_is_partial }]"
:title="trustBadgeTitle"
:aria-label="trustBadgeTitle"
role="img"
>
<span class="card__trust-num">{{ trust?.composite_score ?? '?' }}</span>
<span class="card__trust-label">Trust</span>
@ -115,6 +137,7 @@
class="card__enrich-btn"
:class="{ 'card__enrich-btn--spinning': enriching, 'card__enrich-btn--error': enrichError }"
:title="enrichError ? 'Enrichment failed — try again' : 'Refresh score now'"
:aria-label="enrichError ? 'Enrichment failed, try again' : 'Refresh trust score'"
:disabled="enriching"
@click.stop="onEnrich"
>{{ enrichError ? '✗' : '↻' }}</button>
@ -124,12 +147,15 @@
class="card__block-btn"
:class="{ 'card__block-btn--active': isBlocked }"
:title="isBlocked ? 'Seller is blocked' : 'Block this seller'"
:aria-label="isBlocked ? `${seller.username} is blocked` : `Block seller ${seller.username}`"
:aria-pressed="isBlocked"
@click.stop="isBlocked ? null : (blockingOpen = !blockingOpen)"
></button>
</div>
<!-- Trust feedback: calm "looks right / wrong" signal buttons -->
<!-- Trust feedback: opt-in signal buttons (off by default, enabled in Settings) -->
<TrustFeedbackButtons
v-if="trustSignalEnabled"
:seller-id="`ebay::${listing.seller_platform_id}`"
:trust="trust"
/>
@ -159,14 +185,21 @@ import type { Listing, TrustScore, Seller } from '../stores/search'
import { useSearchStore } from '../stores/search'
import { useBlocklistStore } from '../stores/blocklist'
import TrustFeedbackButtons from './TrustFeedbackButtons.vue'
import { useTrustSignalPref } from '../composables/useTrustSignalPref'
const { enabled: trustSignalEnabled } = useTrustSignalPref()
const props = defineProps<{
listing: Listing
trust: TrustScore | null
seller: Seller | null
marketPrice: number | null
selected?: boolean
selectMode?: boolean
}>()
const emit = defineEmits<{ toggle: [] }>()
const store = useSearchStore()
const blocklist = useBlocklistStore()
const enriching = ref(false)
@ -365,17 +398,55 @@ const formattedMarket = computed(() => {
box-shadow: var(--shadow-md);
}
/* Selection */
.listing-card--selected {
border-color: var(--app-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-primary) 30%, transparent);
}
.listing-card:hover .card__select-btn {
display: flex !important; /* reveal on hover even when v-show hides it */
}
.card__select-btn {
position: absolute;
top: var(--space-2);
left: var(--space-2);
z-index: 5;
width: 20px;
height: 20px;
border-radius: var(--radius-sm);
border: 1.5px solid var(--color-border);
background: var(--color-surface-raised);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
color: #fff;
opacity: 0.7;
}
.card__select-btn:hover,
.card__select-btn--checked { opacity: 1; }
.card__select-btn:hover { border-color: var(--app-primary); }
.card__select-btn--checked {
background: var(--app-primary);
border-color: var(--app-primary);
}
/* Thumbnail */
.card__thumb {
width: 80px;
height: 80px;
border-radius: var(--radius-md);
overflow: hidden;
overflow: visible; /* allow checkbox to poke out */
flex-shrink: 0;
background: var(--color-surface-raised);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.card__img {
@ -442,9 +513,9 @@ const formattedMarket = computed(() => {
}
.card__flag-badge {
background: rgba(248, 81, 73, 0.15);
background: color-mix(in srgb, var(--color-error) 15%, transparent);
color: var(--color-error);
border: 1px solid rgba(248, 81, 73, 0.3);
border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent);
padding: 1px var(--space-2);
border-radius: var(--radius-sm);
font-size: 0.6875rem;
@ -563,6 +634,7 @@ const formattedMarket = computed(() => {
}
.listing-card:hover .card__block-btn { opacity: 0.5; }
.listing-card:hover .card__block-btn:hover { opacity: 1; color: var(--color-error); border-color: var(--color-error); }
.card__block-btn:focus-visible { opacity: 0.6; outline: 2px solid var(--app-primary); outline-offset: 2px; }
.card__block-btn--active { opacity: 1 !important; color: var(--color-error); border-color: var(--color-error); cursor: default; }
/* Block popover */
@ -688,7 +760,7 @@ const formattedMarket = computed(() => {
.listing-card--triple-red:hover {
animation: none;
border-color: var(--color-error);
box-shadow: 0 0 10px 2px rgba(248, 81, 73, 0.35);
box-shadow: 0 0 10px 2px color-mix(in srgb, var(--color-error) 35%, transparent);
}
.listing-card--triple-red:hover::after {
@ -698,12 +770,12 @@ const formattedMarket = computed(() => {
@keyframes triple-red-glow {
0%, 100% {
border-color: rgba(248, 81, 73, 0.5);
box-shadow: 0 0 5px 1px rgba(248, 81, 73, 0.2);
border-color: color-mix(in srgb, var(--color-error) 50%, transparent);
box-shadow: 0 0 5px 1px color-mix(in srgb, var(--color-error) 20%, transparent);
}
50% {
border-color: var(--color-error);
box-shadow: 0 0 14px 3px rgba(248, 81, 73, 0.45);
box-shadow: 0 0 14px 3px color-mix(in srgb, var(--color-error) 45%, transparent);
}
}

View file

@ -288,9 +288,9 @@ function formatDate(iso: string | null): string {
letter-spacing: 0.04em;
}
.bl-source-badge--manual { background: rgba(88, 166, 255, 0.15); color: var(--app-primary); }
.bl-source-badge--csv_import { background: rgba(164, 120, 255, 0.15); color: #a478ff; }
.bl-source-badge--community { background: rgba(63, 185, 80, 0.15); color: var(--trust-high); }
.bl-source-badge--manual { background: color-mix(in srgb, var(--color-info) 15%, transparent); color: var(--color-info); }
.bl-source-badge--csv_import { background: color-mix(in srgb, var(--color-accent) 15%, transparent); color: var(--color-accent); }
.bl-source-badge--community { background: color-mix(in srgb, var(--color-success) 15%, transparent); color: var(--color-success); }
.bl-remove-btn {
background: none;

View file

@ -908,7 +908,7 @@ async function onSearch() {
flex-shrink: 0;
transition: background 150ms ease;
}
.cancel-btn:hover { background: rgba(248, 81, 73, 0.1); }
.cancel-btn:hover { background: color-mix(in srgb, var(--color-error) 10%, transparent); }
.cancel-btn:focus-visible { outline: 2px solid var(--color-error); outline-offset: 2px; }
.save-bookmark-btn {
@ -1221,8 +1221,8 @@ async function onSearch() {
align-items: center;
gap: var(--space-3);
padding: var(--space-4);
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
background: color-mix(in srgb, var(--color-error) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent);
border-radius: var(--radius-md);
color: var(--color-error);
font-size: 0.9375rem;
@ -1269,8 +1269,8 @@ async function onSearch() {
display: flex;
gap: var(--space-3);
align-items: flex-start;
background: rgba(248, 81, 73, 0.08);
border: 1px solid rgba(248, 81, 73, 0.35);
background: color-mix(in srgb, var(--color-error) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--color-error) 35%, transparent);
border-radius: var(--radius-lg);
padding: var(--space-4) var(--space-5);
font-size: 0.9375rem;
@ -1556,7 +1556,7 @@ async function onSearch() {
color: var(--color-error);
}
.bulk-bar__btn--danger:hover:not(:disabled) {
background: rgba(248, 81, 73, 0.12);
background: color-mix(in srgb, var(--color-error) 12%, transparent);
}
.bulk-bar__btn--report {