Spaces:
Sleeping
Sleeping
| // ExtractionCard.jsx | |
| import SafetyCard from './SafetyCard' | |
| import { useState, useEffect } from 'react' | |
| let _fieldCounter = 0 | |
| const DEFAULT_SCHEMA = { | |
| patient: { name: null, age: null, sex: null }, | |
| diagnoses: [], | |
| history: { medical: [], surgical: [] }, | |
| medications: [], | |
| allergies: [], | |
| vital_signs: { blood_pressure: null, heart_rate: null, temperature: null, oxygen_saturation: null }, | |
| laboratory: [], | |
| treatment_ordered: [], | |
| pending_results: [], | |
| } | |
| function withId(item) { | |
| return { ...item, _id: crypto.randomUUID() } | |
| } | |
| function mergeWithSchema(llmOutput) { | |
| const result = structuredJSON(DEFAULT_SCHEMA) | |
| if (llmOutput.patient) result.patient = { ...result.patient, ...llmOutput.patient } | |
| if (llmOutput.vital_signs) result.vital_signs = { ...llmOutput.vital_signs } | |
| if (llmOutput.history) result.history = { ...result.history, ...llmOutput.history } | |
| if (llmOutput.diagnoses?.length > 0) result.diagnoses = llmOutput.diagnoses.filter(Boolean).map(withId) | |
| if (llmOutput.allergies?.length > 0) result.allergies = llmOutput.allergies.filter(Boolean).map(withId) | |
| if (llmOutput.medications?.length > 0) result.medications = llmOutput.medications.filter(Boolean).map(withId) | |
| if (llmOutput.laboratory?.length > 0) result.laboratory = llmOutput.laboratory.filter(Boolean).map(withId) | |
| if (llmOutput.treatment_ordered?.length > 0) result.treatment_ordered = llmOutput.treatment_ordered.filter(Boolean).map(withId) | |
| if (llmOutput.pending_results?.length > 0) result.pending_results = llmOutput.pending_results.filter(Boolean).map((r) => | |
| typeof r === 'object' ? withId(r) : { _id: crypto.randomUUID(), value: r } | |
| ) | |
| if (llmOutput.history?.medical?.length > 0) result.history.medical = llmOutput.history.medical.map((m, i) => ({ _id: crypto.randomUUID(), value: m })) | |
| if (llmOutput.history?.surgical?.length > 0) result.history.surgical = llmOutput.history.surgical.map((s) => ({ _id: crypto.randomUUID(), value: s })) | |
| return result | |
| } | |
| function parseStructuredInfo(structuredInfo) { | |
| if (typeof structuredInfo === 'object' && structuredInfo !== null) { | |
| return mergeWithSchema(structuredInfo) | |
| } | |
| try { | |
| const cleaned = structuredInfo | |
| .replace(/```json|```/g, '') | |
| .replace(/,\s*}/g, '}') | |
| .replace(/,\s*]/g, ']') | |
| .trim() | |
| return mergeWithSchema(JSON.parse(cleaned)) | |
| } catch (e) { | |
| console.log('parse error:', e.message) | |
| return DEFAULT_SCHEMA | |
| } | |
| } | |
| const PATIENT_KEYS = ['name', 'age', 'sex'] | |
| const VITALSIGNS_LABELS = { | |
| blood_pressure: 'Blutdruck', | |
| heart_rate: 'Herzfrequenz', | |
| temperature: 'Temperatur', | |
| oxygen_saturation: 'SpO2', | |
| } | |
| export default function ExtractionCard({ structuredInfo, safetyFlags, onConfirm, loading }) { | |
| const [edited, setEdited] = useState(() => parseStructuredInfo(structuredInfo)) | |
| const [collapsed, setCollapsed] = useState(false) | |
| const [confirmed, setConfirmed] = useState(false) | |
| const [sectionsCollapsed, setSectionsCollapsed] = useState({}) | |
| function toggleSection(name) { | |
| setSectionsCollapsed(prev => ({ ...prev, [name]: !prev[name] })) | |
| } | |
| useEffect(() => { | |
| setEdited(parseStructuredInfo(structuredInfo)) | |
| }, [structuredInfo]) | |
| function update(path, value) { | |
| setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| setNestedValue(next, path, value) | |
| return next | |
| }) | |
| } | |
| function addArrayItem(section, template) { | |
| setEdited(prev => ({ | |
| ...structuredJSON(prev), | |
| [section]: [...(prev[section] ?? []), { ...structuredJSON(template), _id: crypto.randomUUID() }] | |
| })) | |
| } | |
| function removeArrayItem(section, id) { | |
| setEdited(prev => ({ | |
| ...structuredJSON(prev), | |
| [section]: prev[section].filter(item => item._id !== id) | |
| })) | |
| } | |
| function addScalarField(section) { | |
| const n = ++_fieldCounter | |
| setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| next[section][`Feldname ${n}`] = null | |
| return next | |
| }) | |
| } | |
| function removeScalarField(section, key) { | |
| setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| delete next[section][key] | |
| return next | |
| }) | |
| } | |
| function renameScalarField(section, oldKey, newKey) { | |
| if (!newKey || newKey === oldKey) return | |
| setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| next[section] = Object.fromEntries( | |
| Object.entries(next[section]).map(([k, v]) => [k === oldKey ? newKey : k, v]) | |
| ) | |
| return next | |
| }) | |
| } | |
| function handleConfirm() { | |
| setConfirmed(true) | |
| const clean = structuredJSON(edited) | |
| const stripIds = arr => arr?.map(({ _id, ...rest }) => rest) ?? [] | |
| clean.diagnoses = stripIds(clean.diagnoses) | |
| clean.allergies = stripIds(clean.allergies) | |
| clean.medications = stripIds(clean.medications) | |
| clean.laboratory = stripIds(clean.laboratory) | |
| clean.treatment_ordered = stripIds(clean.treatment_ordered) | |
| clean.pending_results = clean.pending_results?.map(r => r.value ?? r) ?? [] | |
| clean.history.medical = clean.history.medical?.map(r => r.value ?? r) ?? [] | |
| clean.history.surgical = clean.history.surgical?.map(r => r.value ?? r) ?? [] | |
| onConfirm(clean) | |
| } | |
| return ( | |
| <div style={s.card}> | |
| <div style={s.header} onClick={() => setCollapsed(c => !c)}> | |
| <div style={{ display:'flex', alignItems:'center', gap:8 }}> | |
| <span style={s.title}>Strukturierte Patientendaten</span> | |
| {confirmed && <span style={s.confirmedBadge}>bestätigt</span>} | |
| </div> | |
| <span style={s.chevron}>{collapsed ? '▶' : '▼'}</span> | |
| </div> | |
| {!collapsed && ( | |
| <> | |
| <div style={s.body}> | |
| {/* Patient */} | |
| <SectionHeader label="Patient" addLabel="+ Feld" onAdd={() => addScalarField('patient')} collapsed={sectionsCollapsed['patient']} onToggle={() => toggleSection('patient')} /> | |
| {!sectionsCollapsed['patient'] && ( | |
| <div style={s.singleSub}> | |
| <div style={s.twoCol}> | |
| <Field label="Name" value={edited.patient?.name} onSave={v => update('patient.name', v)} /> | |
| <Field label="Alter" value={edited.patient?.age} onSave={v => update('patient.age', v)} /> | |
| <Field label="Geschlecht" value={edited.patient?.sex} onSave={v => update('patient.sex', v)} /> | |
| {Object.entries(edited.patient ?? {}) | |
| .filter(([k]) => !PATIENT_KEYS.includes(k)) | |
| .map(([k, v]) => ( | |
| <Field | |
| key={k} | |
| label={k} | |
| value={v} | |
| editableLabel | |
| onSave={val => update(`patient.${k}`, val)} | |
| onLabelSave={newKey => renameScalarField('patient', k, newKey)} | |
| onRemove={() => removeScalarField('patient', k)} | |
| /> | |
| )) | |
| } | |
| </div> | |
| </div> | |
| )} | |
| {/* Anamnese */} | |
| <SectionHeader | |
| label="Anamnese" | |
| collapsed={sectionsCollapsed['history']} | |
| onToggle={() => toggleSection('history')} | |
| customActions={ | |
| <> | |
| <span style={s.addBtn} onClick={() => setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| next.history.medical = [...(next.history.medical ?? []), { _id: crypto.randomUUID(), value: '' }] | |
| return next | |
| })}>+ Medizinisch</span> | |
| <span style={s.addBtn} onClick={() => setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| next.history.surgical = [...(next.history.surgical ?? []), { _id: crypto.randomUUID(), value: '' }] | |
| return next | |
| })}>+ Chirurgisch</span> | |
| </> | |
| } | |
| /> | |
| {!sectionsCollapsed['history'] && ( | |
| !edited.history?.medical?.length && !edited.history?.surgical?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.history?.medical?.map((m) => ( | |
| <SubCard key={m._id} onRemove={() => setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| next.history.medical = next.history.medical.filter(x => x._id !== m._id) | |
| return next | |
| })}> | |
| <Field | |
| label="Medizinisch" | |
| value={typeof m === 'object' ? (m.value ?? JSON.stringify(m)) : m} | |
| onSave={v => setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| const idx = next.history.medical.findIndex(x => x._id === m._id) | |
| if (idx !== -1) next.history.medical[idx].value = v | |
| return next | |
| })} | |
| /> | |
| </SubCard> | |
| ))} | |
| {edited.history?.surgical?.map((s2) => ( | |
| <SubCard key={s2._id} onRemove={() => setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| next.history.surgical = next.history.surgical.filter(x => x._id !== s2._id) | |
| return next | |
| })}> | |
| <Field | |
| label="Chirurgisch" | |
| value={typeof s2 === 'object' ? (s2.value ?? JSON.stringify(s2)) : s2} | |
| onSave={v => setEdited(prev => { | |
| const next = structuredJSON(prev) | |
| const idx = next.history.surgical.findIndex(x => x._id === s2._id) | |
| if (idx !== -1) next.history.surgical[idx].value = v | |
| return next | |
| })} | |
| /> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {/* Diagnosen */} | |
| <SectionHeader label="Diagnosen" addLabel="+ Diagnose" onAdd={() => addArrayItem('diagnoses', { name: '', certainty: 'confirmed' })} collapsed={sectionsCollapsed['diagnoses']} onToggle={() => toggleSection('diagnoses')} /> | |
| {!sectionsCollapsed['diagnoses'] && ( | |
| !edited.diagnoses?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.diagnoses.map((d, i) => ( | |
| <SubCard key={d._id} onRemove={() => removeArrayItem('diagnoses', d._id)}> | |
| <Field label="Name" value={d.name?.value ?? d.name} onSave={v => update(`diagnoses.${i}.name`, v)} /> | |
| <div style={s.field}> | |
| <span style={s.fieldLabel}>Sicherheit</span> | |
| <select | |
| style={s.fieldSelect} | |
| value={d.certainty ?? 'confirmed'} | |
| onChange={e => update(`diagnoses.${i}.certainty`, e.target.value)} | |
| > | |
| <option value="confirmed">bestätigt</option> | |
| <option value="suspected">vermutet</option> | |
| </select> | |
| </div> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {/* Allergien */} | |
| <SectionHeader label="Allergien & Unverträglichkeiten" addLabel="+ Allergie" onAdd={() => addArrayItem('allergies', { substance: '', reaction: '' })} collapsed={sectionsCollapsed['allergies']} onToggle={() => toggleSection('allergies')} /> | |
| {!sectionsCollapsed['allergies'] && ( | |
| !edited.allergies?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.allergies.map((a, i) => ( | |
| <SubCard key={a._id} onRemove={() => removeArrayItem('allergies', a._id)}> | |
| <Field label="Substanz" value={a.substance} onSave={v => update(`allergies.${i}.substance`, v)} /> | |
| <Field label="Reaktion" value={a.reaction} onSave={v => update(`allergies.${i}.reaction`, v)} /> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {/* Medikation */} | |
| <SectionHeader label="Medikation" addLabel="+ Medikament" onAdd={() => addArrayItem('medications', { name: '', dose: '', regimen: '' })} collapsed={sectionsCollapsed['medications']} onToggle={() => toggleSection('medications')} /> | |
| {!sectionsCollapsed['medications'] && ( | |
| !edited.medications?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.medications.map((m, i) => ( | |
| <SubCard key={m._id} onRemove={() => removeArrayItem('medications', m._id)}> | |
| <Field label="Name" value={m.name} onSave={v => update(`medications.${i}.name`, v)} /> | |
| <Field label="Dosis" value={m.dose} onSave={v => update(`medications.${i}.dose`, v)} /> | |
| <Field label="Einnahme" value={m.regimen} onSave={v => update(`medications.${i}.regimen`, v)} /> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {/* Vitalwerte */} | |
| <SectionHeader label="Vitalwerte" addLabel="+ Feld" onAdd={() => addScalarField('vital_signs')} collapsed={sectionsCollapsed['vital_signs']} onToggle={() => toggleSection('vital_signs')} /> | |
| {!sectionsCollapsed['vital_signs'] && ( | |
| <div style={s.singleSub}> | |
| <div style={s.twoCol}> | |
| {Object.entries(edited.vital_signs ?? {}).map(([k, v]) => { | |
| const isKnown = k in VITALSIGNS_LABELS | |
| return ( | |
| <Field | |
| key={k} | |
| label={isKnown ? VITALSIGNS_LABELS[k] : k} | |
| value={v} | |
| editableLabel={!isKnown} | |
| onSave={val => update(`vital_signs.${k}`, val)} | |
| onLabelSave={newKey => renameScalarField('vital_signs', k, newKey)} | |
| onRemove={() => removeScalarField('vital_signs', k)} | |
| /> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| {/* Laborwerte */} | |
| <SectionHeader label="Laborwerte" addLabel="+ Labor" onAdd={() => addArrayItem('laboratory', { name: '', value: '', unit: '', abnormal: false })} collapsed={sectionsCollapsed['laboratory']} onToggle={() => toggleSection('laboratory')} /> | |
| {!sectionsCollapsed['laboratory'] && ( | |
| !edited.laboratory?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.laboratory.map((l, i) => ( | |
| <SubCard key={l._id} onRemove={() => removeArrayItem('laboratory', l._id)} conflict={l.abnormal === true}> | |
| <Field label="Name" value={l.name} onSave={v => update(`laboratory.${i}.name`, v)} /> | |
| <Field label="Wert" value={l.value !== null ? `${l.value ?? ''} ${l.unit ?? ''}`.trim() : null} conflict={l.abnormal === true} onSave={v => update(`laboratory.${i}.value`, v)} /> | |
| <div style={{ ...s.field, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> | |
| <span style={s.fieldLabel}>Abnormal</span> | |
| <input type="checkbox" checked={!!l.abnormal} onChange={e => update(`laboratory.${i}.abnormal`, e.target.checked)} style={{ cursor: 'pointer' }} /> | |
| </div> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {/* Verordnete Behandlung */} | |
| <SectionHeader label="Verordnete Behandlung" addLabel="+ Behandlung" onAdd={() => addArrayItem('treatment_ordered', { name: '', dose: '', route: '' })} collapsed={sectionsCollapsed['treatment_ordered']} onToggle={() => toggleSection('treatment_ordered')} /> | |
| {!sectionsCollapsed['treatment_ordered'] && ( | |
| !edited.treatment_ordered?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.treatment_ordered.map((t, i) => ( | |
| <SubCard key={t._id} onRemove={() => removeArrayItem('treatment_ordered', t._id)}> | |
| <Field label="Name" value={t.name} onSave={v => update(`treatment_ordered.${i}.name`, v)} /> | |
| <Field label="Dosis" value={t.dose} onSave={v => update(`treatment_ordered.${i}.dose`, v)} /> | |
| <Field label="Route" value={t.route} onSave={v => update(`treatment_ordered.${i}.route`, v)} /> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {/* Ausstehende Ergebnisse */} | |
| <SectionHeader label="Ausstehende Ergebnisse" addLabel="+ Ergebnis" onAdd={() => addArrayItem('pending_results', { value: '' })} collapsed={sectionsCollapsed['pending_results']} onToggle={() => toggleSection('pending_results')} /> | |
| {!sectionsCollapsed['pending_results'] && ( | |
| !edited.pending_results?.length | |
| ? <Empty /> | |
| : <div style={s.subCardGrid}> | |
| {edited.pending_results.map((r, i) => ( | |
| <SubCard key={r._id ?? i} onRemove={() => removeArrayItem('pending_results', r._id)}> | |
| <Field | |
| label={`Ergebnis ${i + 1}`} | |
| value={typeof r === 'object' ? (r.value ?? JSON.stringify(r)) : r} | |
| onSave={v => update(`pending_results.${i}.value`, v)} | |
| /> | |
| </SubCard> | |
| ))} | |
| </div> | |
| )} | |
| {safetyFlags && safetyFlags.length > 0 && ( | |
| <div style={{ marginTop: 14 }}> | |
| <SafetyCard flags={safetyFlags} /> | |
| </div> | |
| )} | |
| </div> | |
| <div style={s.confirmStrip}> | |
| <span style={s.confirmHint}>Extraktion korrekt? Werte anklicken zum Bearbeiten.</span> | |
| <button | |
| style={{ ...s.btn, opacity: loading ? 0.6 : 1 }} | |
| disabled={!!loading} | |
| onClick={handleConfirm} | |
| > | |
| Weiter zur Dokumentenzusammenfassung → | |
| </button> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ) | |
| } | |
| // Sub-components | |
| function SubCard({ children, onRemove, conflict }) { | |
| return ( | |
| <div style={{ | |
| ...s.subCard, | |
| ...(conflict ? { borderLeft: '2px solid #EF4444' } : {}), | |
| position: 'relative', | |
| paddingTop: 14, | |
| }}> | |
| <span style={{ ...s.subCardRemove, position: 'absolute', top: 4, right: 6 }} onClick={onRemove}>✕</span> | |
| {children} | |
| </div> | |
| ) | |
| } | |
| function SectionHeader({ label, addLabel, onAdd, customActions, collapsed, onToggle }) { | |
| return ( | |
| <div style={s.sectionHeader} onClick={onToggle} title={collapsed ? 'Aufklappen' : 'Einklappen'}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}> | |
| <span style={{ fontSize: 8, color: '#A09D94' }}>{collapsed ? '▶' : '▼'}</span> | |
| <span style={s.sectionLabel}>{label}</span> | |
| </div> | |
| <div style={{ display: 'flex', gap: 8 }} onClick={e => e.stopPropagation()}> | |
| {customActions} | |
| {onAdd && <span style={s.addBtn} onClick={onAdd}>{addLabel ?? '+ Feld'}</span>} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function Empty() { | |
| return <div style={s.empty}>nicht erwähnt</div> | |
| } | |
| function Field({ label, value, onSave, onRemove, conflict, editableLabel, onLabelSave }) { | |
| const [editingVal, setEditingVal] = useState(false) | |
| const [editingLabel, setEditingLabel] = useState(false) | |
| const [val, setVal] = useState(value ?? '') | |
| const [lbl, setLbl] = useState(label ?? '') | |
| const isEmpty = val === '' || val === null || val === undefined || val === 'null' | |
| function saveVal() { | |
| setEditingVal(false) | |
| onSave(val) | |
| } | |
| function saveLabel() { | |
| setEditingLabel(false) | |
| if (lbl !== label) onLabelSave?.(lbl) | |
| } | |
| return ( | |
| <div style={s.field}> | |
| <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> | |
| {editableLabel && editingLabel ? ( | |
| <input | |
| style={s.labelInput} | |
| value={lbl} | |
| autoFocus | |
| onChange={e => setLbl(e.target.value)} | |
| onBlur={saveLabel} | |
| onKeyDown={e => e.key === 'Enter' && saveLabel()} | |
| /> | |
| ) : ( | |
| <span | |
| style={{ ...s.fieldLabel, ...(editableLabel ? { cursor: 'pointer', borderBottom: '0.5px dashed #C8C6BC' } : {}) }} | |
| onClick={() => editableLabel && setEditingLabel(true)} | |
| > | |
| {label ?? '—'} | |
| </span> | |
| )} | |
| {onRemove && <span style={s.fieldRemove} onClick={onRemove}>✕</span>} | |
| </div> | |
| {editingVal ? ( | |
| <input | |
| style={s.fieldInput} | |
| value={val} | |
| autoFocus | |
| onChange={e => setVal(e.target.value)} | |
| onBlur={saveVal} | |
| onKeyDown={e => e.key === 'Enter' && saveVal()} | |
| /> | |
| ) : ( | |
| <span | |
| style={{ ...s.fieldValue, ...(conflict ? s.conflict : {}), ...(isEmpty ? s.missing : {}) }} | |
| onClick={() => setEditingVal(true)} | |
| title="Klicken zum Bearbeiten" | |
| > | |
| {isEmpty ? 'nicht erwähnt' : val} | |
| </span> | |
| )} | |
| </div> | |
| ) | |
| } | |
| // Helpers | |
| function structuredJSON(obj) { | |
| return JSON.parse(JSON.stringify(obj)) | |
| } | |
| function setNestedValue(obj, path, value) { | |
| const keys = path.split('.') | |
| let current = obj | |
| for (let i = 0; i < keys.length - 1; i++) { | |
| const key = isNaN(keys[i]) ? keys[i] : parseInt(keys[i]) | |
| current = current[key] | |
| } | |
| const lastKey = isNaN(keys[keys.length - 1]) ? keys[keys.length - 1] : parseInt(keys[keys.length - 1]) | |
| current[lastKey] = value | |
| } | |
| // Styles | |
| const s = { | |
| card: { borderRadius: 10, border: '0.5px solid #E2E0D8', background: '#fff', overflow: 'hidden', display: 'flex', flexDirection: 'column', maxHeight: '80vh' }, | |
| header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', cursor: 'pointer', userSelect: 'none' }, | |
| title: { fontSize: 13, fontWeight: 500 }, | |
| chevron: { fontSize: 10, color: '#A09D94' }, | |
| body: { padding: '12px 14px', borderTop: '0.5px solid #E2E0D8', overflowY: 'auto', flex: 1 }, | |
| sectionHeader: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '12px 0 6px' }, | |
| sectionLabel: { fontSize: 10, fontWeight: 500, letterSpacing: '0.06em', textTransform: 'uppercase', color: '#A09D94' }, | |
| addBtn: { fontSize: 10, color: '#378ADD', cursor: 'pointer', userSelect: 'none' }, | |
| subCardList: { display: 'flex', flexDirection: 'column', gap: 6 }, | |
| subCardGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }, | |
| subCard: { background: '#F7F6F3', border: '0.5px solid #E2E0D8', borderRadius: 8, padding: '6px 10px' }, | |
| subCardRemove: { fontSize: 10, color: '#A09D94', cursor: 'pointer', lineHeight: 1 }, | |
| singleSub: { background: '#F7F6F3', border: '0.5px solid #E2E0D8', borderRadius: 8, padding: '6px 10px' }, | |
| twoCol: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 12px' }, | |
| empty: { fontSize: 11, color: '#A09D94', fontStyle: 'italic', padding: '4px 0' }, | |
| field: { display: 'flex', flexDirection: 'column', gap: 1, padding: '3px 0', borderBottom: '0.5px solid #E2E0D8' }, | |
| fieldLabel: { fontSize: 9, fontWeight: 500, letterSpacing: '0.04em', textTransform: 'uppercase', color: '#A09D94' }, | |
| fieldValue: { fontSize: 12, color: '#1A1916', cursor: 'pointer', padding: '1px 2px', borderRadius: 3 }, | |
| fieldSelect: { fontSize: 11, border: 'none', background: 'transparent', color: '#1A1916', cursor: 'pointer', fontFamily: 'inherit', outline: 'none', padding: '1px 2px' }, | |
| fieldInput: { fontSize: 12, border: '0.5px solid #378ADD', borderRadius: 4, padding: '1px 6px', outline: 'none', width: '100%' }, | |
| labelInput: { fontSize: 9, border: 'none', borderBottom: '0.5px dashed #C8C6BC', outline: 'none', background: 'transparent', color: '#A09D94', fontFamily: 'inherit', width: '80px' }, | |
| fieldRemove: { fontSize: 9, color: '#A09D94', cursor: 'pointer', padding: '0 2px', opacity: 0.6 }, | |
| conflict: { color: '#B91C1C' }, | |
| missing: { color: '#A09D94', fontStyle: 'italic' }, | |
| confirmStrip: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', borderTop: '0.5px solid #E2E0D8', background: '#F7F6F3', flexShrink: 0 }, | |
| confirmHint: { fontSize: 11, color: '#6B6960' }, | |
| btn: { fontFamily: 'inherit', fontSize: 11, padding: '4px 12px', borderRadius: 6, border: '0.5px solid #1A1916', background: '#1A1916', color: '#fff', cursor: 'pointer' }, | |
| confirmedBadge:{ fontSize:10, fontWeight:500, padding:'1px 7px', borderRadius:99, background:'#F0FDF4', color:'#166534', border:'0.5px solid #BBF7D0' }, | |
| } |