Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"next": "^15.1.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"reframe": ".",
"tailwind-merge": "^3.6.0",
"wasm-feature-detect": "^1.8.0"
},
Expand Down
43 changes: 37 additions & 6 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
--accent: #3b82f6;
--accent-hover: #1d4ed8;
--accent-muted: rgba(59, 130, 246, 0.12);
--focus-ring: var(--accent);
--focus-ring-glow: rgba(59, 130, 246, 0.45);
--radius: 10px;
--shadow: 0 2px 12px rgba(15, 23, 42, 0.12);
--film-600: #e63946;
Expand All @@ -33,6 +35,8 @@
--accent: #4f6ef7;
--accent-hover: #3a57d4;
--accent-muted: rgba(79, 110, 247, 0.12);
--focus-ring: var(--accent);
--focus-ring-glow: rgba(79, 110, 247, 0.55);
--radius: 10px;
--shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
--warning: #fbbf24;
Expand All @@ -54,6 +58,8 @@
--accent: #FFFF00;
--accent-hover: #FFFF00;
--accent-muted: rgba(255, 255, 0, 0.22);
--focus-ring: var(--accent);
--focus-ring-glow: rgba(255, 255, 0, 0.55);
--radius: 10px;
--shadow: none;

Expand Down Expand Up @@ -125,8 +131,8 @@ textarea {
input:focus,
select:focus,
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-muted);
border-color: var(--focus-ring);
box-shadow: 0 0 0 4px var(--focus-ring-glow);
outline: none;
}

Expand All @@ -139,8 +145,33 @@ textarea:focus {
transition-duration: 200ms;
}

:focus-visible {
outline: 0;
box-shadow: 0 0 0 3px var(--accent-muted);
border-radius: var(--radius);
/*
* Keyboard focus — rules follow @tailwind utilities so they apply app-wide.
* Uses --focus-ring / --focus-ring-glow for light, dark, and high-contrast themes.
*/
button:focus-visible:not(:disabled),
a:focus-visible,
[role="button"]:focus-visible,
[role="slider"]:focus-visible,
[role="tab"]:focus-visible,
summary:focus-visible,
label:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--focus-ring-glow);
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 0;
border-color: var(--focus-ring);
box-shadow: 0 0 0 4px var(--focus-ring-glow);
}

input[type="range"]:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--focus-ring-glow);
}
18 changes: 7 additions & 11 deletions src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { cn, focusRing } from "@/lib/utils";
import { useTheme } from "./ThemeProvider";

export function ThemeToggle() {
Expand All @@ -11,17 +12,12 @@ export function ThemeToggle() {
type="button"
onClick={toggleTheme}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
className="
relative flex items-center justify-center
w-9 h-9 rounded-full
bg-[var(--surface)]
text-[var(--text)]
border border-[var(--border)]
hover:border-[var(--accent)] hover:bg-[var(--accent-muted)]
focus:outline-none focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2
focus:ring-offset-[var(--bg)]
transition-all duration-200
"
className={cn(
"relative flex items-center justify-center w-9 h-9 rounded-full",
"bg-[var(--surface)] text-[var(--text)] border border-[var(--border)]",
"hover:border-[var(--accent)] hover:bg-[var(--accent-muted)] transition-all duration-200",
focusRing
)}
>
{isDark ? (
<svg
Expand Down
7 changes: 7 additions & 0 deletions src/components/ThumbnailStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,13 @@ export default function ThumbnailStrip({
z-index: 2;
}

.thumb-btn:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--focus-ring-glow), var(--shadow);
z-index: 2;
}

.thumb-btn.active {
outline-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent), var(--shadow);
Expand Down
33 changes: 16 additions & 17 deletions src/components/components.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type ReactNode } from "react";
import { cn, focusRing, focusRingInput } from "@/lib/utils";

interface CardProps {
title: string;
Expand Down Expand Up @@ -63,16 +64,15 @@ export function Button({ variant = "primary", className = "", children, ...props

return (
<button
className={`
inline-flex items-center justify-center gap-2
rounded-[var(--radius)] px-4 py-2 text-sm font-medium
focus:outline-none focus:ring-2 focus:ring-offset-2
focus:ring-offset-[var(--bg)]
transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed
${variants[variant]}
${className}
`}
className={cn(
"inline-flex items-center justify-center gap-2",
"rounded-[var(--radius)] px-4 py-2 text-sm font-medium",
"transition-all duration-200",
"disabled:opacity-50 disabled:cursor-not-allowed",
focusRing,
variants[variant],
className
)}
{...props}
>
{children}
Expand All @@ -86,13 +86,12 @@ export function Button({ variant = "primary", className = "", children, ...props
export function Input({ className = "", ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
return (
<input
className={`
w-full rounded-[var(--radius)] border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm text-[var(--text)]
placeholder:text-[var(--muted)]
focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-muted)]
transition-all duration-200
${className}
`}
className={cn(
"w-full rounded-[var(--radius)] border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm text-[var(--text)]",
"placeholder:text-[var(--muted)] transition-all duration-200",
focusRingInput,
className
)}
{...props}
/>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/ui/BaseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { cn } from "@/lib/utils";
import { cn, focusRing } from "@/lib/utils";
import { ButtonHTMLAttributes, forwardRef } from "react";

interface BaseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
Expand Down Expand Up @@ -39,6 +39,7 @@ const BaseButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, BaseButtonP
className={cn(
"flex items-center justify-center gap-2 rounded-lg transition-all duration-200",
"hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100",
focusRing,
variants[variant],
sizes[size],
className
Expand Down
11 changes: 11 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

/**
* Visible keyboard focus ring — uses theme tokens from globals.css
* (--focus-ring, --focus-ring-glow). Apply to buttons, links, and custom controls.
*/
export const focusRing =
"outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)] focus-visible:shadow-[0_0_0_4px_var(--focus-ring-glow)]";

/** Focus ring for text fields and selects (slightly tighter offset). */
export const focusRingInput =
"outline-none focus-visible:border-[var(--focus-ring)] focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-[var(--focus-ring)] focus-visible:shadow-[0_0_0_4px_var(--focus-ring-glow)]";

export function formatBytes(bytes: number, decimals = 1) {
if (bytes === 0) return "0 Bytes";

Expand Down
Loading