From d5651e5fe8c40a14e2033a8c995331de20b4090d Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 13 Apr 2026 20:56:07 -0700 Subject: [PATCH] fix(css): replace hardcoded rgba colors with color-mix(var(--token)) for light/dark parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web/src/assets/theme.css | 3 + web/src/components/FeedbackButton.vue | 179 +++++++++++++++++++++----- web/src/components/ListingCard.vue | 94 ++++++++++++-- web/src/views/BlocklistView.vue | 6 +- web/src/views/SearchView.vue | 12 +- 5 files changed, 239 insertions(+), 55 deletions(-) diff --git a/web/src/assets/theme.css b/web/src/assets/theme.css index 1febc84..272c0d5 100644 --- a/web/src/assets/theme.css +++ b/web/src/assets/theme.css @@ -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); diff --git a/web/src/components/FeedbackButton.vue b/web/src/components/FeedbackButton.vue index d4c996c..3bb7734 100644 --- a/web/src/components/FeedbackButton.vue +++ b/web/src/components/FeedbackButton.vue @@ -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; } diff --git a/web/src/components/ListingCard.vue b/web/src/components/ListingCard.vue index 1244224..217d2bc 100644 --- a/web/src/components/ListingCard.vue +++ b/web/src/components/ListingCard.vue @@ -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" >
+ +
- - {{ listing.title }} - + {{ listing.title }}
@@ -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" > {{ trust?.composite_score ?? '?' }} Trust @@ -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 ? '✗' : '↻' }} @@ -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)" >⚑
- + @@ -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); } } diff --git a/web/src/views/BlocklistView.vue b/web/src/views/BlocklistView.vue index c952479..06a5bf6 100644 --- a/web/src/views/BlocklistView.vue +++ b/web/src/views/BlocklistView.vue @@ -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; diff --git a/web/src/views/SearchView.vue b/web/src/views/SearchView.vue index fa5ad5a..75af7f0 100644 --- a/web/src/views/SearchView.vue +++ b/web/src/views/SearchView.vue @@ -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 {