fix: show pack assets as horizontal detail strip
This commit is contained in:
@@ -784,6 +784,181 @@ input, textarea {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.asset-strip {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
padding-bottom: 4px;
|
||||
scroll-snap-type: x proximity;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.18) transparent;
|
||||
}
|
||||
|
||||
.asset-strip::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.asset-strip::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.16);
|
||||
}
|
||||
|
||||
.asset-tile {
|
||||
flex: 0 0 clamp(136px, 12vw, 172px);
|
||||
scroll-snap-align: start;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.035);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
transition: background 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.asset-tile:hover {
|
||||
background: rgba(255,255,255,0.065);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(230,245,120,0.22),
|
||||
0 18px 50px -34px rgba(230,245,120,0.55);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.asset-tile:focus-visible {
|
||||
outline: 2px solid rgba(230,245,120,0.45);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.asset-tile__thumb {
|
||||
position: relative;
|
||||
display: grid;
|
||||
height: 118px;
|
||||
width: 100%;
|
||||
place-items: center;
|
||||
overflow: visible;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.28);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.asset-tile__thumb > img {
|
||||
max-height: 118px;
|
||||
}
|
||||
|
||||
.asset-tile__state {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
display: grid;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
place-items: center;
|
||||
border-radius: 7px;
|
||||
background: rgba(0,0,0,0.62);
|
||||
color: rgba(255,255,255,0.62);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.10);
|
||||
}
|
||||
|
||||
.asset-tile__state--done {
|
||||
background: rgba(230,245,120,0.14);
|
||||
color: #e6f578;
|
||||
box-shadow: inset 0 0 0 1px rgba(230,245,120,0.28);
|
||||
}
|
||||
|
||||
.asset-tile__body {
|
||||
min-width: 0;
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.asset-detail-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 92;
|
||||
pointer-events: none;
|
||||
background: rgba(0,0,0,0.44);
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(6px);
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.asset-detail-backdrop.is-open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.asset-detail-drawer {
|
||||
position: fixed;
|
||||
z-index: 96;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
width: min(780px, calc(100vw - 96px));
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid rgba(255,255,255,0.14);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255,255,255,0.09), rgba(255,255,255,0.035)),
|
||||
rgba(20,20,21,0.94);
|
||||
box-shadow: -28px 0 90px -34px rgba(0,0,0,0.92);
|
||||
backdrop-filter: blur(22px);
|
||||
transform: translateX(100%);
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
.asset-detail-drawer.is-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.asset-detail-image {
|
||||
display: grid;
|
||||
max-height: min(54vh, 520px);
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255,255,255,0.98), rgba(238,242,230,0.94));
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.46);
|
||||
}
|
||||
|
||||
.asset-detail-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.asset-detail-block {
|
||||
margin-top: 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.22);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.asset-detail-label {
|
||||
margin-bottom: 8px;
|
||||
color: rgba(255,255,255,0.42);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.asset-detail-code,
|
||||
.asset-detail-pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255,255,255,0.68);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.asset-detail-pre {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.gallery-thumb-list {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
@@ -899,6 +1074,18 @@ input, textarea {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asset-tile {
|
||||
flex-basis: 142px;
|
||||
}
|
||||
|
||||
.asset-detail-drawer {
|
||||
width: calc(100vw - 28px);
|
||||
}
|
||||
|
||||
.asset-detail-image {
|
||||
max-height: 42vh;
|
||||
}
|
||||
|
||||
.session-workspace {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { AssetPack, AssetTemplate, GenImage, GenSession, PackKind, ToyAsset } from '@/lib/types';
|
||||
import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES, TEXT_TEMPLATES, VIDEO_TEMPLATES } from '@/lib/templates';
|
||||
import { HoverImagePreview } from './HoverImagePreview';
|
||||
@@ -34,122 +35,215 @@ function aspectCss(aspectRatio: AssetTemplate['aspectRatio'] | string | undefine
|
||||
return aspectRatio.replace(':', ' / ');
|
||||
}
|
||||
|
||||
/* ── Asset Row ────────────────────────────────── */
|
||||
function AssetRow({ template, asset, accent, onRegenerate }: {
|
||||
type AssetDetail = {
|
||||
template: AssetTemplate;
|
||||
asset: ToyAsset | undefined;
|
||||
accent: typeof PACK_ACCENT[PackKind];
|
||||
onRegenerate?: (assetId: string, userRefinement?: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function formatDate(timestamp?: number) {
|
||||
if (!timestamp) return '未生成';
|
||||
return new Date(timestamp).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function AssetTile({ template, asset, onOpen }: {
|
||||
template: AssetTemplate;
|
||||
asset: ToyAsset | undefined;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const [showRedo, setShowRedo] = useState(false);
|
||||
const ready = !!asset;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className={`asset-tile ${ready ? 'asset-tile--ready' : ''}`}
|
||||
title={ready ? `查看${template.title}详情` : `查看${template.title}要求`}
|
||||
>
|
||||
<div className="asset-tile__thumb" style={{ aspectRatio: aspectCss(asset?.aspectRatio ?? template.aspectRatio) }}>
|
||||
{ready ? (
|
||||
<HoverImagePreview
|
||||
src={asset!.url}
|
||||
alt={template.title}
|
||||
aspectRatio={asset!.aspectRatio}
|
||||
imageClassName="h-full w-full rounded-[8px] bg-white object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center rounded-[8px] bg-black/28">
|
||||
<span className="font-mono text-[10px] text-white/32">{template.aspectRatio}</span>
|
||||
</div>
|
||||
)}
|
||||
<span className={`asset-tile__state ${ready ? 'asset-tile__state--done' : ''}`}>
|
||||
{ready ? '✓' : '待'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="asset-tile__body">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="truncate text-[12px] font-semibold text-white">{template.title}</span>
|
||||
{template.required && <span className="shrink-0 text-[9px] text-[#e6f578]/75">必备</span>}
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-left text-[10px] leading-relaxed text-white/42">{template.description}</p>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<span className="font-mono text-[9px] text-white/34">{ASPECT_PX[template.aspectRatio]}</span>
|
||||
<span className={`font-mono text-[10px] ${ready ? 'text-[#dff5a8]' : 'text-white/28'}`}>
|
||||
{ready ? `L${asset!.derivationLevel}` : 'EMPTY'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function AssetDetailDrawer({ detail, onClose, onRegenerate }: {
|
||||
detail: AssetDetail | null;
|
||||
onClose: () => void;
|
||||
onRegenerate: (assetId: string, userRefinement?: string) => Promise<void>;
|
||||
}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [refinement, setRefinement] = useState('');
|
||||
const [regenerating, setRegenerating] = useState(false);
|
||||
const ready = !!asset;
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail) return;
|
||||
setRefinement('');
|
||||
function handleKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') onClose();
|
||||
}
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
}, [detail, onClose]);
|
||||
|
||||
async function handleRedo() {
|
||||
const asset = detail?.asset;
|
||||
if (!asset || !onRegenerate || regenerating) return;
|
||||
const ok = window.confirm('重新生成这 1 张会再次调用图片模型并产生费用。确认继续?');
|
||||
if (!ok) return;
|
||||
setRegenerating(true);
|
||||
try {
|
||||
await onRegenerate(asset.id, refinement);
|
||||
setShowRedo(false);
|
||||
setRefinement('');
|
||||
onClose();
|
||||
} finally {
|
||||
setRegenerating(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-[72px_minmax(0,1fr)_minmax(78px,auto)] gap-3 p-3 rounded-[8px] bg-white/[0.035] ring-1 ring-white/[0.06] hover:bg-white/[0.05] hover:ring-white/[0.12] transition-all 2xl:grid-cols-[84px_minmax(0,1fr)_minmax(104px,auto)]">
|
||||
{/* thumbnail */}
|
||||
<div
|
||||
className="relative flex w-[70px] max-h-[86px] items-center justify-center overflow-visible rounded-[8px] bg-black/30 ring-1 ring-white/[0.08] 2xl:w-[82px] 2xl:max-h-[98px]"
|
||||
style={{ aspectRatio: aspectCss(asset?.aspectRatio ?? template.aspectRatio) }}
|
||||
>
|
||||
{ready ? (
|
||||
<HoverImagePreview
|
||||
src={asset!.url}
|
||||
alt={template.title}
|
||||
aspectRatio={asset!.aspectRatio}
|
||||
imageClassName="max-w-full max-h-full object-contain rounded-lg bg-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-1 rounded-lg bg-black/25 px-2 py-1" style={{ aspectRatio: aspectCss(template.aspectRatio) }}>
|
||||
<span className="text-[9px] text-white/30 font-mono">{template.aspectRatio}</span>
|
||||
</div>
|
||||
)}
|
||||
{template.required && !ready && (
|
||||
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-[#e6f578] ring-2 ring-[#e6f578]/30" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* info */}
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[13px] font-medium text-white leading-snug">{template.title}</span>
|
||||
{template.required && !ready && (
|
||||
<span className="text-[9px] text-[#e6f578]/80 uppercase tracking-widest">必备</span>
|
||||
)}
|
||||
{ready && <span className="chip chip-live text-[10px] py-0">✓</span>}
|
||||
if (!mounted) return null;
|
||||
|
||||
const template = detail?.template;
|
||||
const asset = detail?.asset;
|
||||
const ready = !!asset;
|
||||
const drawer = (
|
||||
<>
|
||||
<div className={`asset-detail-backdrop ${detail ? 'is-open' : ''}`} onClick={onClose} />
|
||||
<aside className={`asset-detail-drawer ${detail ? 'is-open' : ''}`} aria-hidden={!detail}>
|
||||
{template && (
|
||||
<>
|
||||
<div className="flex shrink-0 items-start justify-between gap-4 border-b border-white/10 p-5">
|
||||
<div className="min-w-0">
|
||||
<span className="section-eyebrow">Asset Detail</span>
|
||||
<h2 className="mt-2 text-lg font-semibold text-white">{template.title}</h2>
|
||||
<p className="mt-1 text-xs leading-relaxed text-white/44">{template.description}</p>
|
||||
</div>
|
||||
<p className="text-[11px] text-white/45 leading-relaxed line-clamp-1">{template.description}</p>
|
||||
<button
|
||||
onClick={() => setShowPrompt(s => !s)}
|
||||
className="text-[10px] text-white/30 hover:text-[#e6f578] transition-colors flex items-center gap-1"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="grid h-10 w-10 shrink-0 place-items-center rounded-[8px] bg-white/[0.06] text-white/60 ring-1 ring-white/10 transition-colors hover:bg-white/[0.10] hover:text-white"
|
||||
title="关闭详情"
|
||||
aria-label="关闭详情"
|
||||
>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={showPrompt ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true">
|
||||
<path d="M6 6l12 12M18 6 6 18" strokeLinecap="round" />
|
||||
</svg>
|
||||
Prompt
|
||||
</button>
|
||||
{showPrompt && (
|
||||
<pre className="p-2 text-[10px] text-white/60 bg-black/40 rounded-lg ring-1 ring-white/[0.07] font-mono whitespace-pre-wrap break-all max-h-28 overflow-y-auto">
|
||||
{asset?.prompt ?? template.promptTemplate}
|
||||
</pre>
|
||||
)}
|
||||
{ready && asset.anchorImageUrl && (
|
||||
<div className="text-[10px] text-white/35 font-mono truncate">
|
||||
anchor: {asset.anchorAssetId ?? asset.anchorImageUrl}
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-5">
|
||||
<div className="asset-detail-image" style={{ aspectRatio: aspectCss(asset?.aspectRatio ?? template.aspectRatio) }}>
|
||||
{ready ? (
|
||||
<img src={asset.url} alt={template.title} />
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center text-sm text-white/36">待生成</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* meta */}
|
||||
<div className="flex shrink-0 flex-col items-end justify-between gap-3 text-right">
|
||||
<span className="text-[10px] font-mono text-white/40">{ASPECT_PX[template.aspectRatio]}</span>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className={`text-[10px] ${ready ? 'text-[#dff5a8]' : 'text-white/25'}`}>
|
||||
{ready ? `L${asset!.derivationLevel}` : '待生成'}
|
||||
</span>
|
||||
{ready && onRegenerate && (
|
||||
<button
|
||||
onClick={() => setShowRedo(value => !value)}
|
||||
className="mt-1 rounded-[8px] bg-white/[0.055] px-2 py-1.5 text-[10px] text-white/60 ring-1 ring-white/[0.08] transition-colors hover:bg-white/[0.10] hover:text-white 2xl:px-2.5"
|
||||
>
|
||||
{showRedo ? '收起重做' : '重做'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2 text-[11px]">
|
||||
<DetailItem label="状态" value={ready ? asset.status : '待生成'} />
|
||||
<DetailItem label="尺寸" value={ASPECT_PX[template.aspectRatio]} />
|
||||
<DetailItem label="比例" value={asset?.aspectRatio ?? template.aspectRatio} />
|
||||
<DetailItem label="层级" value={ready ? `L${asset.derivationLevel}` : '-'} />
|
||||
<DetailItem label="视图" value={asset?.view ?? template.view} />
|
||||
<DetailItem label="创建" value={formatDate(asset?.createdAt)} />
|
||||
<DetailItem label="模板" value={template.id} />
|
||||
<DetailItem label="版本" value={asset?.version ?? '-'} />
|
||||
</div>
|
||||
|
||||
{showRedo && ready && (
|
||||
<div className="col-start-2 col-span-2 flex items-center gap-2 border-t border-amber-400/10 pt-2">
|
||||
{(asset?.anchorAssetId || asset?.anchorImageUrl) && (
|
||||
<div className="asset-detail-block">
|
||||
<div className="asset-detail-label">Anchor</div>
|
||||
<div className="asset-detail-code">{asset.anchorAssetId ?? asset.anchorImageUrl}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="asset-detail-block">
|
||||
<div className="asset-detail-label">Prompt</div>
|
||||
<pre className="asset-detail-pre">{asset?.prompt ?? template.promptTemplate}</pre>
|
||||
</div>
|
||||
|
||||
{template.checklist.length > 0 && (
|
||||
<div className="asset-detail-block">
|
||||
<div className="asset-detail-label">检查点</div>
|
||||
<div className="space-y-1.5">
|
||||
{template.checklist.map(item => (
|
||||
<div key={item} className="flex gap-2 text-[11px] leading-relaxed text-white/62">
|
||||
<span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-[#e6f578]/70" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ready && (
|
||||
<div className="asset-detail-block">
|
||||
<div className="asset-detail-label">单张重做</div>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={refinement}
|
||||
onChange={event => setRefinement(event.target.value)}
|
||||
placeholder="重做要求"
|
||||
className="min-w-0 flex-1 rounded-[8px] bg-black/30 ring-1 ring-white/[0.08] px-2.5 py-2 text-[11px] text-white/80 outline-none focus:ring-[#e6f578]/40"
|
||||
placeholder="补充重做要求"
|
||||
className="min-w-0 flex-1 rounded-[8px] bg-black/30 px-3 py-2 text-[12px] text-white/82 outline-none ring-1 ring-white/[0.09] focus:ring-[#e6f578]/40"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRedo}
|
||||
disabled={regenerating}
|
||||
className="rounded-[8px] bg-[#d6b36a]/18 px-3 py-2 text-[10px] text-[#f2d38c] ring-1 ring-[#d6b36a]/24 transition-colors hover:bg-[#d6b36a]/26 disabled:opacity-40"
|
||||
className="rounded-[8px] bg-[#d6b36a]/18 px-4 py-2 text-[12px] font-semibold text-[#f2d38c] ring-1 ring-[#d6b36a]/24 transition-colors hover:bg-[#d6b36a]/26 disabled:opacity-40"
|
||||
>
|
||||
{regenerating ? '...' : '确认重做'}
|
||||
{regenerating ? '...' : '确认'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
return createPortal(drawer, document.body);
|
||||
}
|
||||
|
||||
function DetailItem({ label, value }: { label: string; value: string | number }) {
|
||||
return (
|
||||
<div className="rounded-[8px] bg-white/[0.035] p-3 ring-1 ring-white/[0.07]">
|
||||
<div className="text-[10px] text-white/34">{label}</div>
|
||||
<div className="mt-1 truncate text-[12px] text-white/76">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,6 +257,7 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs
|
||||
onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise<void>;
|
||||
}) {
|
||||
const [open, setOpen] = useState(kind === 'patent');
|
||||
const [detail, setDetail] = useState<AssetDetail | null>(null);
|
||||
const accent = PACK_ACCENT[kind];
|
||||
const templates = PACK_TEMPLATES[kind];
|
||||
const generatedCount = pack?.assets.length ?? 0;
|
||||
@@ -203,17 +298,23 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs
|
||||
|
||||
{/* asset list — collapsible */}
|
||||
{open && (
|
||||
<div className="px-4 pb-4 space-y-2 border-t border-white/[0.05]">
|
||||
<div className="pt-3 space-y-2">
|
||||
<div className="border-t border-white/[0.05] px-4 pb-4">
|
||||
<div className="asset-strip pt-3">
|
||||
{templates.map(template => {
|
||||
const asset = pack?.assets.find(a => a.templateId === template.id);
|
||||
return (
|
||||
<AssetRow key={template.id} template={template} asset={asset} accent={accent} onRegenerate={onRegenerateAsset} />
|
||||
<AssetTile
|
||||
key={template.id}
|
||||
template={template}
|
||||
asset={asset}
|
||||
onOpen={() => setDetail({ template, asset })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AssetDetailDrawer detail={detail} onClose={() => setDetail(null)} onRegenerate={onRegenerateAsset} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user