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:
SnapOtter
2026-07-19 16:23:15 +08:00
committed by GitHub
parent 51022628dc
commit 6339370093
35 changed files with 83 additions and 59 deletions
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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">
+2
View File
@@ -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) ─── */
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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">
+1 -1
View File
@@ -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 }}
/>
))}
+3 -3
View File
@@ -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}
/>
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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>
+8
View File
@@ -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;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

+14
View File
@@ -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"]) {