Replace native selects with NovaSelect
This commit is contained in:
@@ -7,6 +7,8 @@ import NovaCardGradientPicker from '../../components/nova-cards/NovaCardGradient
|
||||
import NovaCardFontPicker from '../../components/nova-cards/NovaCardFontPicker'
|
||||
import NovaCardAutosaveIndicator from '../../components/nova-cards/NovaCardAutosaveIndicator'
|
||||
import NovaCardPresetPicker from '../../components/nova-cards/NovaCardPresetPicker'
|
||||
import Checkbox from '../../components/ui/Checkbox'
|
||||
import NovaSelect from '../../components/ui/NovaSelect'
|
||||
|
||||
const defaultMobileSteps = [
|
||||
{ key: 'format', label: 'Format', description: 'Choose the canvas shape and basic direction.' },
|
||||
@@ -948,10 +950,9 @@ export default function StudioCardEditor() {
|
||||
const currentProjectSummary = summarizeProjectSnapshot(card.project_json || {})
|
||||
|
||||
const editorTabs = [
|
||||
{ key: 'background', label: 'Background' },
|
||||
{ key: 'content', label: 'Content' },
|
||||
{ key: 'typography', label: 'Typography' },
|
||||
{ key: 'layout', label: 'Layout' },
|
||||
{ key: 'background', label: 'Canvas' },
|
||||
{ key: 'content', label: 'Text' },
|
||||
{ key: 'style', label: 'Style' },
|
||||
{ key: 'publish', label: 'Publish' },
|
||||
]
|
||||
|
||||
@@ -1012,9 +1013,20 @@ export default function StudioCardEditor() {
|
||||
{/* Tab panels */}
|
||||
<div className="mt-2 rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
|
||||
|
||||
{/* BACKGROUND TAB */}
|
||||
{/* CANVAS TAB */}
|
||||
{activeTab === 'background' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Canvas format</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(editorOptions.formats || []).map((format) => (
|
||||
<button key={format.key} type="button" onClick={() => updateCard({ format: format.key })} className={pillClasses((card.format || 'square') === format.key)}>
|
||||
{format.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Template</div>
|
||||
<NovaCardTemplatePicker templates={editorOptions.templates || []} selectedId={card.template_id} onSelect={handleTemplateSelect} />
|
||||
@@ -1076,12 +1088,10 @@ export default function StudioCardEditor() {
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Overlay & depth</div>
|
||||
<div className="space-y-4">
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-2 block">Overlay style</span>
|
||||
<select value={card.project_json?.background?.overlay_style || 'dark-soft'} onChange={(event) => updateCard({}, { background: { overlay_style: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{overlayOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={card.project_json?.background?.overlay_style || 'dark-soft'} onChange={(val) => updateCard({}, { background: { overlay_style: val } })} options={(overlayOptions || []).map((o) => ({ value: o.value, label: o.label }))} />
|
||||
</div>
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<span>Overlay opacity</span>
|
||||
@@ -1098,12 +1108,10 @@ export default function StudioCardEditor() {
|
||||
</div>
|
||||
<input type="range" min="0" max="32" step="4" value={card.project_json?.background?.blur_level || 0} onChange={(event) => updateCard({}, { background: { blur_level: Number(event.target.value) } })} className="w-full" />
|
||||
</label>
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-2 block">Focal position</span>
|
||||
<select value={card.project_json?.background?.focal_position || 'center'} onChange={(event) => updateCard({}, { background: { focal_position: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{(editorOptions.focal_positions || []).map((position) => <option key={position.key} value={position.key}>{position.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={card.project_json?.background?.focal_position || 'center'} onChange={(val) => updateCard({}, { background: { focal_position: val } })} options={(editorOptions.focal_positions || []).map((p) => ({ value: p.key, label: p.label }))} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1146,9 +1154,15 @@ export default function StudioCardEditor() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CONTENT TAB */}
|
||||
{/* TEXT TAB */}
|
||||
{activeTab === 'content' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Font family</div>
|
||||
<NovaCardFontPicker fonts={editorOptions.font_presets || []} selectedKey={card.project_json?.typography?.font_preset} onSelect={handleFontSelect} />
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 pt-4">
|
||||
<label className="block text-sm text-slate-300">
|
||||
<span className="mb-2 block">Title</span>
|
||||
<input value={card.title || ''} onChange={(event) => updateTextField('title', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35" />
|
||||
@@ -1183,19 +1197,9 @@ export default function StudioCardEditor() {
|
||||
<button type="button" onClick={() => moveTextBlock(index, -1)} disabled={index === 0} className="rounded border border-white/10 bg-white/[0.05] px-1.5 py-0.5 text-[10px] text-white disabled:opacity-30">▲</button>
|
||||
<button type="button" onClick={() => moveTextBlock(index, 1)} disabled={index === textBlocks.length - 1} className="rounded border border-white/10 bg-white/[0.05] px-1.5 py-0.5 text-[10px] text-white disabled:opacity-30">▼</button>
|
||||
</div>
|
||||
<select value={block.type || 'body'} onChange={(event) => updateTextBlock(index, { type: event.target.value })} className="rounded-xl border border-white/10 bg-[#0d1726] px-2 py-2 text-xs text-white outline-none">
|
||||
<option value="title">Title</option>
|
||||
<option value="quote">Quote</option>
|
||||
<option value="author">Author</option>
|
||||
<option value="source">Source</option>
|
||||
<option value="body">Body</option>
|
||||
<option value="caption">Caption</option>
|
||||
</select>
|
||||
<NovaSelect value={block.type || 'body'} onChange={(val) => updateTextBlock(index, { type: val })} searchable={false} options={[{ value: 'title', label: 'Title' }, { value: 'quote', label: 'Quote' }, { value: 'author', label: 'Author' }, { value: 'source', label: 'Source' }, { value: 'body', label: 'Body' }, { value: 'caption', label: 'Caption' }]} />
|
||||
<input value={block.text || ''} onChange={(event) => updateTextBlock(index, { text: event.target.value, enabled: block.type === 'title' || block.type === 'quote' ? true : Boolean(event.target.value.trim()) })} className="min-w-0 flex-1 rounded-xl border border-white/10 bg-[#0d1726] px-3 py-2 text-sm text-white outline-none" />
|
||||
<label className="flex items-center gap-1 text-xs text-slate-400">
|
||||
<input type="checkbox" checked={block.enabled !== false} onChange={(e) => updateTextBlock(index, { enabled: e.target.checked })} className="h-4 w-4 rounded border-white/20 bg-[#0d1726]" />
|
||||
On
|
||||
</label>
|
||||
<Checkbox checked={block.enabled !== false} onChange={(e) => updateTextBlock(index, { enabled: e.target.checked })} label="On" />
|
||||
<button type="button" onClick={() => removeTextBlock(index)} disabled={block.type === 'title' || block.type === 'quote'} className="text-rose-300 transition hover:text-rose-200 disabled:opacity-30">×</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1203,17 +1207,12 @@ export default function StudioCardEditor() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TYPOGRAPHY TAB */}
|
||||
{activeTab === 'typography' && (
|
||||
{/* STYLE TAB */}
|
||||
{activeTab === 'style' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Font family</div>
|
||||
<NovaCardFontPicker fonts={editorOptions.font_presets || []} selectedKey={card.project_json?.typography?.font_preset} onSelect={handleFontSelect} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Quote size</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -1318,39 +1317,20 @@ export default function StudioCardEditor() {
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Quote mark & panel</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-1.5 block text-xs">Quote mark style</span>
|
||||
<select value={card.project_json?.typography?.quote_mark_preset || 'none'} onChange={(event) => updateCard({}, { typography: { quote_mark_preset: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{(editorOptions.quote_mark_presets || []).map((preset) => <option key={preset.key} value={preset.key}>{preset.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block text-sm text-slate-300">
|
||||
<NovaSelect value={card.project_json?.typography?.quote_mark_preset || 'none'} onChange={(val) => updateCard({}, { typography: { quote_mark_preset: val } })} options={(editorOptions.quote_mark_presets || []).map((p) => ({ value: p.key, label: p.label }))} />
|
||||
</div>
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-1.5 block text-xs">Text panel style</span>
|
||||
<select value={card.project_json?.typography?.text_panel_style || 'none'} onChange={(event) => updateCard({}, { typography: { text_panel_style: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{(editorOptions.text_panel_styles || []).map((style) => <option key={style.key} value={style.key}>{style.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={card.project_json?.typography?.text_panel_style || 'none'} onChange={(val) => updateCard({}, { typography: { text_panel_style: val } })} options={(editorOptions.text_panel_styles || []).map((s) => ({ value: s.key, label: s.label }))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* LAYOUT TAB */}
|
||||
{activeTab === 'layout' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Format</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(editorOptions.formats || []).map((format) => (
|
||||
<button key={format.key} type="button" onClick={() => updateCard({ format: format.key })} className={pillClasses((card.format || 'square') === format.key)}>
|
||||
{format.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 pt-2">
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Layout preset</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -1412,25 +1392,19 @@ export default function StudioCardEditor() {
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Frame & effects</div>
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-1.5 block">Frame</span>
|
||||
<select value={card.project_json?.frame?.preset || 'none'} onChange={(event) => updateCard({}, { frame: { preset: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{(editorOptions.frame_presets || []).map((preset) => <option key={preset.key} value={preset.key}>{preset.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={card.project_json?.frame?.preset || 'none'} onChange={(val) => updateCard({}, { frame: { preset: val } })} options={(editorOptions.frame_presets || []).map((p) => ({ value: p.key, label: p.label }))} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-1.5 block text-xs">Color grade</span>
|
||||
<select value={card.project_json?.effects?.color_grade || 'none'} onChange={(event) => updateCard({}, { effects: { color_grade: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{(editorOptions.color_grade_presets || []).map((preset) => <option key={preset.key} value={preset.key}>{preset.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block text-sm text-slate-300">
|
||||
<NovaSelect value={card.project_json?.effects?.color_grade || 'none'} onChange={(val) => updateCard({}, { effects: { color_grade: val } })} options={(editorOptions.color_grade_presets || []).map((p) => ({ value: p.key, label: p.label }))} />
|
||||
</div>
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-1.5 block text-xs">Effect</span>
|
||||
<select value={card.project_json?.effects?.effect_preset || 'none'} onChange={(event) => updateCard({}, { effects: { effect_preset: event.target.value } })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
{(editorOptions.effect_presets || []).map((preset) => <option key={preset.key} value={preset.key}>{preset.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={card.project_json?.effects?.effect_preset || 'none'} onChange={(val) => updateCard({}, { effects: { effect_preset: val } })} options={(editorOptions.effect_presets || []).map((p) => ({ value: p.key, label: p.label }))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1489,19 +1463,17 @@ export default function StudioCardEditor() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PUBLISH TAB */}
|
||||
{activeTab === 'publish' && (
|
||||
<div className="space-y-5">
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-2 block">Category</span>
|
||||
<select value={card.category_id || ''} onChange={(event) => updateCard({ category_id: event.target.value ? Number(event.target.value) : null })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
<option value="">Select category</option>
|
||||
{(editorOptions.categories || []).map((cat) => <option key={cat.id} value={cat.id}>{cat.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={String(card.category_id || '')} onChange={(val) => updateCard({ category_id: val ? Number(val) : null })} placeholder="Select category" options={(editorOptions.categories || []).map((c) => ({ value: String(c.id), label: c.name }))} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 text-sm text-slate-300">Visibility</div>
|
||||
@@ -1528,10 +1500,10 @@ export default function StudioCardEditor() {
|
||||
{ key: 'allow_export', label: 'Allow export' },
|
||||
{ key: 'allow_background_reuse', label: 'Allow background reuse' },
|
||||
].map(({ key, label }) => (
|
||||
<label key={key} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
|
||||
<div key={key} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
|
||||
<span>{label}</span>
|
||||
<input type="checkbox" checked={key === 'allow_export' ? Boolean(card.allow_export !== false) : Boolean(card[key])} onChange={(event) => updateCard({ [key]: event.target.checked })} className="h-4 w-4 rounded border-white/20 bg-[#0d1726]" />
|
||||
</label>
|
||||
<Checkbox checked={key === 'allow_export' ? Boolean(card.allow_export !== false) : Boolean(card[key])} onChange={(event) => updateCard({ [key]: event.target.checked })} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1541,22 +1513,16 @@ export default function StudioCardEditor() {
|
||||
</label>
|
||||
|
||||
{advancedMode && (
|
||||
<label className="block text-sm text-slate-300">
|
||||
<div className="block text-sm text-slate-300">
|
||||
<span className="mb-2 block">Style family</span>
|
||||
<select value={card.style_family || ''} onChange={(event) => updateCard({ style_family: event.target.value || null })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
<option value="">None</option>
|
||||
{(editorOptions.style_families || []).map((sf) => <option key={sf.key} value={sf.key}>{sf.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<NovaSelect value={card.style_family || ''} onChange={(val) => updateCard({ style_family: val || null })} placeholder="None" options={(editorOptions.style_families || []).map((sf) => ({ value: sf.key, label: sf.label }))} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Save to collection</div>
|
||||
<div className="flex gap-2">
|
||||
<select value={selectedCollectionId} onChange={(event) => setSelectedCollectionId(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
|
||||
<option value="">Default saved cards</option>
|
||||
{collections.map((collection) => <option key={collection.id} value={collection.id}>{collection.name}</option>)}
|
||||
</select>
|
||||
<NovaSelect value={String(selectedCollectionId || '')} onChange={(val) => setSelectedCollectionId(val)} placeholder="Default saved cards" options={collections.map((c) => ({ value: String(c.id), label: c.name }))} />
|
||||
<button type="button" onClick={saveToCollection} disabled={!cardId || busy} className="rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60">Save</button>
|
||||
</div>
|
||||
<button type="button" onClick={createCollection} className="mt-2 text-sm text-slate-400 transition hover:text-white">+ Create collection</button>
|
||||
|
||||
Reference in New Issue
Block a user