feat: refine tool UI and result actions
This commit is contained in:
479
src/main.tsx
479
src/main.tsx
@@ -66,19 +66,23 @@ const defaultProviderForm: ProviderForm = {
|
||||
};
|
||||
|
||||
const initialGenerationSteps: GenerationStep[] = [
|
||||
{ label: '保存 Provider 配置', status: 'pending' },
|
||||
{ label: '提交生成任务', status: 'pending' },
|
||||
{ label: '请求并等待图像模型返回', status: 'pending' },
|
||||
{ label: '保存图片到应用数据文件夹', status: 'pending' },
|
||||
{ label: '更新本次打开图片列表', status: 'pending' },
|
||||
{ label: '保存配置', status: 'pending' },
|
||||
{ label: '提交任务', status: 'pending' },
|
||||
{ label: '等待模型返回', status: 'pending' },
|
||||
{ label: '保存到应用文件夹', status: 'pending' },
|
||||
{ label: '更新结果列表', status: 'pending' },
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [providers, setProviders] = useState<ProviderConfig[]>([]);
|
||||
const [providerForm, setProviderForm] = useState<ProviderForm>(defaultProviderForm);
|
||||
const [prompt, setPrompt] = useState('一只赛博朋克风格的橘猫坐在霓虹灯下');
|
||||
const [imageSize, setImageSize] = useState('1024x1024');
|
||||
const [imageQuality, setImageQuality] = useState('auto');
|
||||
const [status, setStatus] = useState('准备就绪');
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState<SessionImage | null>(null);
|
||||
const [sessionImages, setSessionImages] = useState<SessionImage[]>([]);
|
||||
const [materialPaths, setMaterialPaths] = useState<string[]>([]);
|
||||
const [generationSteps, setGenerationSteps] = useState<GenerationStep[]>(initialGenerationSteps);
|
||||
@@ -124,13 +128,11 @@ function App() {
|
||||
|
||||
async function saveProvider() {
|
||||
setIsBusy(true);
|
||||
setStatus('正在保存 Provider 配置...');
|
||||
setStatus('正在保存配置...');
|
||||
try {
|
||||
await invoke('upsert_provider', {
|
||||
input: providerForm,
|
||||
});
|
||||
await invoke('upsert_provider', { input: providerForm });
|
||||
await refreshProviders();
|
||||
setStatus('Provider 已保存,可以直接生成图片');
|
||||
setStatus('配置已保存');
|
||||
} catch (error) {
|
||||
setStatus(`保存失败:${formatError(error)}`);
|
||||
} finally {
|
||||
@@ -140,14 +142,14 @@ function App() {
|
||||
|
||||
async function deleteProvider(id: string) {
|
||||
setIsBusy(true);
|
||||
setStatus('正在删除 Provider...');
|
||||
setStatus('正在删除配置...');
|
||||
try {
|
||||
await invoke('delete_provider', { id });
|
||||
await refreshProviders();
|
||||
if (providerForm.id === id) {
|
||||
setProviderForm(defaultProviderForm);
|
||||
}
|
||||
setStatus('Provider 已删除');
|
||||
setStatus('配置已删除');
|
||||
} catch (error) {
|
||||
setStatus(`删除失败:${formatError(error)}`);
|
||||
} finally {
|
||||
@@ -167,7 +169,7 @@ function App() {
|
||||
image_model: provider.image_model ?? defaultProviderForm.image_model,
|
||||
enabled: provider.enabled,
|
||||
}));
|
||||
setStatus('已载入 Provider,修改后点击保存即可覆盖当前配置。');
|
||||
setStatus('已切换模型配置');
|
||||
}
|
||||
|
||||
async function generateImage() {
|
||||
@@ -176,9 +178,7 @@ function App() {
|
||||
setStatus('正在生成图片...');
|
||||
try {
|
||||
startStep(0);
|
||||
await invoke('upsert_provider', {
|
||||
input: providerForm,
|
||||
});
|
||||
await invoke('upsert_provider', { input: providerForm });
|
||||
startStep(1);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
||||
startStep(2);
|
||||
@@ -187,8 +187,8 @@ function App() {
|
||||
provider_id: providerForm.id,
|
||||
prompt,
|
||||
model: providerForm.image_model,
|
||||
size: '1024x1024',
|
||||
quality: 'auto',
|
||||
size: imageSize,
|
||||
quality: imageQuality,
|
||||
image_paths: materialPaths,
|
||||
},
|
||||
});
|
||||
@@ -206,7 +206,7 @@ function App() {
|
||||
...images,
|
||||
]);
|
||||
setStep(4, 'done');
|
||||
setStatus(`生成完成,已保存到应用数据文件夹:${result.asset.file_path}`);
|
||||
setStatus(`生成完成:${result.asset.file_path}`);
|
||||
} catch (error) {
|
||||
setGenerationSteps((steps) =>
|
||||
steps.map((step) => (step.status === 'active' ? { ...step, status: 'error' } : step)),
|
||||
@@ -218,15 +218,15 @@ function App() {
|
||||
}
|
||||
|
||||
async function pickMaterialImages() {
|
||||
setStatus('正在打开素材图片选择器...');
|
||||
setStatus('正在打开素材选择器...');
|
||||
try {
|
||||
const paths = await invoke<string[]>('pick_material_images');
|
||||
if (paths.length === 0) {
|
||||
setStatus('未选择素材图片');
|
||||
setStatus('未选择素材');
|
||||
return;
|
||||
}
|
||||
setMaterialPaths((current) => Array.from(new Set([...current, ...paths])));
|
||||
setStatus(`已导入 ${paths.length} 张素材图片`);
|
||||
setStatus(`已导入 ${paths.length} 张素材`);
|
||||
} catch (error) {
|
||||
setStatus(`打开素材选择器失败:${formatError(error)}`);
|
||||
}
|
||||
@@ -236,186 +236,293 @@ function App() {
|
||||
setMaterialPaths((current) => current.filter((item) => item !== path));
|
||||
}
|
||||
|
||||
async function revealImage(path: string) {
|
||||
try {
|
||||
await invoke('reveal_path', { path });
|
||||
} catch (error) {
|
||||
setStatus(`打开文件位置失败:${formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function openGeneratedDir() {
|
||||
try {
|
||||
await invoke('open_generated_dir');
|
||||
} catch (error) {
|
||||
setStatus(`打开保存目录失败:${formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshProviders().catch(() => setStatus('后端未启动或数据库初始化失败'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="app">
|
||||
<section className="hero">
|
||||
<h1>Image Draw AI</h1>
|
||||
<p>图片默认保存到应用数据文件夹。</p>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-title">
|
||||
<main className="app-shell">
|
||||
<header className="topbar">
|
||||
<div className="brand">
|
||||
<div className="brand-mark">绘</div>
|
||||
<div>
|
||||
<p className="eyebrow">Provider</p>
|
||||
<h2>模型供应商配置</h2>
|
||||
</div>
|
||||
<button onClick={refreshProviders} disabled={isBusy}>刷新</button>
|
||||
</div>
|
||||
|
||||
<div className="grid two">
|
||||
<label>
|
||||
配置 ID
|
||||
<input
|
||||
value={providerForm.id}
|
||||
onChange={(event) => updateProviderForm('id', event.target.value)}
|
||||
placeholder="default-openai"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
名称
|
||||
<input
|
||||
value={providerForm.name}
|
||||
onChange={(event) => updateProviderForm('name', event.target.value)}
|
||||
placeholder="OpenAI / 中转站"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Base URL
|
||||
<input
|
||||
value={providerForm.base_url}
|
||||
onChange={(event) => updateProviderForm('base_url', event.target.value)}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
/>
|
||||
<small>填写 API 地址,不是中转站网页地址;通常以 /v1 结尾。</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
API Key
|
||||
<input
|
||||
value={providerForm.api_key}
|
||||
onChange={(event) => updateProviderForm('api_key', event.target.value)}
|
||||
placeholder="sk-... 或中转站 key"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid two">
|
||||
<label>
|
||||
文本模型
|
||||
<input
|
||||
value={providerForm.text_model}
|
||||
onChange={(event) => updateProviderForm('text_model', event.target.value)}
|
||||
placeholder="gpt-5"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
图像模型
|
||||
<input
|
||||
value={providerForm.image_model}
|
||||
onChange={(event) => updateProviderForm('image_model', event.target.value)}
|
||||
placeholder="gpt-image-2"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<button onClick={saveProvider} disabled={isBusy}>保存 Provider</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-title">
|
||||
<div>
|
||||
<p className="eyebrow">Generate</p>
|
||||
<h2>生成图片</h2>
|
||||
<h1>Image Draw AI</h1>
|
||||
<p>图片默认保存到应用数据文件夹</p>
|
||||
</div>
|
||||
</div>
|
||||
<textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} />
|
||||
<div className="material-toolbar">
|
||||
<button onClick={pickMaterialImages} disabled={isBusy}>导入素材图片</button>
|
||||
{materialPaths.length > 0 && (
|
||||
<button onClick={() => setMaterialPaths([])} disabled={isBusy}>清空素材</button>
|
||||
)}
|
||||
<span>{materialPaths.length > 0 ? `已选择 ${materialPaths.length} 张素材,将使用图像编辑模式` : '未选择素材,将使用文生图模式'}</span>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<span>{path}</span>
|
||||
</article>
|
||||
))}
|
||||
<div className="topbar-actions">
|
||||
<div className="current-provider">
|
||||
<span>当前模型</span>
|
||||
<strong>{providerForm.image_model}</strong>
|
||||
</div>
|
||||
)}
|
||||
<button className="ghost" onClick={() => setIsSettingsOpen(true)}>设置</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<button className="primary" onClick={generateImage} disabled={isBusy}>
|
||||
{isBusy ? '正在生成...' : '生成图片'}
|
||||
</button>
|
||||
<div className={`progress-card ${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>
|
||||
<section className="workspace">
|
||||
<aside className="compose-card">
|
||||
<div className="section-heading">
|
||||
<span>创作区</span>
|
||||
<strong>{materialPaths.length > 0 ? '素材生成' : '文字生成'}</strong>
|
||||
</div>
|
||||
|
||||
<label className="field prompt-field">
|
||||
<span>提示词</span>
|
||||
<textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<div className="material-panel">
|
||||
<div className="material-toolbar">
|
||||
<button onClick={pickMaterialImages} disabled={isBusy}>导入素材</button>
|
||||
{materialPaths.length > 0 && (
|
||||
<button className="ghost" onClick={() => setMaterialPaths([])} disabled={isBusy}>清空</button>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<div className="params-card">
|
||||
<div className="section-heading">
|
||||
<span>生成参数</span>
|
||||
<strong>基础</strong>
|
||||
</div>
|
||||
<div className="segmented">
|
||||
{['1024x1024', '1024x1536', '1536x1024'].map((size) => (
|
||||
<button
|
||||
className={imageSize === size ? 'active' : ''}
|
||||
key={size}
|
||||
onClick={() => setImageSize(size)}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="segmented compact">
|
||||
{['auto', 'high', 'medium', 'low'].map((quality) => (
|
||||
<button
|
||||
className={imageQuality === quality ? 'active' : ''}
|
||||
key={quality}
|
||||
onClick={() => setImageQuality(quality)}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{quality}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="status">{status}</p>
|
||||
|
||||
<button className="generate-button" onClick={generateImage} disabled={isBusy}>
|
||||
{isBusy ? '正在生成...' : '开始生成'}
|
||||
</button>
|
||||
|
||||
<div className={`progress-card ${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>
|
||||
|
||||
<p className="status">{status}</p>
|
||||
</aside>
|
||||
|
||||
<section className="result-card">
|
||||
<div className="section-heading result-heading">
|
||||
<span>结果区</span>
|
||||
<div className="heading-actions">
|
||||
<strong>本次生成 {sessionImages.length} 张</strong>
|
||||
<button className="ghost mini" onClick={openGeneratedDir}>打开目录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sessionImages.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div>暂无图片</div>
|
||||
<p>输入提示词,点击开始生成后会显示在这里。</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="image-grid">
|
||||
{sessionImages.map((image) => (
|
||||
<article className="image-card" key={image.id}>
|
||||
<button className="image-preview-button" onClick={() => setPreviewImage(image)}>
|
||||
<img src={convertFileSrc(image.file_path)} alt={image.prompt} />
|
||||
</button>
|
||||
<div>
|
||||
<strong>{image.created_at}</strong>
|
||||
<p>{image.prompt}</p>
|
||||
<button className="ghost mini" onClick={() => revealImage(image.file_path)}>定位文件</button>
|
||||
<span>{image.file_path}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-title">
|
||||
<div>
|
||||
<p className="eyebrow">Session</p>
|
||||
<h2>本次打开生成的图片</h2>
|
||||
</div>
|
||||
<span className="count">{sessionImages.length} 张</span>
|
||||
</div>
|
||||
{sessionImages.length === 0 ? (
|
||||
<p>当前打开周期内还没有生成图片。</p>
|
||||
) : (
|
||||
<div className="image-grid">
|
||||
{sessionImages.map((image) => (
|
||||
<article className="image-card" key={image.id}>
|
||||
<img src={convertFileSrc(image.file_path)} alt={image.prompt} />
|
||||
<div>
|
||||
<strong>{image.created_at}</strong>
|
||||
<p>{image.prompt}</p>
|
||||
<span>{image.file_path}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{isSettingsOpen && (
|
||||
<div className="drawer-layer">
|
||||
<button className="drawer-mask" onClick={() => setIsSettingsOpen(false)} aria-label="关闭设置" />
|
||||
<aside className="settings-drawer">
|
||||
<div className="drawer-header">
|
||||
<div>
|
||||
<span>设置</span>
|
||||
<h2>模型供应商</h2>
|
||||
</div>
|
||||
<button className="ghost" onClick={() => setIsSettingsOpen(false)}>关闭</button>
|
||||
</div>
|
||||
|
||||
<section className="panel">
|
||||
<h2>已保存 Provider</h2>
|
||||
{providers.length === 0 ? (
|
||||
<p>暂无 Provider,请先保存配置。</p>
|
||||
) : (
|
||||
<ul className="provider-list">
|
||||
{providers.map((provider) => (
|
||||
<li key={provider.id}>
|
||||
<div className="provider-item">
|
||||
<button className="link-button" onClick={() => loadProvider(provider)} disabled={isBusy}>
|
||||
<strong>{provider.name}</strong>
|
||||
<span>{provider.base_url}</span>
|
||||
<span>{provider.image_model}</span>
|
||||
</button>
|
||||
<button className="danger" onClick={() => deleteProvider(provider.id)} disabled={isBusy}>
|
||||
删除
|
||||
</button>
|
||||
<div className="settings-content">
|
||||
<section className="settings-group">
|
||||
<div className="section-heading">
|
||||
<span>基础信息</span>
|
||||
<strong>配置名称</strong>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
<div className="grid two">
|
||||
<label className="field">
|
||||
<span>配置 ID</span>
|
||||
<input value={providerForm.id} onChange={(event) => updateProviderForm('id', event.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>名称</span>
|
||||
<input value={providerForm.name} onChange={(event) => updateProviderForm('name', event.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings-group">
|
||||
<div className="section-heading">
|
||||
<span>接口信息</span>
|
||||
<strong>中转站 / OpenAI</strong>
|
||||
</div>
|
||||
<label className="field">
|
||||
<span>Base URL</span>
|
||||
<input
|
||||
value={providerForm.base_url}
|
||||
onChange={(event) => updateProviderForm('base_url', event.target.value)}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
/>
|
||||
<small>填写 API 地址,不是中转站网页地址;通常以 /v1 结尾。</small>
|
||||
</label>
|
||||
|
||||
<label className="field">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
value={providerForm.api_key}
|
||||
onChange={(event) => updateProviderForm('api_key', event.target.value)}
|
||||
placeholder="sk-... 或中转站 key"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
</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">
|
||||
<button onClick={saveProvider} disabled={isBusy}>保存配置</button>
|
||||
<button className="ghost" onClick={refreshProviders} disabled={isBusy}>刷新</button>
|
||||
</div>
|
||||
|
||||
<div className="saved-providers">
|
||||
<div className="section-heading">
|
||||
<span>已保存</span>
|
||||
<strong>{providers.length} 个配置</strong>
|
||||
</div>
|
||||
{providers.length === 0 ? (
|
||||
<p className="muted">暂无配置,保存后会出现在这里。</p>
|
||||
) : (
|
||||
<ul className="provider-list">
|
||||
{providers.map((provider) => (
|
||||
<li key={provider.id}>
|
||||
<div className="provider-item">
|
||||
<button className="link-button" onClick={() => loadProvider(provider)} disabled={isBusy}>
|
||||
<strong>{provider.name}</strong>
|
||||
<span>{provider.base_url}</span>
|
||||
<span>{provider.image_model}</span>
|
||||
</button>
|
||||
<button className="danger" onClick={() => deleteProvider(provider.id)} disabled={isBusy}>删除</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewImage && (
|
||||
<div className="preview-layer">
|
||||
<button className="drawer-mask" onClick={() => setPreviewImage(null)} aria-label="关闭预览" />
|
||||
<section className="preview-modal">
|
||||
<div className="preview-header">
|
||||
<div>
|
||||
<span>图片预览</span>
|
||||
<h2>{previewImage.created_at}</h2>
|
||||
</div>
|
||||
<button className="ghost" onClick={() => setPreviewImage(null)}>关闭</button>
|
||||
</div>
|
||||
<img src={convertFileSrc(previewImage.file_path)} alt={previewImage.prompt} />
|
||||
<div className="preview-info">
|
||||
<p>{previewImage.prompt}</p>
|
||||
<button className="ghost" onClick={() => revealImage(previewImage.file_path)}>在文件夹中显示</button>
|
||||
<span>{previewImage.file_path}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
814
src/styles.css
814
src/styles.css
@@ -1,7 +1,11 @@
|
||||
:root {
|
||||
color: #f8fafc;
|
||||
background: radial-gradient(circle at top left, #1e3a8a 0, #0f172a 34%, #020617 100%);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #172033;
|
||||
background: #eef3fb;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -12,129 +16,487 @@ button, textarea, input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
color: #ffffff;
|
||||
background: #2563eb;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 11px 16px;
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease, opacity 0.16s ease;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.22);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.62;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px;
|
||||
button.ghost {
|
||||
background: #eef4ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: 24px;
|
||||
button.danger {
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: #38bdf8;
|
||||
button.mini {
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
margin: 0 0 8px;
|
||||
text-transform: uppercase;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin: 0 0 12px;
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(15, 23, 42, 0.86);
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
align-items: flex-start;
|
||||
.topbar {
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(203, 213, 225, 0.8);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin: 0 auto 18px;
|
||||
max-width: 1440px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.row {
|
||||
.brand {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
||||
border-radius: 18px;
|
||||
color: #ffffff;
|
||||
display: grid;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
height: 52px;
|
||||
place-items: center;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
margin: 0 0 7px;
|
||||
}
|
||||
|
||||
.brand p, .muted {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.current-provider {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 160px;
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.current-provider span, .section-heading span, .drawer-header span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.current-provider strong {
|
||||
color: #1d4ed8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: minmax(360px, 440px) minmax(0, 1fr);
|
||||
margin: 0 auto;
|
||||
max-width: 1440px;
|
||||
}
|
||||
|
||||
.compose-card, .result-card, .settings-drawer {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(203, 213, 225, 0.9);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.compose-card {
|
||||
align-self: start;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
min-height: calc(100vh - 132px);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.heading-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-heading strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.field {
|
||||
color: #334155;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 18px;
|
||||
color: #172033;
|
||||
background: #f8fafc;
|
||||
outline: none;
|
||||
padding: 13px 14px;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
background: #ffffff;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.18);
|
||||
}
|
||||
|
||||
.prompt-field textarea {
|
||||
min-height: 190px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.grid.two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
color: #cbd5e1;
|
||||
.material-panel {
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 22px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.material-toolbar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.material-toolbar span {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.drop-hint {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.material-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.material-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
.material-card img {
|
||||
aspect-ratio: 1;
|
||||
background: #e2e8f0;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 10px 16px;
|
||||
color: #e2e8f0;
|
||||
background: #334155;
|
||||
cursor: pointer;
|
||||
.material-card button {
|
||||
justify-self: center;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
.generate-button {
|
||||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
||||
border-radius: 18px;
|
||||
font-size: 17px;
|
||||
padding: 15px 18px;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
align-items: flex-start;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 22px;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid rgba(148, 163, 184, 0.32);
|
||||
border-top-color: #2563eb;
|
||||
border-radius: 999px;
|
||||
flex: 0 0 auto;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.progress-card.is-loading .spinner {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.step-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.step {
|
||||
align-items: center;
|
||||
background: #eef2f7;
|
||||
border-radius: 999px;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
padding: 6px 9px;
|
||||
}
|
||||
|
||||
.step span {
|
||||
background: #94a3b8;
|
||||
border-radius: 999px;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
color: #f8fafc;
|
||||
background: #020617;
|
||||
outline: none;
|
||||
.step.active span {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.16);
|
||||
.step.done {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 140px;
|
||||
margin-bottom: 16px;
|
||||
resize: vertical;
|
||||
.step.done span {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.step.error {
|
||||
background: #ffe4e6;
|
||||
color: #e11d48;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.step.error span {
|
||||
background: #e11d48;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: #93c5fd;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.result-heading {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
align-items: center;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 26px;
|
||||
color: #64748b;
|
||||
display: grid;
|
||||
min-height: 420px;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state div {
|
||||
color: #334155;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.image-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-card img {
|
||||
aspect-ratio: 1;
|
||||
background: #e2e8f0;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-card div {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.image-card p {
|
||||
color: #475569;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image-card span {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.drawer-layer {
|
||||
inset: 0;
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.drawer-mask {
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-mask:hover {
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.settings-drawer {
|
||||
animation: slideIn 0.22s ease-out;
|
||||
border-radius: 28px 0 0 28px;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
max-width: 520px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: min(92vw, 520px);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.drawer-header h2 {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.saved-providers {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.provider-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -149,209 +511,183 @@ textarea {
|
||||
|
||||
.link-button {
|
||||
align-items: flex-start;
|
||||
background: rgba(30, 41, 59, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #172033;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 5px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: rgba(185, 28, 28, 0.9);
|
||||
color: #fee2e2;
|
||||
}
|
||||
|
||||
.link-button span {
|
||||
color: #94a3b8;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(28px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid.two {
|
||||
grid-template-columns: 1fr;
|
||||
.app-shell {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
.topbar, .topbar-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
.grid.two, .provider-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
color: #93c5fd;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
.params-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 22px;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.image-card {
|
||||
background: rgba(2, 6, 23, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
.segmented {
|
||||
background: #eef2f7;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.image-card img {
|
||||
aspect-ratio: 1;
|
||||
background: #020617;
|
||||
.segmented.compact {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-card div {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
.image-preview-button:hover:not(:disabled) {
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.image-card p {
|
||||
color: #cbd5e1;
|
||||
.preview-layer {
|
||||
inset: 0;
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.preview-modal {
|
||||
animation: previewIn 0.22s ease-out;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 30px 90px rgba(15, 23, 42, 0.24);
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
left: 50%;
|
||||
max-height: 90vh;
|
||||
max-width: min(920px, 92vw);
|
||||
overflow: auto;
|
||||
padding: 18px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 760px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-header span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.preview-header h2 {
|
||||
font-size: 18px;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.preview-modal > img {
|
||||
background: #eef2f7;
|
||||
border-radius: 22px;
|
||||
max-height: 62vh;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-info {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-info p {
|
||||
color: #334155;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image-card span {
|
||||
.preview-info span {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
align-items: flex-start;
|
||||
background: rgba(2, 6, 23, 0.5);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
@keyframes previewIn {
|
||||
from { opacity: 0; transform: translate(-50%, -48%) scale(0.98); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 22px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid rgba(148, 163, 184, 0.28);
|
||||
border-top-color: #38bdf8;
|
||||
border-radius: 999px;
|
||||
flex: 0 0 auto;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.progress-card.is-loading .spinner {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.step-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.step {
|
||||
align-items: center;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.step span {
|
||||
background: #475569;
|
||||
border-radius: 999px;
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
color: #93c5fd;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.step.active span {
|
||||
background: #38bdf8;
|
||||
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.16);
|
||||
}
|
||||
|
||||
.step.done {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.step.done span {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.step.error {
|
||||
color: #fca5a5;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.step.error span {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.material-toolbar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.material-toolbar span {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.material-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.material-card {
|
||||
background: rgba(2, 6, 23, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.material-card img {
|
||||
aspect-ratio: 1;
|
||||
background: #020617;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.material-card button {
|
||||
justify-self: center;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.material-card span {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 0 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user