fix: polish layout and app branding

This commit is contained in:
2026-04-27 11:15:53 +08:00
parent 8254994df8
commit 20bd25e136
7 changed files with 311 additions and 205 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -54,7 +54,9 @@ pub async fn generate_image(
let model = input let model = input
.model .model
.clone() .clone()
.filter(|model| !model.trim().is_empty())
.or_else(|| provider.image_model.clone()) .or_else(|| provider.image_model.clone())
.filter(|model| !model.trim().is_empty())
.ok_or_else(|| AppError::Provider("image model is empty".to_string()))?; .ok_or_else(|| AppError::Provider("image model is empty".to_string()))?;
let task = repository::create_generation_task( let task = repository::create_generation_task(

View File

@@ -13,23 +13,23 @@
"windows": [ "windows": [
{ {
"title": "Image Draw AI", "title": "Image Draw AI",
"width": 1100, "width": 1280,
"height": 760, "height": 1160,
"minWidth": 900, "minWidth": 1180,
"minHeight": 620 "minHeight": 1100
} }
], ],
"security": { "security": {
"csp": null, "csp": null,
"assetProtocol": { "assetProtocol": {
"enable": true, "enable": true,
"scope": ["$APPDATA/**"] "scope": ["$APPDATA/**", "$HOME/**", "$PICTURE/**", "$DOWNLOAD/**", "$DESKTOP/**", "$DOCUMENT/**"]
} }
} }
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"icon": [] "icon": ["icons/icon.png"]
} }
} }

1
src/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { convertFileSrc, invoke } from '@tauri-apps/api/core'; import { convertFileSrc, invoke } from '@tauri-apps/api/core';
import appLogo from './assets/logo.svg';
import './styles.css'; import './styles.css';
type ProviderConfig = { type ProviderConfig = {
@@ -20,8 +21,8 @@ type ProviderForm = {
kind: string; kind: string;
base_url: string; base_url: string;
api_key: string; api_key: string;
text_model: string; text_model?: string | null;
image_model: string; image_model?: string | null;
enabled: boolean; enabled: boolean;
}; };
@@ -60,8 +61,8 @@ const defaultProviderForm: ProviderForm = {
kind: 'openai-compatible', kind: 'openai-compatible',
base_url: 'https://api.openai.com/v1', base_url: 'https://api.openai.com/v1',
api_key: '', api_key: '',
text_model: 'gpt-5', text_model: null,
image_model: 'gpt-image-2', image_model: null,
enabled: true, enabled: true,
}; };
@@ -73,10 +74,15 @@ const initialGenerationSteps: GenerationStep[] = [
{ label: '更新结果列表', status: 'pending' }, { label: '更新结果列表', status: 'pending' },
]; ];
const imageModelOptions = ['gpt-image-1', 'gpt-image-1.5', 'gpt-image-2'];
const imageSizeOptions = ['1024x1024', '1024x1536', '1536x1024'];
const imageQualityOptions = ['auto', 'high', 'medium', 'low'];
function App() { function App() {
const [providers, setProviders] = useState<ProviderConfig[]>([]); const [providers, setProviders] = useState<ProviderConfig[]>([]);
const [providerForm, setProviderForm] = useState<ProviderForm>(defaultProviderForm); const [providerForm, setProviderForm] = useState<ProviderForm>(defaultProviderForm);
const [prompt, setPrompt] = useState('一只赛博朋克风格的橘猫坐在霓虹灯下'); const [prompt, setPrompt] = useState('一只赛博朋克风格的橘猫坐在霓虹灯下');
const [selectedImageModel, setSelectedImageModel] = useState('gpt-image-2');
const [imageSize, setImageSize] = useState('1024x1024'); const [imageSize, setImageSize] = useState('1024x1024');
const [imageQuality, setImageQuality] = useState('auto'); const [imageQuality, setImageQuality] = useState('auto');
const [status, setStatus] = useState('准备就绪'); const [status, setStatus] = useState('准备就绪');
@@ -119,8 +125,8 @@ function App() {
kind: current.kind, kind: current.kind,
base_url: current.base_url, base_url: current.base_url,
api_key: current.api_key ?? form.api_key, api_key: current.api_key ?? form.api_key,
text_model: current.text_model ?? defaultProviderForm.text_model, text_model: current.text_model ?? null,
image_model: current.image_model ?? defaultProviderForm.image_model, image_model: null,
enabled: current.enabled, enabled: current.enabled,
})); }));
} }
@@ -130,7 +136,7 @@ function App() {
setIsBusy(true); setIsBusy(true);
setStatus('正在保存配置...'); setStatus('正在保存配置...');
try { try {
await invoke('upsert_provider', { input: providerForm }); await invoke('upsert_provider', { input: { ...providerForm, image_model: null } });
await refreshProviders(); await refreshProviders();
setStatus('配置已保存'); setStatus('配置已保存');
} catch (error) { } catch (error) {
@@ -165,8 +171,8 @@ function App() {
kind: provider.kind, kind: provider.kind,
base_url: provider.base_url, base_url: provider.base_url,
api_key: provider.api_key ?? form.api_key, api_key: provider.api_key ?? form.api_key,
text_model: provider.text_model ?? defaultProviderForm.text_model, text_model: provider.text_model ?? null,
image_model: provider.image_model ?? defaultProviderForm.image_model, image_model: null,
enabled: provider.enabled, enabled: provider.enabled,
})); }));
setStatus('已切换模型配置'); setStatus('已切换模型配置');
@@ -178,7 +184,7 @@ function App() {
setStatus('正在生成图片...'); setStatus('正在生成图片...');
try { try {
startStep(0); startStep(0);
await invoke('upsert_provider', { input: providerForm }); await invoke('upsert_provider', { input: { ...providerForm, image_model: null } });
startStep(1); startStep(1);
await new Promise((resolve) => window.setTimeout(resolve, 120)); await new Promise((resolve) => window.setTimeout(resolve, 120));
startStep(2); startStep(2);
@@ -186,7 +192,7 @@ function App() {
input: { input: {
provider_id: providerForm.id, provider_id: providerForm.id,
prompt, prompt,
model: providerForm.image_model, model: selectedImageModel,
size: imageSize, size: imageSize,
quality: imageQuality, quality: imageQuality,
image_paths: materialPaths, image_paths: materialPaths,
@@ -260,7 +266,7 @@ function App() {
<main className="app-shell"> <main className="app-shell">
<header className="topbar"> <header className="topbar">
<div className="brand"> <div className="brand">
<div className="brand-mark"></div> <img className="brand-mark" src={appLogo} alt="Image Draw AI" />
<div> <div>
<h1>Image Draw AI</h1> <h1>Image Draw AI</h1>
<p></p> <p></p>
@@ -269,7 +275,7 @@ function App() {
<div className="topbar-actions"> <div className="topbar-actions">
<div className="current-provider"> <div className="current-provider">
<span></span> <span></span>
<strong>{providerForm.image_model}</strong> <strong>{selectedImageModel}</strong>
</div> </div>
<button className="ghost" onClick={() => setIsSettingsOpen(true)}></button> <button className="ghost" onClick={() => setIsSettingsOpen(true)}></button>
</div> </div>
@@ -288,25 +294,30 @@ function App() {
</label> </label>
<div className="material-panel"> <div className="material-panel">
<div className="material-toolbar"> <div className="material-header">
<button onClick={pickMaterialImages} disabled={isBusy}></button> <div>
{materialPaths.length > 0 && ( <strong></strong>
<button className="ghost" onClick={() => setMaterialPaths([])} disabled={isBusy}></button> <span>{materialPaths.length > 0 ? `${materialPaths.length} 张,图像编辑模式` : '可选,支持多张'}</span>
)}
<span>{materialPaths.length > 0 ? `${materialPaths.length} 张素材` : '未导入素材'}</span>
</div>
<p className="drop-hint"> PNG / JPG / WEBP使</p>
{materialPaths.length > 0 && (
<div className="material-grid">
{materialPaths.map((path) => (
<article className="material-card" key={path}>
<img src={convertFileSrc(path)} alt="素材图片" />
<button onClick={() => removeMaterialImage(path)} disabled={isBusy}></button>
</article>
))}
</div> </div>
)} {materialPaths.length > 0 && (
<button className="ghost mini" onClick={() => setMaterialPaths([])} disabled={isBusy}></button>
)}
</div>
<div className="reference-strip">
<button className="add-reference-card" onClick={pickMaterialImages} disabled={isBusy}>
<span>+</span>
<strong></strong>
<small>PNG/JPG/WEBP</small>
</button>
{materialPaths.map((path, index) => (
<article className="reference-card" key={path}>
<img src={convertFileSrc(path)} alt="素材图片" />
<span>{index + 1}</span>
<button onClick={() => removeMaterialImage(path)} disabled={isBusy}>×</button>
</article>
))}
</div>
</div> </div>
<div className="params-card"> <div className="params-card">
@@ -314,29 +325,31 @@ function App() {
<span></span> <span></span>
<strong></strong> <strong></strong>
</div> </div>
<div className="segmented"> <label className="field compact-field">
{['1024x1024', '1024x1536', '1536x1024'].map((size) => ( <span></span>
<button <select value={selectedImageModel} onChange={(event) => setSelectedImageModel(event.target.value)} disabled={isBusy}>
className={imageSize === size ? 'active' : ''} {imageModelOptions.map((model) => (
key={size} <option key={model} value={model}>{model}</option>
onClick={() => setImageSize(size)} ))}
disabled={isBusy} </select>
> </label>
{size} <div className="grid two">
</button> <label className="field compact-field">
))} <span></span>
</div> <select value={imageSize} onChange={(event) => setImageSize(event.target.value)} disabled={isBusy}>
<div className="segmented compact"> {imageSizeOptions.map((size) => (
{['auto', 'high', 'medium', 'low'].map((quality) => ( <option key={size} value={size}>{size}</option>
<button ))}
className={imageQuality === quality ? 'active' : ''} </select>
key={quality} </label>
onClick={() => setImageQuality(quality)} <label className="field compact-field">
disabled={isBusy} <span></span>
> <select value={imageQuality} onChange={(event) => setImageQuality(event.target.value)} disabled={isBusy}>
{quality} {imageQualityOptions.map((quality) => (
</button> <option key={quality} value={quality}>{quality}</option>
))} ))}
</select>
</label>
</div> </div>
</div> </div>
@@ -344,22 +357,7 @@ function App() {
{isBusy ? '正在生成...' : '开始生成'} {isBusy ? '正在生成...' : '开始生成'}
</button> </button>
<div className={`progress-card ${isBusy ? 'is-loading' : ''}`}> {status !== '准备就绪' && <p className="status">{status}</p>}
<div className="spinner" aria-hidden="true" />
<div className="progress-content">
<strong>{isBusy ? '生成中' : '生成流程'}</strong>
<ol className="step-list">
{generationSteps.map((step) => (
<li className={`step ${step.status}`} key={step.label}>
<span />
{step.label}
</li>
))}
</ol>
</div>
</div>
<p className="status">{status}</p>
</aside> </aside>
<section className="result-card"> <section className="result-card">
@@ -393,6 +391,21 @@ function App() {
))} ))}
</div> </div>
)} )}
<div className={`progress-card result-progress ${isBusy ? 'is-loading' : ''}`}>
<div className="spinner" aria-hidden="true" />
<div className="progress-content">
<strong>{isBusy ? '生成中' : '生成流程'}</strong>
<ol className="step-list">
{generationSteps.map((step) => (
<li className={`step ${step.status}`} key={step.label}>
<span />
{step.label}
</li>
))}
</ol>
</div>
</div>
</section> </section>
</section> </section>
@@ -452,23 +465,6 @@ function App() {
</label> </label>
</section> </section>
<section className="settings-group">
<div className="section-heading">
<span></span>
<strong></strong>
</div>
<div className="grid two">
<label className="field">
<span></span>
<input value={providerForm.text_model} onChange={(event) => updateProviderForm('text_model', event.target.value)} />
</label>
<label className="field">
<span></span>
<input value={providerForm.image_model} onChange={(event) => updateProviderForm('image_model', event.target.value)} />
</label>
</div>
</section>
<div className="drawer-actions"> <div className="drawer-actions">
<button onClick={saveProvider} disabled={isBusy}></button> <button onClick={saveProvider} disabled={isBusy}></button>
<button className="ghost" onClick={refreshProviders} disabled={isBusy}></button> <button className="ghost" onClick={refreshProviders} disabled={isBusy}></button>
@@ -489,7 +485,6 @@ function App() {
<button className="link-button" onClick={() => loadProvider(provider)} disabled={isBusy}> <button className="link-button" onClick={() => loadProvider(provider)} disabled={isBusy}>
<strong>{provider.name}</strong> <strong>{provider.name}</strong>
<span>{provider.base_url}</span> <span>{provider.base_url}</span>
<span>{provider.image_model}</span>
</button> </button>
<button className="danger" onClick={() => deleteProvider(provider.id)} disabled={isBusy}></button> <button className="danger" onClick={() => deleteProvider(provider.id)} disabled={isBusy}></button>
</div> </div>

View File

@@ -1,6 +1,9 @@
:root { :root {
color: #172033; color: #172033;
background: #eef3fb; background:
radial-gradient(circle at 8% 10%, rgba(255, 221, 77, 0.22), transparent 28%),
radial-gradient(circle at 92% 8%, rgba(96, 239, 154, 0.18), transparent 24%),
linear-gradient(135deg, #f7f9ff 0%, #eef4fb 46%, #f8fbf6 100%);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
} }
@@ -10,9 +13,10 @@
body { body {
margin: 0; margin: 0;
overflow: hidden;
} }
button, textarea, input { button, textarea, input, select {
font: inherit; font: inherit;
} }
@@ -54,21 +58,28 @@ button.mini {
} }
.app-shell { .app-shell {
min-height: 100vh; display: grid;
padding: 22px; gap: 18px;
grid-template-rows: auto minmax(0, 1fr);
height: 100vh;
min-width: 1120px;
overflow: hidden;
padding: 20px;
} }
.topbar { .topbar {
align-items: center; align-items: center;
background: rgba(255, 255, 255, 0.82); backdrop-filter: blur(18px);
border: 1px solid rgba(203, 213, 225, 0.8); background: rgba(255, 255, 255, 0.76);
border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.86);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 28px;
box-shadow: 0 20px 60px rgba(30, 41, 59, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.72);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin: 0 auto 18px; margin: 0 auto;
max-width: 1440px; max-width: 1440px;
padding: 16px 18px; padding: 16px 18px;
width: 100%;
} }
.brand { .brand {
@@ -78,15 +89,12 @@ button.mini {
} }
.brand-mark { .brand-mark {
align-items: center; background: linear-gradient(180deg, #ffffff, #f8fafc);
background: linear-gradient(135deg, #2563eb, #7c3aed);
border-radius: 18px; border-radius: 18px;
color: #ffffff; box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12), inset 0 0 0 1px rgba(15, 23, 42, 0.08);
display: grid;
font-size: 24px;
font-weight: 900;
height: 52px; height: 52px;
place-items: center; object-fit: contain;
padding: 6px;
width: 52px; width: 52px;
} }
@@ -108,8 +116,8 @@ button.mini {
} }
.current-provider { .current-provider {
background: #f8fafc; background: rgba(248, 250, 252, 0.88);
border: 1px solid #e2e8f0; border: 1px solid rgba(226, 232, 240, 0.96);
border-radius: 16px; border-radius: 16px;
display: grid; display: grid;
gap: 4px; gap: 4px;
@@ -129,29 +137,56 @@ button.mini {
} }
.workspace { .workspace {
align-items: stretch;
display: grid; display: grid;
gap: 18px; gap: 18px;
grid-template-columns: minmax(360px, 440px) minmax(0, 1fr); grid-template-columns: minmax(360px, 440px) minmax(0, 1fr);
margin: 0 auto; margin: 0 auto;
max-width: 1440px; max-width: 1440px;
height: 100%;
min-height: 0;
width: 100%;
} }
.compose-card, .result-card, .settings-drawer { .compose-card, .result-card, .settings-drawer {
background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(18px);
border: 1px solid rgba(203, 213, 225, 0.9); background: rgba(255, 255, 255, 0.86);
border-radius: 28px; border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.1); border-radius: 30px;
box-shadow: 0 28px 80px rgba(30, 41, 59, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.74);
} }
.compose-card { .compose-card {
align-self: start; display: flex;
flex-direction: column;
gap: 12px;
min-height: 0;
overflow: hidden;
padding: 16px;
}
.compose-card > .section-heading,
.compose-card > .material-panel,
.compose-card > .params-card,
.compose-card > .generate-button,
.compose-card > .status {
flex: 0 0 auto;
}
.prompt-field {
display: grid; display: grid;
gap: 18px; grid-template-rows: auto minmax(0, 1fr);
padding: 20px; flex: 1 1 auto;
gap: 8px;
min-height: 260px;
overflow: hidden;
} }
.result-card { .result-card {
min-height: calc(100vh - 132px); display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 20px; padding: 20px;
} }
@@ -160,6 +195,7 @@ button.mini {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
min-height: 34px;
} }
.heading-actions { .heading-actions {
@@ -180,25 +216,36 @@ button.mini {
font-weight: 800; font-weight: 800;
} }
input, textarea { input, textarea, select {
max-width: 100%;
width: 100%; width: 100%;
border: 1px solid #dbe4f0; border: 1px solid #dbe4f0;
border-radius: 18px; border-radius: 18px;
color: #172033; color: #172033;
background: #f8fafc; background: rgba(248, 250, 252, 0.88);
outline: none; outline: none;
padding: 13px 14px; padding: 13px 14px;
} }
input:focus, textarea:focus { input:focus, textarea:focus, select:focus {
background: #ffffff; background: #ffffff;
border-color: #60a5fa; border-color: #60a5fa;
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.18); box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.18);
} }
select {
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, #64748b 50%), linear-gradient(135deg, #64748b 50%, transparent 50%);
background-position: calc(100% - 18px) 52%, calc(100% - 12px) 52%;
background-size: 6px 6px, 6px 6px;
background-repeat: no-repeat;
}
.prompt-field textarea { .prompt-field textarea {
min-height: 190px; height: 100%;
resize: vertical; min-height: 0;
max-width: 100%;
resize: none;
} }
small { small {
@@ -216,76 +263,154 @@ small {
} }
.material-panel { .material-panel {
background: #f8fafc; background: rgba(248, 250, 252, 0.72);
border: 1px dashed #cbd5e1; border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: 22px; border-radius: 22px;
padding: 14px; display: grid;
gap: 12px;
grid-template-rows: auto 120px;
height: 200px;
overflow: hidden;
padding: 12px;
} }
.material-toolbar { .material-header {
align-items: center; align-items: center;
display: flex; display: flex;
flex-wrap: wrap; justify-content: space-between;
gap: 10px; gap: 12px;
} }
.material-toolbar span { .material-header div {
display: grid;
gap: 3px;
}
.material-header strong {
color: #0f172a;
}
.material-header span {
color: #64748b; color: #64748b;
font-size: 12px;
}
.reference-strip {
display: flex;
gap: 10px;
min-height: 120px;
overflow-x: auto;
overflow-y: hidden;
padding: 8px 2px;
}
.add-reference-card, .reference-card {
border-radius: 18px;
flex: 0 0 104px;
height: 104px;
}
.add-reference-card {
align-items: center;
background: linear-gradient(180deg, #ffffff, #f8fafc);
border: 1px dashed rgba(37, 99, 235, 0.42);
color: #2563eb;
display: grid;
gap: 2px;
justify-items: center;
padding: 12px;
}
.add-reference-card:hover:not(:disabled) {
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.14);
}
.add-reference-card > span {
align-items: center;
background: #eff6ff;
border-radius: 999px;
display: grid;
font-size: 22px;
height: 34px;
place-items: center;
width: 34px;
}
.add-reference-card strong {
font-size: 13px; font-size: 13px;
} }
.drop-hint { .add-reference-card small {
color: #94a3b8; font-size: 11px;
font-size: 12px;
margin: 10px 0 0;
} }
.material-grid { .reference-card {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
margin-top: 12px;
}
.material-card {
background: #ffffff; background: #ffffff;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 18px; box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08);
display: grid;
gap: 8px;
overflow: hidden; overflow: hidden;
padding-bottom: 8px; position: relative;
} }
.material-card img { .reference-card img {
aspect-ratio: 1;
background: #e2e8f0; background: #e2e8f0;
height: 100%;
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
} }
.material-card button { .reference-card button {
justify-self: center; align-items: center;
padding: 6px 10px; background: rgba(15, 23, 42, 0.72);
border-radius: 999px;
display: grid;
height: 24px;
padding: 0;
place-items: center;
position: absolute;
right: 7px;
top: 7px;
width: 24px;
}
.reference-card > span {
align-items: center;
background: rgba(255, 255, 255, 0.88);
border-radius: 999px;
bottom: 7px;
color: #334155;
display: grid;
font-size: 12px;
font-weight: 900;
height: 24px;
left: 7px;
place-items: center;
position: absolute;
width: 24px;
} }
.generate-button { .generate-button {
background: linear-gradient(135deg, #2563eb, #7c3aed); background: linear-gradient(135deg, #2563eb, #7c3aed 58%, #f97316);
border-radius: 18px; border-radius: 18px;
box-shadow: 0 18px 36px rgba(37, 99, 235, 0.28);
font-size: 17px; font-size: 17px;
padding: 15px 18px; padding: 15px 18px;
} }
.progress-card { .progress-card {
align-items: flex-start; align-items: flex-start;
background: #f8fafc; background: rgba(248, 250, 252, 0.82);
border: 1px solid #e2e8f0; border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: 22px; border-radius: 22px;
display: flex; display: flex;
gap: 14px; gap: 14px;
padding: 15px; padding: 15px;
} }
.result-progress {
margin-top: auto;
}
.spinner { .spinner {
border: 3px solid rgba(148, 163, 184, 0.32); border: 3px solid rgba(148, 163, 184, 0.32);
border-top-color: #2563eb; border-top-color: #2563eb;
@@ -374,13 +499,21 @@ small {
.empty-state { .empty-state {
align-items: center; align-items: center;
border: 1px dashed #cbd5e1; background: linear-gradient(180deg, rgba(248, 250, 252, 0.5), rgba(255, 255, 255, 0.76));
border: 1px dashed rgba(148, 163, 184, 0.68);
border-radius: 26px; border-radius: 26px;
color: #64748b; color: #64748b;
display: grid; display: grid;
flex: 1;
min-height: 420px; min-height: 420px;
place-items: center; place-items: center;
text-align: center; text-align: center;
margin-bottom: 16px;
}
.empty-state > div {
display: grid;
place-items: center;
} }
.empty-state div { .empty-state div {
@@ -390,17 +523,22 @@ small {
margin-bottom: 8px; margin-bottom: 8px;
} }
.empty-state p {
margin: 0;
}
.image-grid { .image-grid {
display: grid; display: grid;
gap: 16px; gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
margin-bottom: 18px;
} }
.image-card { .image-card {
background: #ffffff; background: #ffffff;
border: 1px solid #e2e8f0; border: 1px solid rgba(226, 232, 240, 0.9);
border-radius: 22px; border-radius: 24px;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08); box-shadow: 0 18px 46px rgba(15, 23, 42, 0.1);
overflow: hidden; overflow: hidden;
} }
@@ -487,6 +625,11 @@ small {
gap: 10px; gap: 10px;
} }
.drawer-actions button {
border-radius: 12px;
padding: 8px 14px;
}
.saved-providers { .saved-providers {
border-top: 1px solid #e2e8f0; border-top: 1px solid #e2e8f0;
display: grid; display: grid;
@@ -536,28 +679,11 @@ small {
} }
@media (max-width: 980px) { @media (max-width: 980px) {
.workspace { .workspace { grid-template-columns: minmax(360px, 440px) minmax(0, 1fr); }
grid-template-columns: 1fr;
}
.result-card {
min-height: 420px;
}
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.app-shell { .grid.two, .provider-item { grid-template-columns: 1fr; }
padding: 12px;
}
.topbar, .topbar-actions {
align-items: stretch;
flex-direction: column;
}
.grid.two, .provider-item {
grid-template-columns: 1fr;
}
} }
.params-card { .params-card {
@@ -565,37 +691,18 @@ small {
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 22px; border-radius: 22px;
display: grid; display: grid;
gap: 12px; gap: 8px;
padding: 14px; padding: 10px;
} }
.segmented { .compact-field {
background: #eef2f7;
border-radius: 16px;
display: grid;
gap: 6px; gap: 6px;
grid-template-columns: repeat(3, 1fr);
padding: 6px;
} }
.segmented.compact { .compact-field input,
grid-template-columns: repeat(4, 1fr); .compact-field select {
} border-radius: 14px;
padding: 10px 12px;
.segmented button {
background: transparent;
color: #64748b;
padding: 9px 8px;
}
.segmented button:hover:not(:disabled) {
box-shadow: none;
}
.segmented button.active {
background: #ffffff;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
color: #1d4ed8;
} }
.image-preview-button { .image-preview-button {

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />