fix(ui): wave 2 from critique re-run — AA contrast, unified PageHeader, checkout confirm
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m46s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m46s
Nav: teams/settings/services back out of the nav until product-ready (user decision); teams page was already half-finished (UUID member display is a backend limitation). Payment: explicit confirmation dialog (plan, amount, billing period, Stripe note) before any redirect; ?plan= URL now pre-opens the dialog instead of triggering a silent checkout. Contrast AA sweep (71 replacements): functional micro-labels raised to >=60-65% opacity and >=10px across sidebar, header, pricing, landing; decorative all-caps 'interface' label removed. Design unification: new PageHeader component (accent pill + serif base/accent title) applied to settings, services, reviews, teams; pricing header returned to the editorial voice (serif + accent pill); dead GlossaryCard deleted. i18n residuals: suggestion chips, 'Standard' provider label, notification close, model-combobox strings extracted (+broken bg-surface/border-border- subtle tokens fixed); ~45 new keys EN+FR; 13 dead keys removed; zero missing keys verified. Reviews: icon-only row actions now carry visible text; 'Approve all' is two-step armed-confirm. Accelerators: Ctrl/Cmd+Enter submits; arrow-key navigation in the language combobox; recent-jobs history cap 8->20 with per-job download; source=target config rejected; empty 'Master Quality' badge removed. Cookie consent: reopenable via footer link, emoji replaced with drawn icon. Verified: build exit 0, vitest 9/9, eslint 64 errors = previous level, 0 missing i18n keys.
This commit is contained in:
@@ -6,6 +6,13 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const STORAGE_KEY = "cookie-consent";
|
||||
const REOPEN_EVENT = "wordly:reopen-cookie-consent";
|
||||
|
||||
/** Reopen the banner after a choice was made (footer link uses this). */
|
||||
export function reopenCookieConsent() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
window.dispatchEvent(new Event(REOPEN_EVENT));
|
||||
}
|
||||
|
||||
export function CookieConsent() {
|
||||
const { t } = useI18n();
|
||||
@@ -13,6 +20,9 @@ export function CookieConsent() {
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(!localStorage.getItem(STORAGE_KEY));
|
||||
const onReopen = () => setVisible(true);
|
||||
window.addEventListener(REOPEN_EVENT, onReopen);
|
||||
return () => window.removeEventListener(REOPEN_EVENT, onReopen);
|
||||
}, []);
|
||||
|
||||
function acceptAll() {
|
||||
@@ -38,7 +48,7 @@ export function CookieConsent() {
|
||||
<div className="mx-auto max-w-3xl rounded-t-2xl border border-border bg-background/95 backdrop-blur-md shadow-2xl p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl shrink-0" aria-hidden="true">
|
||||
🍪
|
||||
<CookieIcon />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground mb-1">
|
||||
@@ -63,3 +73,16 @@ export function CookieConsent() {
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
/** Drawn icon instead of an emoji — keeps one consistent icon system. */
|
||||
function CookieIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-foreground/70" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<circle cx="9" cy="9" r="0.5" fill="currentColor" />
|
||||
<circle cx="14" cy="13" r="0.5" fill="currentColor" />
|
||||
<circle cx="10" cy="15" r="0.5" fill="currentColor" />
|
||||
<circle cx="16" cy="8" r="0.5" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { Search, List, Loader2, Check } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
export interface ModelOption {
|
||||
id: string;
|
||||
@@ -31,6 +32,7 @@ export function ModelCombobox({
|
||||
providerLabel,
|
||||
placeholder = "nom-du-modele",
|
||||
}: ModelComboboxProps) {
|
||||
const { t } = useI18n();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [filter, setFilter] = useState("");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -108,17 +110,17 @@ export function ModelCombobox({
|
||||
type="button"
|
||||
onClick={handleBrowse}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-1.5 px-3 h-10 rounded-lg border border-border-subtle bg-surface text-sm text-muted-foreground hover:bg-surface-elevated hover:text-foreground transition-colors disabled:opacity-50"
|
||||
className="flex items-center gap-1.5 px-3 h-10 rounded-lg border border-border bg-card text-sm text-muted-foreground hover:bg-card hover:text-foreground transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? <Loader2 className="size-4 animate-spin" /> : <List className="size-4" />}
|
||||
<span className="hidden sm:inline">Parcourir</span>
|
||||
<span className="hidden sm:inline">{t('common.browse')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border-subtle bg-card shadow-xl max-h-72 overflow-hidden flex flex-col">
|
||||
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border bg-card shadow-xl max-h-72 overflow-hidden flex flex-col">
|
||||
{/* Filter */}
|
||||
<div className="p-2 border-b border-border-subtle">
|
||||
<div className="p-2 border-b border-border">
|
||||
<Input
|
||||
placeholder={`Filtrer...`}
|
||||
value={filter}
|
||||
@@ -132,7 +134,7 @@ export function ModelCombobox({
|
||||
<ul ref={listRef} className="overflow-y-auto flex-1">
|
||||
{filtered.length === 0 ? (
|
||||
<li className="px-3 py-4 text-xs text-muted-foreground text-center">
|
||||
{models.length === 0 ? "Aucun modele recupere" : "Aucun modele ne correspond"}
|
||||
{models.length === 0 ? t('common.noModels') : t('common.noModelMatch')}
|
||||
</li>
|
||||
) : (
|
||||
filtered.map((model, i) => (
|
||||
@@ -141,7 +143,7 @@ export function ModelCombobox({
|
||||
onClick={() => handleSelect(model)}
|
||||
onMouseEnter={() => setHighlightIndex(i)}
|
||||
className={`flex items-center justify-between px-3 py-2 text-xs cursor-pointer transition-colors ${
|
||||
i === highlightIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-surface-elevated"
|
||||
i === highlightIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-card"
|
||||
} ${value === model.id ? "bg-primary/5" : ""}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -160,7 +162,7 @@ export function ModelCombobox({
|
||||
</ul>
|
||||
|
||||
{models.length > 0 && (
|
||||
<div className="px-3 py-1.5 border-t border-border-subtle text-xs text-muted-foreground text-center bg-card">
|
||||
<div className="px-3 py-1.5 border-t border-border text-xs text-muted-foreground text-center bg-card">
|
||||
{filtered.length} modele{filtered.length !== 1 ? "s" : ""}
|
||||
{filter ? ` sur ${models.length}` : ""} — nom libre possible
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Loader2 } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useI18n } from "@/lib/i18n"
|
||||
|
||||
const notificationVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pe-8 shadow-lg transition-all duration-300 ease-out",
|
||||
@@ -74,6 +75,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
||||
children,
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useI18n()
|
||||
const [visible, setVisible] = React.useState(true)
|
||||
const [progress, setProgress] = React.useState(100)
|
||||
|
||||
@@ -181,7 +183,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="absolute right-2 top-2 flex-shrink-0 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
aria-label="Close notification"
|
||||
aria-label={t("common.closeNotification")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user