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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
52
components/firmware-preview.tsx
Normal file
52
components/firmware-preview.tsx
Normal 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
27
components/ui-preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user