fix: loosen glass dashboard workspace

This commit is contained in:
2026-05-20 00:51:41 +08:00
parent 7b63ade1b2
commit 7fcda19ba2
8 changed files with 323 additions and 202 deletions

View File

@@ -1388,6 +1388,100 @@
"message": "fix: enlarge result thumbnails",
"hash": "a13bd05",
"files_changed": 1
},
{
"ts": "2026-05-19T21:05:38+08:00",
"type": "commit",
"message": "chore: record enlarged thumbnail workflow",
"hash": "7b63ade",
"files_changed": 1
},
{
"ts": "2026-05-19T13:10:07Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T13:20:07Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T13:30:08Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T13:40:08Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T13:50:08Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T14:00:08Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T15:40:29Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T15:50:29Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T16:00:29Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T16:02:50Z",
"type": "session-end",
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-19T16:02:50Z",
"type": "session-end",
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交chore: record enlarged thumbnail workflow",
"files_changed": 1
},
{
"ts": "2026-05-20T00:24:31+08:00",
"type": "commit",
"message": "auto-save 2026-05-20 00:24 (~5)",
"hash": "e72ba50",
"files_changed": 5
},
{
"ts": "2026-05-20T00:29:58+08:00",
"type": "commit",
"message": "auto-save 2026-05-20 00:29 (~4)",
"hash": "92df778",
"files_changed": 4
},
{
"ts": "2026-05-20T00:51:41+08:00",
"type": "commit",
"message": "auto-save 2026-05-20 00:51 (~8)",
"hash": "3547987",
"files_changed": 8
}
]
}

View File

@@ -3,7 +3,7 @@
@tailwind utilities;
html, body {
background: #071108;
background: #0f0f10;
color: #FFFFFF;
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "ss01";
-webkit-font-smoothing: antialiased;
@@ -12,13 +12,17 @@ html, body {
body {
font-family: ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "PingFang SC", "Noto Sans SC", "Helvetica Neue", sans-serif;
background-color: #071108;
background-image: linear-gradient(180deg, rgba(22, 42, 16, 0.92), rgba(5, 12, 4, 1));
background-color: #0f0f10;
background-image:
radial-gradient(circle at 18% 18%, rgba(110, 99, 200, 0.16), transparent 28%),
radial-gradient(circle at 78% 28%, rgba(73, 199, 182, 0.14), transparent 28%),
radial-gradient(circle at 48% 76%, rgba(185, 194, 42, 0.14), transparent 34%),
linear-gradient(180deg, #171717 0%, #0f0f10 48%, #080809 100%);
background-attachment: fixed;
min-height: 100vh;
}
::selection { background: rgba(230, 245, 120, 0.28); color: #fff; }
::selection { background: rgba(185, 194, 42, 0.28); color: #fff; }
.login-oasis-loop {
position: fixed;
@@ -124,13 +128,13 @@ body {
.app-oasis .login-oasis-canvas {
z-index: 0;
opacity: 0.74;
filter: saturate(0.92) contrast(1.04);
opacity: 0.18;
filter: saturate(0.72) contrast(0.92) grayscale(0.34);
}
.app-oasis .login-oasis-loop {
opacity: 1;
filter: saturate(1.08) brightness(1.2);
opacity: 0.2;
filter: saturate(0.72) brightness(0.72);
}
.app-oasis-shade {
@@ -139,10 +143,13 @@ body {
z-index: 1;
pointer-events: none;
background:
linear-gradient(90deg, rgba(0, 0, 0, 0.42), rgba(9, 22, 7, 0.14) 42%, rgba(0, 0, 0, 0.42)),
linear-gradient(180deg, rgba(0, 0, 0, 0.28), rgba(0, 0, 0, 0.02) 36%, rgba(0, 0, 0, 0.54)),
linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px);
radial-gradient(circle at 21% 24%, rgba(110, 99, 200, 0.18), transparent 30%),
radial-gradient(circle at 84% 28%, rgba(73, 199, 182, 0.15), transparent 28%),
radial-gradient(circle at 58% 84%, rgba(185, 194, 42, 0.12), transparent 34%),
linear-gradient(90deg, rgba(0, 0, 0, 0.52), rgba(20, 20, 20, 0.26) 44%, rgba(0, 0, 0, 0.46)),
linear-gradient(180deg, rgba(0, 0, 0, 0.20), rgba(0, 0, 0, 0.02) 36%, rgba(0, 0, 0, 0.58)),
linear-gradient(90deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
background-size: auto, auto, 64px 64px, 64px 64px;
}
@@ -154,7 +161,7 @@ body {
z-index: 2;
height: 34vh;
pointer-events: none;
opacity: 0.46;
opacity: 0.08;
background:
linear-gradient(180deg, transparent, rgba(10, 25, 7, 0.62) 60%, rgba(0, 0, 0, 0.82)),
repeating-linear-gradient(103deg, transparent 0 13px, rgba(204, 230, 120, 0.18) 13px 14px, transparent 14px 28px),
@@ -168,7 +175,7 @@ body {
}
.btn-primary {
@apply active:scale-[0.98];
background: linear-gradient(135deg, rgba(230, 245, 120, 0.96), rgba(214, 179, 106, 0.94));
background: linear-gradient(135deg, rgba(217, 223, 42, 0.96), rgba(214, 179, 106, 0.94));
color: #081006;
box-shadow: 0 6px 24px -8px rgba(99, 102, 241, 0.6), inset 0 1px 0 rgba(255,255,255,0.18);
}
@@ -177,43 +184,43 @@ body {
}
.btn-ghost {
@apply text-white/80 active:scale-[0.98];
border: 1px solid rgba(140, 180, 120, 0.14);
background: rgba(10, 18, 10, 0.34);
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.055);
}
.btn-outline {
@apply text-white/80 active:scale-[0.98];
border: 1px solid rgba(140, 180, 120, 0.16);
background: rgba(10, 18, 10, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
}
.btn-glass {
@apply text-white backdrop-blur-xl active:scale-[0.98];
border: 1px solid rgba(140, 180, 120, 0.18);
background: rgba(10, 18, 10, 0.42);
border: 1px solid rgba(255, 255, 255, 0.13);
background: rgba(255, 255, 255, 0.07);
}
/* ===== Cards (glass) ===== */
.card {
@apply relative rounded-[8px] backdrop-blur-xl;
border: 1px solid rgba(140, 180, 120, 0.14);
border: 1px solid rgba(255, 255, 255, 0.10);
background:
linear-gradient(180deg, rgba(17, 28, 13, 0.62), rgba(5, 10, 5, 0.54)),
rgba(10, 18, 10, 0.42);
linear-gradient(135deg, rgba(255, 255, 255, 0.085), rgba(255, 255, 255, 0.035)),
rgba(42, 42, 42, 0.58);
box-shadow:
0 1px 0 0 rgba(255,255,255,0.08) inset,
0 22px 70px -30px rgba(0,0,0,0.82),
0 0 42px -34px rgba(230, 245, 120, 0.64);
0 0 42px -34px rgba(73, 199, 182, 0.64);
}
.card-2 {
@apply rounded-[8px] backdrop-blur-xl;
border: 1px solid rgba(140, 180, 120, 0.12);
background: rgba(8, 16, 8, 0.42);
border: 1px solid rgba(255, 255, 255, 0.10);
background: rgba(255, 255, 255, 0.055);
}
.card-hover {
@apply transition-all;
}
.card-hover:hover,
.card:hover {
border-color: rgba(140, 180, 120, 0.28);
border-color: rgba(255, 255, 255, 0.18);
}
/* ===== Chips ===== */
@@ -230,8 +237,8 @@ body {
}
.chip-neutral {
color: rgba(255, 255, 255, 0.7);
border-color: rgba(140, 180, 120, 0.14);
background: rgba(10, 18, 10, 0.34);
border-color: rgba(255, 255, 255, 0.11);
background: rgba(255, 255, 255, 0.055);
}
.chip-violet {
border-color: rgba(230, 245, 120, 0.28);
@@ -242,8 +249,8 @@ body {
/* ===== Segmented ===== */
.seg {
@apply inline-flex p-1 rounded-[8px] gap-1;
border: 1px solid rgba(140, 180, 120, 0.13);
background: rgba(10, 18, 10, 0.36);
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.055);
backdrop-filter: blur(16px);
}
.seg-item {
@@ -258,10 +265,12 @@ body {
/* ===== Tiles ===== */
.tile {
@apply relative aspect-square overflow-hidden rounded-[8px] transition-all cursor-pointer;
background: rgba(10, 18, 10, 0.44);
background:
linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.025)),
rgba(42, 42, 42, 0.62);
box-shadow: 0 1px 0 rgba(255,255,255,0.08) inset, 0 18px 50px -28px rgba(0,0,0,0.8);
--tw-ring-color: rgba(140, 180, 120, 0.14);
border: 1px solid rgba(140, 180, 120, 0.1);
--tw-ring-color: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.10);
}
.tile-selected {
position: relative;
@@ -294,8 +303,8 @@ input, textarea {
}
.field {
@apply w-full rounded-[8px] px-3.5 py-3 text-sm text-white placeholder:text-white/30 outline-none resize-none transition-colors;
background: rgba(10, 18, 10, 0.42);
border: 1px solid rgba(140, 180, 120, 0.14);
background: rgba(18, 18, 18, 0.56);
border: 1px solid rgba(255, 255, 255, 0.11);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.field:focus {
@@ -306,23 +315,23 @@ input, textarea {
/* ===== KBD ===== */
.kbd {
@apply inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-[6px] text-[10px] font-medium text-white/70;
border: 1px solid rgba(140, 180, 120, 0.16);
background: rgba(10, 18, 10, 0.46);
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}
/* ===== Section header ===== */
.section-eyebrow {
@apply inline-block text-[10px] font-semibold uppercase tracking-[0.18em];
color: rgba(230, 245, 120, 0.82);
text-shadow: 0 0 22px rgba(230, 245, 120, 0.16);
color: rgba(217, 223, 42, 0.82);
text-shadow: 0 0 22px rgba(217, 223, 42, 0.16);
}
.glass-sidebar {
border-right: 1px solid rgba(140, 180, 120, 0.14);
border-right: 1px solid rgba(255, 255, 255, 0.10);
background:
linear-gradient(180deg, rgba(8, 16, 7, 0.72), rgba(2, 5, 2, 0.62)),
rgba(10, 18, 10, 0.42);
linear-gradient(180deg, rgba(46, 46, 46, 0.78), rgba(22, 22, 22, 0.70)),
rgba(42, 42, 42, 0.54);
box-shadow:
inset -1px 0 0 rgba(255,255,255,0.04),
16px 0 70px -44px rgba(0,0,0,0.95);
@@ -330,7 +339,33 @@ input, textarea {
}
.app-oasis main {
background: linear-gradient(90deg, rgba(0,0,0,0.18), rgba(10,18,10,0.08));
background: linear-gradient(90deg, rgba(0,0,0,0.16), rgba(42,42,42,0.08));
}
.dashboard-workbench {
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 8px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025)),
rgba(32, 32, 32, 0.48);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
0 24px 90px -46px rgba(0, 0, 0, 0.92);
backdrop-filter: blur(18px);
}
.result-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
gap: 16px;
align-items: start;
}
@media (min-width: 1440px) {
.result-grid {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 18px;
}
}
/* ===== Login (cloned from SKG source) ===== */

View File

@@ -291,8 +291,8 @@ export default function Home() {
onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
onNew={() => setCurrent(null)}
/>
<main className={`flex-1 overflow-y-auto transition-[padding] duration-200 ${current && sidebarOpen ? 'pl-[340px]' : ''}`}>
<div className="mx-auto max-w-[1240px] px-10 py-8">
<main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-[1680px] px-5 py-6 sm:px-8 xl:px-10">
<header className="flex items-center justify-between mb-8 gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-[8px] bg-gradient-to-br from-[#e6f578] to-[#d6b36a] flex items-center justify-center shrink-0 shadow-glow-violet">
@@ -353,7 +353,7 @@ export default function Home() {
/>
)}
{current && (
<section className="space-y-5">
<section className="dashboard-workbench space-y-5 p-5">
<div className="flex items-end justify-between gap-4">
<div>
<span className="section-eyebrow">Step · 02 · Quick Screen</span>

View File

@@ -21,7 +21,8 @@ function nextPreviewState(event: PointerEvent<HTMLElement>, aspectRatio?: string
const gap = 18;
const margin = 12;
const ratio = parseRatio(aspectRatio);
const width = Math.min(620, Math.max(280, window.innerWidth * 0.42));
const maxWidth = ratio < 0.8 ? 380 : ratio > 1.35 ? 620 : 500;
const width = Math.min(maxWidth, Math.max(260, window.innerWidth * 0.38));
const height = Math.min(window.innerHeight * 0.82, width / ratio);
let left = event.clientX + gap;
let top = event.clientY + gap;
@@ -69,7 +70,7 @@ export function HoverImagePreview({
/>
{preview && (
<div
className="pointer-events-none fixed z-[90] rounded-[8px] bg-white p-2 shadow-2xl ring-1 ring-white/20"
className="pointer-events-none fixed z-[90] rounded-[8px] bg-white p-2 shadow-[0_24px_80px_-24px_rgba(0,0,0,0.86)] ring-1 ring-white/20"
style={{ left: preview.left, top: preview.top, width: preview.width }}
>
<img src={src} alt="" className="max-h-[82vh] w-full object-contain" style={{ aspectRatio: parseRatio(aspectRatio) }} />

View File

@@ -60,9 +60,12 @@ function AssetRow({ template, asset, accent, onRegenerate }: {
}
}
return (
<div className="grid grid-cols-[76px_1fr_auto] gap-3 p-3 rounded-[8px] bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
<div className="grid grid-cols-[96px_minmax(0,1fr)_minmax(132px,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">
{/* thumbnail */}
<div className="group/thumb relative w-[72px] h-[72px] rounded-[8px] overflow-visible bg-white/[0.04] ring-1 ring-white/[0.07] flex items-center justify-center">
<div
className="relative flex w-[92px] max-h-[110px] items-center justify-center overflow-visible rounded-[8px] bg-black/30 ring-1 ring-white/[0.08]"
style={{ aspectRatio: aspectCss(asset?.aspectRatio ?? template.aspectRatio) }}
>
{ready ? (
<HoverImagePreview
src={asset!.url}
@@ -81,7 +84,7 @@ function AssetRow({ template, asset, accent, onRegenerate }: {
</div>
{/* info */}
<div className="min-w-0 space-y-1">
<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 && (
@@ -109,45 +112,43 @@ function AssetRow({ template, asset, accent, onRegenerate }: {
anchor: {asset.anchorAssetId ?? asset.anchorImageUrl}
</div>
)}
{ready && onRegenerate && (
<div className="mt-2 pt-2 border-t border-amber-400/10 flex items-center justify-between gap-3">
<span className="text-[10px] text-white/35"></span>
<button
onClick={() => setShowRedo(value => !value)}
className="text-[10px] text-white/55 hover:text-white rounded-lg px-2 py-1 ring-1 ring-white/[0.08] bg-white/[0.04]"
>
{showRedo ? '收起' : '调整后生成'}
</button>
</div>
)}
{showRedo && ready && (
<div className="flex items-center gap-2 pt-1">
<input
value={refinement}
onChange={event => setRefinement(event.target.value)}
placeholder="重做要求"
className="min-w-0 flex-1 rounded-lg bg-black/30 ring-1 ring-white/[0.08] px-2 py-1 text-[11px] text-white/80 outline-none focus:ring-[#e6f578]/40"
/>
<button
onClick={handleRedo}
disabled={regenerating}
className="rounded-lg bg-white/[0.12] hover:bg-white/[0.18] text-white text-[10px] px-2 py-1 disabled:opacity-40"
>
{regenerating ? '...' : '确认生成'}
</button>
</div>
)}
</div>
{/* meta */}
<div className="flex flex-col items-end justify-between text-right shrink-0">
<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.5 py-1.5 text-[10px] text-white/60 ring-1 ring-white/[0.08] transition-colors hover:bg-white/[0.10] hover:text-white"
>
{showRedo ? '收起重做' : '重做'}
</button>
)}
</div>
</div>
{showRedo && ready && (
<div className="col-start-2 col-span-2 flex items-center gap-2 border-t border-amber-400/10 pt-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"
/>
<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"
>
{regenerating ? '...' : '确认重做'}
</button>
</div>
)}
</div>
);
}
@@ -366,7 +367,9 @@ function VideoSection({ videoLoading, primaryImage, onGenerateVideo }: {
return (
<div key={template.id} className="grid grid-cols-[72px_1fr_auto] gap-3 p-3 rounded-[8px] bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
<div className="aspect-square rounded-[8px] bg-gradient-to-br from-[#e6f578]/15 to-[#8cb478]/15 ring-1 ring-[#e6f578]/20 flex flex-col items-center justify-center text-[#e6f578] text-[9px] font-mono gap-1">
<span></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5.2v13.6L18.5 12 8 5.2z" />
</svg>
<span className="text-[8px]">{template.duration}s</span>
</div>
<div className="min-w-0 space-y-1">

View File

@@ -17,9 +17,9 @@ function cssRatioToNumber(ratio: string | undefined) {
function thumbnailWidth(ratio: string | undefined) {
const value = cssRatioToNumber(ratio);
if (value > 1.35) return 'min(100%, 660px)';
if (value < 0.8) return 'min(100%, 276px)';
return 'min(100%, 468px)';
if (value > 1.35) return 'min(100%, 560px)';
if (value < 0.8) return 'min(100%, 260px)';
return 'min(100%, 390px)';
}
export default function ResultGrid({ images, onAction }: ResultGridProps) {
@@ -42,12 +42,11 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
return () => window.removeEventListener('keydown', handler);
}, [images, onAction]);
const cols = images.length === 1 ? 'grid-cols-[minmax(220px,520px)]' : images.length <= 4 ? 'grid-cols-2' : images.length <= 9 ? 'grid-cols-3' : 'grid-cols-4';
const selectedCount = images.filter(i => i.status === 'selected').length;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2 text-[11px] text-white/55">
<kbd className="kbd">1</kbd>
<span className="text-white/40"></span>
@@ -66,11 +65,11 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
</div>
</div>
<div className={`grid ${cols} items-start gap-3`}>
<div className="result-grid">
{images.map((img, i) => (
<div
key={img.id}
className={`tile group ${img.status === 'selected' ? 'tile-selected' : ''} ${img.status === 'rejected' ? 'tile-rejected' : ''}`}
className={`tile group justify-self-center ${img.status === 'selected' ? 'tile-selected' : ''} ${img.status === 'rejected' ? 'tile-rejected' : ''}`}
style={{ aspectRatio: ratios[img.id] ?? '1 / 1', width: thumbnailWidth(ratios[img.id]), maxWidth: '100%' }}
>
<HoverImagePreview
@@ -102,8 +101,13 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
</button>
<button
onClick={() => onAction(img.id, img.status === 'rejected' ? 'reset' : 'reject')}
className="px-3 py-1.5 rounded-lg bg-white/[0.10] text-white/85 text-xs font-medium hover:bg-rose-500/60 hover:text-white transition-colors backdrop-blur"
></button>
className="flex h-8 w-10 items-center justify-center rounded-[8px] bg-white/[0.10] text-white/85 transition-colors hover:bg-rose-500/60 hover:text-white backdrop-blur"
title={img.status === 'rejected' ? '取消打叉' : '打叉'}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6">
<path d="M6 6l12 12M18 6L6 18" strokeLinecap="round" />
</svg>
</button>
</div>
</div>
))}

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import type { GenSession } from '@/lib/types';
function modeLabel(mode?: GenSession['inputMode']) {
@@ -25,9 +24,9 @@ function ActiveSessionDetail({ session }: { session: GenSession }) {
const packCount = session.packs?.length ?? 0;
return (
<div className="ml-3 mt-2 rounded-[8px] border border-[#e6f578]/24 bg-[#e6f578]/[0.055] p-3 shadow-[0_18px_52px_-38px_rgba(230,245,120,0.82)]">
<div className="rounded-[8px] border border-white/10 bg-white/[0.055] p-3 shadow-[0_18px_52px_-38px_rgba(230,245,120,0.68)] backdrop-blur-xl">
<div className="flex items-center justify-between gap-2">
<div className="text-[9px] uppercase tracking-[0.16em] text-[#e6f578]/65"></div>
<div className="text-[9px] uppercase tracking-[0.16em] text-[#b9c22a]/80">Current session</div>
<span className="rounded-full border border-white/10 bg-black/28 px-2 py-0.5 text-[9px] text-white/48">
{modeLabel(session.inputMode)}
</span>
@@ -57,7 +56,7 @@ function ActiveSessionDetail({ session }: { session: GenSession }) {
<div className="mb-1.5 text-[9px] uppercase tracking-[0.14em] text-white/34">Reference</div>
<div className="grid grid-cols-4 gap-1.5">
{refs.slice(0, 4).map((ref, index) => (
<div key={`${ref.url}-${index}`} className="relative aspect-square overflow-hidden rounded-[6px] bg-white ring-1 ring-white/10">
<div key={`${ref.url}-${index}`} className="relative aspect-square overflow-hidden rounded-[6px] bg-white/90 ring-1 ring-white/10">
<img src={ref.url} alt={ref.label} className="h-full w-full object-contain" />
</div>
))}
@@ -88,28 +87,8 @@ export default function Sidebar({
onPick: (id: string) => void;
onNew: () => void;
}) {
const rootRef = useRef<HTMLElement | null>(null);
const rowRefs = useRef<Record<string, HTMLDivElement | null>>({});
const [detailTop, setDetailTop] = useState(140);
const activeSession = sessions.find(session => session.id === currentId) ?? null;
function syncDetailTop() {
if (!currentId) return;
const row = rowRefs.current[currentId];
const root = rootRef.current;
if (!row || !root) return;
const rowRect = row.getBoundingClientRect();
const rootRect = root.getBoundingClientRect();
const rawTop = rowRect.top - rootRect.top;
const maxTop = Math.max(96, rootRect.height - 360);
setDetailTop(Math.min(Math.max(96, rawTop), maxTop));
}
useEffect(() => {
syncDetailTop();
}, [currentId, sessions.length, open]);
if (!open) {
return (
<aside className="glass-sidebar w-12 shrink-0 flex flex-col items-center py-4">
@@ -127,103 +106,108 @@ export default function Sidebar({
}
return (
<aside ref={rootRef} className="relative flex shrink-0 items-stretch overflow-visible">
<div className="glass-sidebar w-72 shrink-0 flex flex-col">
<div className="px-4 pt-5 pb-3 flex items-center gap-3">
<div className="w-8 h-8 rounded-[8px] bg-gradient-to-br from-[#e6f578] to-[#d6b36a] flex items-center justify-center shadow-glow-violet">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#081006" strokeWidth="2.5">
<path d="M12 2l2.5 6.5L21 11l-6.5 2.5L12 20l-2.5-6.5L3 11l6.5-2.5z" strokeLinejoin="round" />
</svg>
<aside className="glass-sidebar w-[304px] shrink-0 overflow-hidden">
<div className="flex h-full min-h-0">
<div className="flex w-[58px] shrink-0 flex-col items-center border-r border-white/[0.06] py-5">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-[#d9df2a] via-[#a7c83b] to-[#49c7b6] text-[11px] font-bold text-[#081006] shadow-[0_12px_34px_-18px_rgba(185,194,42,0.9)]">
Lo
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-semibold tracking-tight">Toy Patent</div>
<div className="text-[10px] text-white/40">AI </div>
<div className="mt-7 flex flex-col gap-4 text-white/32">
{['M4 6h16M4 12h16M4 18h16', 'M4 5h7v7H4zM13 5h7v7h-7zM4 14h7v5H4zM13 14h7v5h-7z', 'M5 7h14M7 7v12h10V7M9 7V5h6v2', 'M6 6h12v12H6z'].map((path, index) => (
<span key={index} className={`flex h-8 w-8 items-center justify-center rounded-[8px] ${index === 0 ? 'bg-white/[0.08] text-[#d9df2a]' : 'hover:bg-white/[0.06] hover:text-white/70'}`}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d={path} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
))}
</div>
<button
onClick={onToggle}
className="w-8 h-8 rounded-lg hover:bg-white/[0.08] flex items-center justify-center text-white/60 transition-colors"
title="收起侧栏"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
</div>
<div className="px-4 pb-4">
<button
onClick={onNew}
className="btn btn-primary w-full text-sm"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
</svg>
·
</button>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-center gap-3 px-4 pb-3 pt-5">
<div className="min-w-0 flex-1">
<div className="text-[13px] font-semibold tracking-tight">Toy Patent</div>
<div className="text-[10px] text-white/40">AI </div>
</div>
<button
onClick={onToggle}
className="flex h-8 w-8 items-center justify-center rounded-[8px] text-white/60 transition-colors hover:bg-white/[0.08]"
title="收起侧栏"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
</div>
<div className="px-5 pt-1 pb-2 text-[10px] font-semibold text-white/35 uppercase tracking-[0.18em]">
</div>
<div className="px-4 pb-4">
<button
onClick={onNew}
className="btn btn-primary w-full text-sm"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="flex-1 overflow-y-auto px-3 pb-4 space-y-1" onScroll={syncDetailTop}>
{sessions.length === 0 && (
<div className="px-3 py-12 text-center text-xs text-white/30">
{activeSession && (
<div className="px-3 pb-4">
<ActiveSessionDetail session={activeSession} />
</div>
)}
{sessions.map(s => {
const selectedCount = s.images.filter(i => i.status === 'selected').length;
const active = currentId === s.id;
return (
<div
key={s.id}
ref={node => { rowRefs.current[s.id] = node; }}
className="relative"
>
<button
onClick={() => onPick(s.id)}
className={`w-full text-left px-3 py-2.5 rounded-[8px] text-xs transition-all ${
active
? 'bg-gradient-to-r from-[#e6f578]/24 via-[#8cb478]/18 to-[#d6b36a]/16 ring-1 ring-[#e6f578]/35 text-white shadow-glow-violet'
: 'hover:bg-[#e6f578]/[0.06] text-white/75 ring-1 ring-transparent hover:ring-[#8cb478]/20'
}`}
>
<div className={`line-clamp-2 font-medium leading-relaxed ${active ? 'text-white' : 'text-white/85'}`}>
{s.characterSpec?.name || s.prompt}
</div>
<div className={`mt-1.5 flex justify-between items-center text-[10px] ${active ? 'text-white/65' : 'text-white/40'}`}>
<span>
{new Date(s.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
</span>
<span className="flex items-center gap-1.5">
<span>{s.images.length} </span>
{selectedCount > 0 && (
<span className={active ? 'text-[#e6f578] font-semibold' : 'text-[#d6b36a] font-semibold'}>
{selectedCount}
</span>
)}
</span>
</div>
</button>
</div>
);
})}
</div>
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
4560 · / data
<div className="px-5 pb-2 pt-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white/35">
</div>
<div className="flex-1 space-y-1 overflow-y-auto px-3 pb-4">
{sessions.length === 0 && (
<div className="px-3 py-12 text-center text-xs text-white/30">
</div>
)}
{sessions.map(s => {
const selectedCount = s.images.filter(i => i.status === 'selected').length;
const active = currentId === s.id;
return (
<div key={s.id} className="relative">
<button
onClick={() => onPick(s.id)}
className={`w-full rounded-[8px] px-3 py-2.5 text-left text-xs transition-all ${
active
? 'bg-gradient-to-r from-[#d9df2a]/22 via-[#6e63c8]/16 to-[#49c7b6]/14 text-white ring-1 ring-[#d9df2a]/35 shadow-[0_18px_48px_-32px_rgba(185,194,42,0.86)]'
: 'text-white/75 ring-1 ring-transparent hover:bg-white/[0.055] hover:text-white/90 hover:ring-white/10'
}`}
>
<div className={`line-clamp-2 font-medium leading-relaxed ${active ? 'text-white' : 'text-white/85'}`}>
{s.characterSpec?.name || s.prompt}
</div>
<div className={`mt-1.5 flex items-center justify-between text-[10px] ${active ? 'text-white/65' : 'text-white/40'}`}>
<span>
{new Date(s.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
</span>
<span className="flex items-center gap-1.5">
<span>{s.images.length} </span>
{selectedCount > 0 && (
<span className={active ? 'font-semibold text-[#d9df2a]' : 'font-semibold text-[#d6b36a]'}>
{selectedCount}
</span>
)}
</span>
</div>
</button>
</div>
);
})}
</div>
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
4560 · / data
</div>
</div>
</div>
{activeSession && (
<div
className="absolute left-full z-30 w-80 pl-3"
style={{ top: detailTop }}
>
<ActiveSessionDetail session={activeSession} />
</div>
)}
</aside>
);
}

File diff suppressed because one or more lines are too long