mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(a11y): focus indicators meet the 3:1 non-text contrast bar (#574)
Fixes #568. Adds a real --color-ring token (ink orange #A85518 light / #F0A766 dark) and sweeps all 57 focus-indicator occurrences onto it: soft opacity rings blended to 1.2-1.7:1, border-only indicators sat at 2.6-3.0:1, and four focus:ring-ring sites referenced a token that never existed. Sponsor button keeps pink via pink-700; range sliders and the file list gain their missing keyboard indicators; landing skip link and form borders hardened; the palette contrast guard pins the ring at 3:1 in both themes.
This commit is contained in:
@@ -45,7 +45,7 @@ function renderIcon(iconName: string): string {
|
||||
id="hero-tool-search"
|
||||
autocomplete="off"
|
||||
placeholder={t(locale, "home.hero.searchPlaceholder")}
|
||||
class="w-full rounded-2xl border border-border bg-surface py-4 pe-4 ps-12 text-base shadow-sm outline-none transition-all placeholder:text-muted focus:border-primary focus:shadow-md"
|
||||
class="w-full rounded-2xl border border-border bg-surface py-4 pe-4 ps-12 text-base shadow-sm outline-none transition-all placeholder:text-muted focus:border-primary-ink focus:shadow-md"
|
||||
aria-label={t(locale, "home.hero.searchPlaceholder")}
|
||||
aria-expanded="false"
|
||||
aria-controls="hero-search-results"
|
||||
|
||||
@@ -220,7 +220,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
||||
autocomplete="off"
|
||||
aria-label={t(locale, "home.toolGrid.searchLabel")}
|
||||
placeholder={t(locale, "home.toolGrid.searchPlaceholder")}
|
||||
class="h-13 w-full rounded-lg border border-border bg-surface py-3 ps-12 pe-12 text-base text-foreground outline-none transition-colors placeholder:text-muted focus:border-primary"
|
||||
class="h-13 w-full rounded-lg border border-border bg-surface py-3 ps-12 pe-12 text-base text-foreground outline-none transition-colors placeholder:text-muted focus:border-primary-ink"
|
||||
/>
|
||||
<button
|
||||
id="tool-command-clear"
|
||||
|
||||
@@ -103,22 +103,22 @@ const subjects = [
|
||||
>
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium">{t(locale, "contact.form.name")}</label>
|
||||
<input type="text" id="name" name="name" required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none" />
|
||||
<input type="text" id="name" name="name" required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary-ink focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium">{t(locale, "contact.form.email")}</label>
|
||||
<input type="email" id="email" name="email" required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none" />
|
||||
<input type="email" id="email" name="email" required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary-ink focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="company" class="block text-sm font-medium">{t(locale, "contact.form.company")}</label>
|
||||
<input type="text" id="company" name="company" class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none" />
|
||||
<input type="text" id="company" name="company" class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary-ink focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="subject" class="block text-sm font-medium">{t(locale, "contact.form.subject")}</label>
|
||||
<select id="subject" name="subject" required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none">
|
||||
<select id="subject" name="subject" required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary-ink focus:outline-none">
|
||||
<option value="" disabled selected>{t(locale, "contact.form.selectTopic")}</option>
|
||||
{subjects.map((s) => (
|
||||
<option value={s}>{s}</option>
|
||||
@@ -128,7 +128,7 @@ const subjects = [
|
||||
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium">{t(locale, "contact.form.message")}</label>
|
||||
<textarea id="message" name="message" rows={5} required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none resize-none"></textarea>
|
||||
<textarea id="message" name="message" rows={5} required class="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary-ink focus:outline-none resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full rounded-lg px-6 py-3 text-sm font-semibold">
|
||||
|
||||
@@ -163,7 +163,7 @@ const collectionJsonLd = {
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder={searchPlaceholder}
|
||||
class="w-full rounded-xl border border-border bg-surface py-2.5 pe-4 ps-11 text-sm outline-none transition-colors placeholder:text-muted focus:border-primary"
|
||||
class="w-full rounded-xl border border-border bg-surface py-2.5 pe-4 ps-11 text-sm outline-none transition-colors placeholder:text-muted focus:border-primary-ink"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
|
||||
@@ -351,6 +351,8 @@ code, pre, kbd {
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
outline: 2px solid var(--color-primary-ink-strong);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ─── STEP CONNECTOR (How It Works) ─── */
|
||||
|
||||
@@ -312,7 +312,7 @@ export function Dropzone({
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder={t.dropzone.urlPlaceholder}
|
||||
className="flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
className="flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-none"
|
||||
disabled={urlLoading}
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -183,7 +183,7 @@ export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalPr
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
className="w-full ps-8 pe-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground placeholder:text-muted-foreground"
|
||||
className="w-full ps-8 pe-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-ring text-foreground placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SearchBar({ value, onChange, placeholder }: SearchBarProps) {
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={resolvedPlaceholder}
|
||||
aria-label={resolvedPlaceholder}
|
||||
className="w-full ps-10 pe-4 py-2 rounded-lg border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full ps-10 pe-4 py-2 rounded-lg border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -135,7 +135,7 @@ export function UrlImportModal({ onClose, onImport }: UrlImportModalProps) {
|
||||
placeholder={
|
||||
"https://example.com/photo.jpg\nhttps://example.com/report.pdf\n- https://example.com/clip.mp4\n[My file](https://example.com/recording.mp3)"
|
||||
}
|
||||
className="w-full min-h-[120px] max-h-[240px] resize-y rounded-lg border border-border bg-muted px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
className="w-full min-h-[120px] max-h-[240px] resize-y rounded-lg border border-border bg-muted px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t.urlImport.placeholder}</p>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export function CanvasResizeDialog({ open, onClose }: { open: boolean; onClose:
|
||||
|
||||
const inputCn = cn(
|
||||
"h-8 w-full rounded border border-border bg-card px-2 text-sm text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -144,7 +144,7 @@ export function CanvasResizeDialog({ open, onClose }: { open: boolean; onClose:
|
||||
onChange={(e) => setFill(e.target.value)}
|
||||
className={cn(
|
||||
"h-7 w-20 rounded border border-border bg-card px-1.5 text-xs text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function ColorSwatch({
|
||||
"relative rounded transition-shadow shrink-0",
|
||||
SIZE_CLASSES[size],
|
||||
showBorder && "border border-border",
|
||||
active && "ring-2 ring-primary ring-offset-1 ring-offset-card",
|
||||
active && "ring-2 ring-primary-ink ring-offset-1 ring-offset-card",
|
||||
onClick && "cursor-pointer hover:ring-1 hover:ring-primary/50",
|
||||
!onClick && "cursor-default",
|
||||
className,
|
||||
|
||||
@@ -530,7 +530,7 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
||||
type="number"
|
||||
value={settings.width}
|
||||
onChange={(e) => handleWidthChange(Number.parseInt(e.target.value, 10) || 1)}
|
||||
className="w-full px-2 py-1 text-xs bg-muted rounded border border-border text-foreground outline-none focus:border-primary"
|
||||
className="w-full px-2 py-1 text-xs bg-muted rounded border border-border text-foreground outline-none focus:border-ring"
|
||||
min={1}
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">Width</span>
|
||||
@@ -553,7 +553,7 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
||||
type="number"
|
||||
value={settings.height}
|
||||
onChange={(e) => handleHeightChange(Number.parseInt(e.target.value, 10) || 1)}
|
||||
className="w-full px-2 py-1 text-xs bg-muted rounded border border-border text-foreground outline-none focus:border-primary"
|
||||
className="w-full px-2 py-1 text-xs bg-muted rounded border border-border text-foreground outline-none focus:border-ring"
|
||||
min={1}
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">Height</span>
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ImageResizeDialog({ open, onClose }: { open: boolean; onClose: (
|
||||
|
||||
const inputCn = cn(
|
||||
"h-8 w-full rounded border border-border bg-card px-2 text-sm text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function CropOptions() {
|
||||
|
||||
const inputCn = cn(
|
||||
"h-6 w-16 rounded border border-border bg-card px-1.5 text-xs text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -93,7 +93,7 @@ export function CropOptions() {
|
||||
onChange={handleAspectChange}
|
||||
className={cn(
|
||||
"h-6 rounded border border-border bg-card px-1.5 text-xs text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
)}
|
||||
>
|
||||
{ASPECT_RATIOS.map((r) => (
|
||||
|
||||
@@ -208,7 +208,7 @@ export function SelectionOptions() {
|
||||
onChange={(e) => setMagicWandTolerance(Number(e.target.value))}
|
||||
className={cn(
|
||||
"h-6 w-14 rounded border border-border bg-card px-1.5 text-xs text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ function NumericInput({
|
||||
step={step ?? 1}
|
||||
className={cn(
|
||||
"h-6 w-16 rounded border border-border bg-card px-1.5 text-xs text-foreground",
|
||||
"focus:border-primary focus:outline-none",
|
||||
"focus:border-ring focus:outline-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -128,7 +128,7 @@ function ColorInputFields({
|
||||
className={cn(
|
||||
"flex-1 px-1.5 py-0.5 text-xs font-mono rounded",
|
||||
"bg-muted border border-border text-foreground",
|
||||
"focus:outline-none focus:ring-1 focus:ring-primary",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
)}
|
||||
maxLength={7}
|
||||
spellCheck={false}
|
||||
@@ -160,7 +160,7 @@ function ColorInputFields({
|
||||
className={cn(
|
||||
"w-full px-1 py-0.5 text-xs font-mono rounded",
|
||||
"bg-muted border border-border text-foreground",
|
||||
"focus:outline-none focus:ring-1 focus:ring-primary",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
)}
|
||||
aria-label={`${ch.toUpperCase()} color channel`}
|
||||
data-testid={`color-${ch}-input`}
|
||||
@@ -193,7 +193,7 @@ function ColorInputFields({
|
||||
className={cn(
|
||||
"w-full px-1 py-0.5 text-xs font-mono rounded",
|
||||
"bg-muted border border-border text-foreground",
|
||||
"focus:outline-none focus:ring-1 focus:ring-primary",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
)}
|
||||
aria-label={`${ch.toUpperCase()} color channel`}
|
||||
data-testid={`color-${ch}-input`}
|
||||
@@ -362,7 +362,7 @@ export function ColorPanel() {
|
||||
onClick={() => setPickerTarget(pickerTarget === "bg" ? null : "bg")}
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 w-7 h-7 rounded border-2 transition-shadow",
|
||||
pickerTarget === "bg" ? "border-primary ring-1 ring-primary" : "border-border",
|
||||
pickerTarget === "bg" ? "border-primary ring-1 ring-primary-ink" : "border-border",
|
||||
)}
|
||||
style={{ backgroundColor: backgroundColor }}
|
||||
aria-label={t.a11y.backgroundColor}
|
||||
@@ -374,7 +374,7 @@ export function ColorPanel() {
|
||||
onClick={() => setPickerTarget(pickerTarget === "fg" ? null : "fg")}
|
||||
className={cn(
|
||||
"absolute left-0 top-0 w-7 h-7 rounded border-2 z-10 transition-shadow",
|
||||
pickerTarget === "fg" ? "border-primary ring-1 ring-primary" : "border-border",
|
||||
pickerTarget === "fg" ? "border-primary ring-1 ring-primary-ink" : "border-border",
|
||||
)}
|
||||
style={{ backgroundColor: foregroundColor }}
|
||||
aria-label={t.a11y.foregroundColor}
|
||||
@@ -425,7 +425,7 @@ export function ColorPanel() {
|
||||
className={cn(
|
||||
"w-full px-2 py-1 text-xs font-mono rounded",
|
||||
"bg-muted border border-border text-foreground",
|
||||
"focus:outline-none focus:ring-1 focus:ring-primary",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
)}
|
||||
maxLength={7}
|
||||
spellCheck={false}
|
||||
|
||||
@@ -273,7 +273,7 @@ function BlendModeSelect({ value, onChange }: { value: string; onChange: (mode:
|
||||
id="blend-mode-select"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 min-w-0 h-7 text-xs bg-muted border border-border rounded px-1.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
className="flex-1 min-w-0 h-7 text-xs bg-muted border border-border rounded px-1.5 text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
data-testid="blend-mode-select"
|
||||
>
|
||||
{BLEND_MODES.map((mode) => (
|
||||
@@ -542,7 +542,7 @@ function LayerRow({
|
||||
onBlur={commitRename}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="w-full text-xs bg-muted border border-border rounded px-1 py-0.5 text-foreground outline-none focus:ring-1 focus:ring-primary"
|
||||
className="w-full text-xs bg-muted border border-border rounded px-1 py-0.5 text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||
data-testid={`layer-name-input-${layer.id}`}
|
||||
/>
|
||||
) : (
|
||||
@@ -963,7 +963,7 @@ function StrokeControls({
|
||||
position: e.target.value as "inside" | "center" | "outside",
|
||||
})
|
||||
}
|
||||
className="flex-1 h-6 text-[10px] bg-muted border border-border rounded px-1 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
className="flex-1 h-6 text-[10px] bg-muted border border-border rounded px-1 text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="inside">Inside</option>
|
||||
<option value="center">Center</option>
|
||||
|
||||
@@ -96,7 +96,7 @@ export function FileList({ filterMimePrefix }: { filterMimePrefix?: string }) {
|
||||
placeholder={t.files.searchPlaceholder}
|
||||
value={inputValue}
|
||||
onChange={handleSearchChange}
|
||||
className="w-full ps-8 pe-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground placeholder:text-muted-foreground"
|
||||
className="w-full ps-8 pe-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-ring text-foreground placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,7 +142,7 @@ export function FileList({ filterMimePrefix }: { filterMimePrefix?: string }) {
|
||||
role="listbox"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleListKeyDown}
|
||||
className="flex-1 overflow-y-auto p-2 space-y-0.5 focus:outline-none"
|
||||
className="flex-1 overflow-y-auto p-2 space-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
|
||||
@@ -329,7 +329,7 @@ function SponsorButton({ isDark, compact = false }: { isDark: boolean; compact?:
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"ms-1 inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pink-300 focus-visible:ring-offset-1",
|
||||
"ms-1 inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 dark:focus-visible:ring-pink-300 focus-visible:ring-offset-1",
|
||||
isDark
|
||||
? "border-pink-800 bg-pink-950/30 text-pink-300 hover:bg-pink-950/50"
|
||||
: "border-pink-200 bg-pink-50 text-pink-700 hover:bg-pink-100",
|
||||
|
||||
@@ -760,7 +760,7 @@ export function BeautifyControls({
|
||||
onClick={() => applyPreset(preset)}
|
||||
className={`flex flex-col items-center gap-1 py-1.5 px-1 rounded transition-colors ${
|
||||
selectedPreset === preset.name
|
||||
? "bg-primary/10 ring-1 ring-primary"
|
||||
? "bg-primary/10 ring-1 ring-primary-ink"
|
||||
: "bg-muted hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -159,7 +159,7 @@ function ColorSwatches({
|
||||
onClick={() => onChange(swatch)}
|
||||
className={`h-6 w-6 rounded-full border-2 transition-all ${
|
||||
value.toUpperCase() === swatch
|
||||
? "ring-2 ring-primary ring-offset-1 border-transparent"
|
||||
? "ring-2 ring-primary-ink ring-offset-1 border-transparent"
|
||||
: "border-border hover:border-foreground/30"
|
||||
}`}
|
||||
style={{ backgroundColor: swatch }}
|
||||
|
||||
@@ -457,8 +457,8 @@ function CollageCell({
|
||||
aria-label={`Collage cell ${cellIndex + 1}`}
|
||||
className={cn(
|
||||
"relative overflow-hidden transition-shadow",
|
||||
isSelected && "ring-2 ring-primary ring-offset-1",
|
||||
isOver && !isSelected && "ring-2 ring-primary",
|
||||
isSelected && "ring-2 ring-primary-ink ring-offset-1",
|
||||
isOver && !isSelected && "ring-2 ring-primary-ink",
|
||||
!image && "border-2 border-dashed border-border/50",
|
||||
image && !isSelected && "cursor-grab",
|
||||
image && isSelected && "cursor-grab active:cursor-grabbing",
|
||||
|
||||
@@ -266,7 +266,7 @@ export function CollageSettings() {
|
||||
onClick={() => store.setBgPreset(p.id)}
|
||||
className={cn(
|
||||
"w-7 h-7 rounded-md transition-all",
|
||||
bgPreset === p.id && "ring-2 ring-primary ring-offset-1",
|
||||
bgPreset === p.id && "ring-2 ring-primary-ink ring-offset-1",
|
||||
p.border && "border border-border",
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -106,7 +106,7 @@ export function DuotoneSettings({ onImageStyle, onImageOverlay }: DuotoneSetting
|
||||
}}
|
||||
className={`flex flex-col items-center gap-1 py-1.5 px-1 rounded transition-colors ${
|
||||
shadow === p.shadow && highlight === p.highlight
|
||||
? "bg-primary/10 ring-1 ring-primary"
|
||||
? "bg-primary/10 ring-1 ring-primary-ink"
|
||||
: "bg-muted hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -124,7 +124,7 @@ function LabeledInput({
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full px-2.5 py-1.5 rounded-md border border-input bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
|
||||
className="w-full px-2.5 py-1.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
|
||||
/>
|
||||
{hint && <p className="text-[10px] text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
@@ -510,14 +510,14 @@ export function EditMetadataSettings() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setField("dateMode", "edit")}
|
||||
className={`flex-1 text-xs py-1.5 rounded-md border ${form.dateMode === "edit" ? "bg-primary text-primary-foreground border-primary" : "border-input text-foreground"}`}
|
||||
className={`flex-1 text-xs py-1.5 rounded-md border ${form.dateMode === "edit" ? "bg-primary text-primary-foreground border-primary" : "border-border text-foreground"}`}
|
||||
>
|
||||
Edit Dates
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setField("dateMode", "shift")}
|
||||
className={`flex-1 text-xs py-1.5 rounded-md border ${form.dateMode === "shift" ? "bg-primary text-primary-foreground border-primary" : "border-input text-foreground"}`}
|
||||
className={`flex-1 text-xs py-1.5 rounded-md border ${form.dateMode === "shift" ? "bg-primary text-primary-foreground border-primary" : "border-border text-foreground"}`}
|
||||
>
|
||||
Shift All Dates
|
||||
</button>
|
||||
@@ -558,7 +558,7 @@ export function EditMetadataSettings() {
|
||||
id="em-date-shift-direction"
|
||||
value={form.dateShiftDirection}
|
||||
onChange={(e) => setField("dateShiftDirection", e.target.value as "+" | "-")}
|
||||
className="px-2.5 py-1.5 rounded-md border border-input bg-background text-sm"
|
||||
className="px-2.5 py-1.5 rounded-md border border-border bg-background text-sm"
|
||||
>
|
||||
<option value="+">+ Forward</option>
|
||||
<option value="-">- Backward</option>
|
||||
@@ -685,12 +685,12 @@ export function EditMetadataSettings() {
|
||||
}
|
||||
}}
|
||||
placeholder="Add keyword and press Enter"
|
||||
className="flex-1 px-2.5 py-1.5 rounded-md border border-input bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="flex-1 px-2.5 py-1.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addKeyword}
|
||||
className="px-2 py-1.5 rounded-md border border-input hover:bg-muted/50"
|
||||
className="px-2 py-1.5 rounded-md border border-border hover:bg-muted/50"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -776,7 +776,7 @@ export function EditMetadataSettings() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadTemplate(t.name)}
|
||||
className="flex-1 text-start text-xs px-2 py-1 rounded-md border border-input hover:bg-muted/50 truncate"
|
||||
className="flex-1 text-start text-xs px-2 py-1 rounded-md border border-border hover:bg-muted/50 truncate"
|
||||
>
|
||||
{t.name}
|
||||
</button>
|
||||
@@ -797,13 +797,13 @@ export function EditMetadataSettings() {
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
placeholder="Template name"
|
||||
className="flex-1 px-2.5 py-1.5 rounded-md border border-input bg-background text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="flex-1 px-2.5 py-1.5 rounded-md border border-border bg-background text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveTemplate}
|
||||
disabled={!templateName.trim()}
|
||||
className="px-2 py-1.5 rounded-md border border-input hover:bg-muted/50 disabled:opacity-50"
|
||||
className="px-2 py-1.5 rounded-md border border-border hover:bg-muted/50 disabled:opacity-50"
|
||||
title="Save current values as template"
|
||||
>
|
||||
<BookmarkPlus className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -605,7 +605,7 @@ export function PassportPhotoSettings() {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search countries..."
|
||||
className="w-full ps-7 pe-2 py-1.5 rounded border border-border bg-background text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary"
|
||||
className="w-full ps-7 pe-2 py-1.5 rounded border border-border bg-background text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ export function RotateControls({
|
||||
commitAngleInput();
|
||||
}
|
||||
}}
|
||||
className="w-16 text-center text-sm font-mono font-medium tabular-nums py-1.5 rounded-md bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 pe-4"
|
||||
className="w-16 text-center text-sm font-mono font-medium tabular-nums py-1.5 rounded-md bg-background border border-border focus:outline-none focus:ring-2 focus:ring-ring pe-4"
|
||||
/>
|
||||
<span className="absolute right-2 text-sm font-mono text-muted-foreground pointer-events-none">
|
||||
°
|
||||
|
||||
@@ -196,7 +196,7 @@ export function SignaturePad({ onSave, onCancel }: SignaturePadProps) {
|
||||
type="button"
|
||||
aria-label={`ink ${c}`}
|
||||
onClick={() => setColor(c)}
|
||||
className={`h-5 w-5 rounded-full ${color === c ? "ring-2 ring-primary ring-offset-1" : ""}`}
|
||||
className={`h-5 w-5 rounded-full ${color === c ? "ring-2 ring-primary-ink ring-offset-1" : ""}`}
|
||||
style={{ background: c }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -157,7 +157,7 @@ export function ChangePasswordPage() {
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
placeholder={t.changePassword.currentPasswordPlaceholder}
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -185,7 +185,7 @@ export function ChangePasswordPage() {
|
||||
setShowGenerated(false);
|
||||
}}
|
||||
placeholder={t.changePassword.newPasswordPlaceholder}
|
||||
className={`w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 ${showGenerated ? "font-mono text-sm" : ""}`}
|
||||
className={`w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring ${showGenerated ? "font-mono text-sm" : ""}`}
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
@@ -207,7 +207,7 @@ export function ChangePasswordPage() {
|
||||
setShowGenerated(false);
|
||||
}}
|
||||
placeholder={t.changePassword.confirmPasswordPlaceholder}
|
||||
className={`w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 ${showGenerated ? "font-mono text-sm" : ""}`}
|
||||
className={`w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring ${showGenerated ? "font-mono text-sm" : ""}`}
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
|
||||
@@ -250,7 +250,7 @@ function HomeSearchBar({
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
className="w-full ps-11 pe-20 py-2.5 rounded-lg border border-border bg-card text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/40 transition-shadow"
|
||||
className="w-full ps-11 pe-20 py-2.5 rounded-lg border border-border bg-card text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-shadow"
|
||||
/>
|
||||
{value ? (
|
||||
<button
|
||||
|
||||
@@ -324,7 +324,7 @@ export function LoginPage() {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && mfaCode.length >= 6) handleMfaComplete();
|
||||
}}
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground text-center text-2xl font-mono tracking-[0.5em] focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground text-center text-2xl font-mono tracking-[0.5em] focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
<button
|
||||
@@ -369,7 +369,7 @@ export function LoginPage() {
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t.auth.enterUsername}
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -388,7 +388,7 @@ export function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t.auth.enterPassword}
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@ body {
|
||||
--color-primary-subtle: #FFF5ED;
|
||||
--color-primary-ink: #A85518;
|
||||
--color-primary-ink-strong: #8F4712;
|
||||
--color-ring: #A85518;
|
||||
--color-background: #FAFAF7;
|
||||
--color-foreground: #1A1814;
|
||||
--color-muted: #F3EEE6;
|
||||
@@ -66,6 +67,7 @@ body {
|
||||
--color-primary-ink-strong: #F09550;
|
||||
--color-destructive-ink: #D9635E;
|
||||
--color-success-ink: #2D9D78;
|
||||
--color-ring: #F0A766;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
@@ -95,6 +97,12 @@ input[type="range"]::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
/* The base rule strips the UA outline, so keyboard users need a replacement. */
|
||||
input[type="range"]:focus-visible {
|
||||
outline: 2px solid var(--color-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
@@ -127,6 +127,20 @@ describe("web dark theme", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("focus ring (issue #568)", () => {
|
||||
it("ring meets 3:1 non-text contrast (WCAG 1.4.11) on light surfaces", () => {
|
||||
for (const bg of ["background", "card", "muted", "primary-subtle", "sidebar"]) {
|
||||
expectPair(webLight, "ring", bg, 3);
|
||||
}
|
||||
});
|
||||
|
||||
it("ring meets 3:1 non-text contrast on dark surfaces", () => {
|
||||
for (const bg of ["background", "card", "muted", "sidebar"]) {
|
||||
expectPair(webDark, "ring", bg, 3);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("landing", () => {
|
||||
it("ink text passes AA on light surfaces", () => {
|
||||
for (const bg of ["background", "surface", "background-alt", "primary-subtle"]) {
|
||||
|
||||
Reference in New Issue
Block a user