Show on-device HTML UI mocks as firmware card previews.

Prefer media/preview.html over raster covers so catalog cards mirror each LVGL screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 22:10:05 +08:00
parent 8ae9501263
commit 937c0090d5
8 changed files with 108 additions and 73 deletions

View File

@@ -1,48 +1,10 @@
"use client";
import { Film, FileText, ImageIcon } from "lucide-react";
import { FileText, LayoutTemplate } from "lucide-react";
import type { FirmwareInfo } from "@/lib/firmware";
import { formatBytes, cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
function MediaSlot({ fw, active }: { fw: FirmwareInfo; active: boolean }) {
const src = fw.media.animation || fw.media.cover;
if (src) {
return (
// Dynamic firmware media URLs — next/image needs remotePatterns per host
// eslint-disable-next-line @next/next/no-img-element -- local API / arbitrary media
<img
src={src}
alt=""
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
/>
);
}
const initials = fw.name
.replace(/[^A-Za-z0-9\u4e00-\u9fff]+/g, " ")
.trim()
.split(/\s+/)
.slice(0, 2)
.map((w) => w[0]?.toUpperCase() ?? "")
.join("");
return (
<div
className={cn(
"flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_30%_20%,rgba(240,197,10,0.18),transparent_55%),linear-gradient(160deg,#1a1610,#0e0c09)]",
active && "from-[rgba(240,197,10,0.22)]"
)}
>
<span className="font-[family-name:var(--font-display)] text-3xl font-extrabold tracking-tight text-[var(--accent-hot)]/80">
{initials || "FW"}
</span>
<span className="text-[0.65rem] uppercase tracking-[0.14em] text-[var(--muted)]">
preview soon
</span>
</div>
);
}
import { FirmwarePreview } from "@/components/firmware-preview";
export function FirmwareCard({
fw,
@@ -53,9 +15,8 @@ export function FirmwareCard({
active: boolean;
onSelect: () => void;
}) {
const hasVideo = Boolean(fw.media.video);
const hasPreview = Boolean(fw.media.preview);
const hasDocs = Boolean(fw.media.docs);
const hasCover = Boolean(fw.media.cover || fw.media.animation);
return (
<button
@@ -71,16 +32,11 @@ export function FirmwareCard({
)}
>
<div className="relative aspect-[16/10] w-full overflow-hidden border-b border-[var(--line)]">
<MediaSlot fw={fw} active={active} />
<div className="absolute bottom-2 left-2 flex gap-1">
{hasCover && (
<FirmwarePreview fw={fw} />
<div className="pointer-events-none absolute bottom-2 left-2 flex gap-1">
{hasPreview && (
<span className="rounded bg-black/55 p-1 text-[var(--ink)] backdrop-blur-sm">
<ImageIcon className="size-3" aria-hidden />
</span>
)}
{hasVideo && (
<span className="rounded bg-black/55 p-1 text-[var(--ink)] backdrop-blur-sm">
<Film className="size-3" aria-hidden />
<LayoutTemplate className="size-3" aria-hidden />
</span>
)}
{hasDocs && (
@@ -91,7 +47,7 @@ export function FirmwareCard({
</div>
<Badge
variant="muted"
className="absolute bottom-2 right-2 bg-black/55 text-[0.65rem] backdrop-blur-sm"
className="pointer-events-none absolute bottom-2 right-2 bg-black/55 text-[0.65rem] backdrop-blur-sm"
>
{formatBytes(fw.size)}
</Badge>

View File

@@ -6,6 +6,7 @@ import type { FirmwareInfo } from "@/lib/firmware";
import { formatBytes } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { EspWebInstall } from "@/components/esp-web-install";
import { FirmwarePreview } from "@/components/firmware-preview";
export function FirmwareDetail({
fw,
@@ -24,8 +25,6 @@ export function FirmwareDetail({
);
}
const preview = fw.media.video || fw.media.animation || fw.media.cover;
return (
<aside className="sticky top-4 flex flex-col overflow-hidden rounded-[14px] border border-[var(--line)] bg-[color-mix(in_srgb,var(--bg-elev)_92%,transparent)] backdrop-blur-sm">
<div className="border-b border-[var(--line)] px-4 py-3">
@@ -37,32 +36,26 @@ export function FirmwareDetail({
</h3>
</div>
<div className="aspect-video w-full border-b border-[var(--line)] bg-black/40">
{fw.media.video ? (
<div className="aspect-[4/3] w-full border-b border-[var(--line)] bg-black/40">
{fw.media.preview ? (
<FirmwarePreview fw={fw} />
) : fw.media.video ? (
<video
key={fw.media.video}
className="h-full w-full object-cover"
controls
playsInline
preload="metadata"
poster={fw.media.cover || undefined}
>
<source src={fw.media.video} />
</video>
) : preview ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={preview}
alt=""
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full flex-col items-center justify-center gap-1 px-4 text-center">
<p className="m-0 text-sm text-[var(--muted)]">
{t("firmware.mediaPlaceholder")}
</p>
<p className="m-0 text-xs text-[var(--muted)] opacity-70">
meta.json → media.cover / animation / video
media/preview.html
</p>
</div>
)}

View File

@@ -0,0 +1,52 @@
"use client";
import type { FirmwareInfo } from "@/lib/firmware";
import { cn } from "@/lib/utils";
/** Renders HTML on-device UI mock (preferred) — never uses raster covers. */
export function FirmwarePreview({
fw,
className,
title,
}: {
fw: FirmwareInfo;
className?: string;
title?: string;
}) {
if (fw.media.preview) {
return (
<iframe
title={title || `${fw.name} UI preview`}
src={fw.media.preview}
className={cn("h-full w-full border-0 bg-[#0A1016]", className)}
loading="lazy"
sandbox="allow-scripts"
tabIndex={-1}
/>
);
}
const initials = fw.name
.replace(/[^A-Za-z0-9\u4e00-\u9fff]+/g, " ")
.trim()
.split(/\s+/)
.slice(0, 2)
.map((w) => w[0]?.toUpperCase() ?? "")
.join("");
return (
<div
className={cn(
"flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_30%_20%,rgba(240,197,10,0.18),transparent_55%),linear-gradient(160deg,#1a1610,#0e0c09)]",
className
)}
>
<span className="font-[family-name:var(--font-display)] text-3xl font-extrabold tracking-tight text-[var(--accent-hot)]/80">
{initials || "FW"}
</span>
<span className="text-[0.65rem] uppercase tracking-[0.14em] text-[var(--muted)]">
HTML preview soon
</span>
</div>
);
}

27
components/ui-preview.tsx Normal file
View File

@@ -0,0 +1,27 @@
"use client";
import { cn } from "@/lib/utils";
/** Scales an on-device HTML mock (320×240) into the card / detail media slot. */
export function UiPreview({
src,
className,
title,
}: {
src: string;
className?: string;
title?: string;
}) {
return (
<div className={cn("relative h-full w-full overflow-hidden bg-[#0a0806]", className)}>
<iframe
src={src}
title={title || "Firmware UI preview"}
className="pointer-events-none absolute inset-0 h-full w-full border-0"
loading="lazy"
sandbox="allow-scripts"
tabIndex={-1}
/>
</div>
);
}