- Every major settings tab, from speech engine selection through
- advanced whisper.cpp tuning. Click any shot to expand it.
+ Every sidebar settings page, from speech model selection through
+ advanced whisper.cpp tuning. Click any shot to expand it — images
+ follow the site light/dark theme.
{settingsShots.map((shot, index) => (
diff --git a/web/src/components/theme-screenshot.tsx b/web/src/components/theme-screenshot.tsx
new file mode 100644
index 0000000..1e1873c
--- /dev/null
+++ b/web/src/components/theme-screenshot.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import Image, { type ImageProps } from "next/image";
+import React from "react";
+
+type CommonProps = {
+ src: string;
+ srcDark: string;
+ alt: string;
+ className?: string;
+};
+
+/**
+ * Renders light and dark screenshot variants. Visibility follows the
+ * `dark` class on `` from next-themes, so the active theme's image
+ * shows without a hydration flash.
+ *
+ * Default loading is "eager" for both siblings so the inactive theme is
+ * already fetched when the user toggles. Callers can still pass "lazy"
+ * for below-the-fold galleries (both siblings share the value).
+ */
+export function ThemeScreenshotImg({
+ src,
+ srcDark,
+ alt,
+ width,
+ height,
+ className = "",
+ loading = "eager",
+}: CommonProps & {
+ width: number;
+ height: number;
+ loading?: "lazy" | "eager";
+}) {
+ const shared = `${className}`.trim();
+ return (
+ <>
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+ >
+ );
+}
+
+type ThemeScreenshotNextProps = CommonProps &
+ Omit
& {
+ src: string;
+ srcDark: string;
+ alt: string;
+ };
+
+/**
+ * next/image twin for homepage embeds that already use Image.
+ *
+ * `priority` only applies to the light (default) image so Next preloads a
+ * single LCP candidate. The dark twin uses eager loading so theme toggles
+ * still have the asset ready without a second priority preload.
+ */
+export function ThemeScreenshotImage({
+ src,
+ srcDark,
+ alt,
+ className = "",
+ priority,
+ loading,
+ ...rest
+}: ThemeScreenshotNextProps) {
+ const shared = `${className}`.trim();
+ const darkLoading = priority ? "eager" : loading;
+ return (
+ <>
+
+
+ >
+ );
+}
+
+/** Derive `/screenshots/dark/` from a light `/screenshots/` path. */
+export function darkScreenshotPath(src: string): string {
+ if (src.includes("/dark/")) {
+ return src;
+ }
+ return src.replace("/screenshots/", "/screenshots/dark/");
+}