.relationships-section had card styling but lacked the margin-bottom + break-inside:avoid that .section-card has, so it sat flush against the Notes card below it and looked like one merged card (and could split across a multicol break). Add both to match section-card. Affects every asset detail page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
794 lines
20 KiB
Vue
794 lines
20 KiB
Vue
<template>
|
|
<div class="relationships-section">
|
|
<div class="section-header">
|
|
<h3 class="section-title">Relationships</h3>
|
|
<button
|
|
v-if="authStore.isAuthenticated"
|
|
class="btn btn-sm btn-primary"
|
|
@click="showAddModal = true"
|
|
>
|
|
Add Relationship
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">Loading relationships...</div>
|
|
|
|
<div v-else-if="!hasRelationships" class="empty-state">
|
|
No relationships defined.
|
|
</div>
|
|
|
|
<template v-else>
|
|
<!-- Symmetric connections: one direction-blind entry per peer -->
|
|
<div v-if="connectedItems.length > 0" class="relationship-group">
|
|
<h4 class="group-title">Connected</h4>
|
|
<div class="relationship-list">
|
|
<div
|
|
v-for="item in connectedItems"
|
|
:key="item.key"
|
|
class="relationship-item"
|
|
>
|
|
<div class="rel-icon"><component :is="getAssetIcon(item.peer?.assettypename || item.peer?.assettype)" :size="16" /></div>
|
|
<div class="rel-content">
|
|
<div class="rel-line">
|
|
<router-link :to="getAssetRoute(item.peer)" class="rel-name">
|
|
{{ item.peer?.name || item.peer?.assetnumber || 'Unknown' }}
|
|
</router-link>
|
|
<span class="badge" :style="colorStyle(colorForType(item.relationshiptypename))">{{ item.relationshiptypename }}</span>
|
|
</div>
|
|
<div class="rel-meta">
|
|
<span class="rel-type-badge">{{ item.peer?.assettypename || item.peer?.assettype }}</span>
|
|
</div>
|
|
<div v-if="item.notes" class="rel-notes">{{ item.notes }}</div>
|
|
</div>
|
|
<button
|
|
v-if="authStore.isAuthenticated"
|
|
class="btn-icon delete"
|
|
@click="deleteItem(item)"
|
|
title="Remove relationship"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Directional relationships: natural per-row phrasing, no jargon -->
|
|
<div v-if="directionalItems.length > 0" class="relationship-group">
|
|
<div class="relationship-list">
|
|
<div
|
|
v-for="item in directionalItems"
|
|
:key="item.key"
|
|
class="relationship-item"
|
|
>
|
|
<div class="rel-icon"><component :is="getAssetIcon(item.peer?.assettypename || item.peer?.assettype)" :size="16" /></div>
|
|
<div class="rel-content">
|
|
<div class="rel-line">
|
|
<template v-if="item.direction === 'outgoing'">
|
|
<span class="badge" :style="colorStyle(colorForType(item.relationshiptypename))">{{ item.relationshiptypename }}</span>
|
|
<span class="rel-arrow">-></span>
|
|
<router-link :to="getAssetRoute(item.peer)" class="rel-name">
|
|
{{ item.peer?.name || item.peer?.assetnumber || 'Unknown' }}
|
|
</router-link>
|
|
</template>
|
|
<template v-else>
|
|
<span class="rel-arrow"><-</span>
|
|
<span class="badge" :style="colorStyle(colorForType(item.relationshiptypename))">{{ item.relationshiptypename }}</span>
|
|
<span class="rel-from">from</span>
|
|
<router-link :to="getAssetRoute(item.peer)" class="rel-name">
|
|
{{ item.peer?.name || item.peer?.assetnumber || 'Unknown' }}
|
|
</router-link>
|
|
</template>
|
|
</div>
|
|
<div class="rel-meta">
|
|
<span class="rel-type-badge">{{ item.peer?.assettypename || item.peer?.assettype }}</span>
|
|
</div>
|
|
<div v-if="item.notes" class="rel-notes">{{ item.notes }}</div>
|
|
</div>
|
|
<button
|
|
v-if="authStore.isAuthenticated"
|
|
class="btn-icon delete"
|
|
@click="deleteItem(item)"
|
|
title="Remove relationship"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Add Relationship Modal -->
|
|
<div v-if="showAddModal" class="modal-overlay" @click.self="closeModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Add Relationship</h3>
|
|
<button class="btn-icon" @click="closeModal">×</button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>Direction</label>
|
|
<select v-model="newRel.direction" class="form-control">
|
|
<option value="outgoing">This asset -> Target asset</option>
|
|
<option value="incoming">Source asset -> This asset</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Relationship Type</label>
|
|
<select v-model="newRel.relationshiptypeid" class="form-control" required>
|
|
<option value="">Select type...</option>
|
|
<option
|
|
v-for="t in relationshipTypes"
|
|
:key="t.relationshiptypeid"
|
|
:value="t.relationshiptypeid"
|
|
>
|
|
{{ t.relationshiptype }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ newRel.direction === 'outgoing' ? 'Target Asset' : 'Source Asset' }}</label>
|
|
<div class="asset-search">
|
|
<input
|
|
type="text"
|
|
v-model="assetSearchQuery"
|
|
class="form-control"
|
|
placeholder="Search assets..."
|
|
@input="searchAssets"
|
|
/>
|
|
<div v-if="assetSearchResults.length > 0" class="search-results">
|
|
<div
|
|
v-for="asset in assetSearchResults"
|
|
:key="asset.assetid"
|
|
class="search-result-item"
|
|
:class="{ selected: newRel.targetAssetId === asset.assetid }"
|
|
@click="selectAsset(asset)"
|
|
>
|
|
<span class="result-icon">{{ getAssetIcon(asset.assettype) }}</span>
|
|
<span class="result-name">{{ asset.name || asset.assetnumber }}</span>
|
|
<span class="result-type">{{ asset.assettype }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="selectedAsset" class="selected-asset">
|
|
<span class="result-icon">{{ getAssetIcon(selectedAsset.assettype) }}</span>
|
|
<span>{{ selectedAsset.name || selectedAsset.assetnumber }}</span>
|
|
<button class="btn-icon" @click="clearSelectedAsset">×</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Notes (optional)</label>
|
|
<textarea
|
|
v-model="newRel.notes"
|
|
class="form-control"
|
|
rows="2"
|
|
placeholder="Additional notes about this relationship..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
<button
|
|
class="btn btn-primary"
|
|
:disabled="!canSave"
|
|
@click="saveRelationship"
|
|
>
|
|
Add Relationship
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
import { Cog, Monitor, Printer, Globe, Package, Ruler } from 'lucide-vue-next'
|
|
import { assetsApi, relationshipTypesApi } from '../api'
|
|
import { colorStyle } from '@/utils/colorStyle'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import { useToast } from '../composables/toast'
|
|
import { apiError } from '../utils/apiError'
|
|
const toast = useToast()
|
|
|
|
const props = defineProps({
|
|
assetid: {
|
|
type: Number,
|
|
default: null
|
|
},
|
|
// Alternative: lookup by machine/asset number
|
|
machinenumber: {
|
|
type: String,
|
|
default: null
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['updated'])
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
const loading = ref(true)
|
|
const resolvedAssetId = ref(null)
|
|
const lookupFailed = ref(false)
|
|
const outgoing = ref([])
|
|
const incoming = ref([])
|
|
const relationshipTypes = ref([])
|
|
|
|
function colorForType(name) {
|
|
const t = relationshipTypes.value.find(rt => rt.relationshiptype === name)
|
|
return t?.color || null
|
|
}
|
|
const showAddModal = ref(false)
|
|
|
|
// New relationship form
|
|
const newRel = ref({
|
|
direction: 'outgoing',
|
|
relationshiptypeid: '',
|
|
targetAssetId: null,
|
|
notes: ''
|
|
})
|
|
|
|
const assetSearchQuery = ref('')
|
|
const assetSearchResults = ref([])
|
|
const selectedAsset = ref(null)
|
|
let searchTimeout = null
|
|
|
|
const hasRelationships = computed(() => outgoing.value.length > 0 || incoming.value.length > 0)
|
|
|
|
// Symmetric types collapse to one entry per {peer, type} unordered pair. All
|
|
// stored direction rows (both directions, plus any legacy duplicates) fold
|
|
// into one displayed entry; its rowIds carries every collapsed relationshipid
|
|
// so a delete removes them all.
|
|
const connectedItems = computed(() => {
|
|
const byPair = new Map()
|
|
const rows = [
|
|
...outgoing.value.map(rel => ({ rel, peer: rel.targetasset })),
|
|
...incoming.value.map(rel => ({ rel, peer: rel.sourceasset })),
|
|
]
|
|
for (const { rel, peer } of rows) {
|
|
if (rel.isdirectional !== false) continue
|
|
const selfid = resolvedAssetId.value
|
|
const peerid = peer?.assetid
|
|
const lo = Math.min(selfid, peerid)
|
|
const hi = Math.max(selfid, peerid)
|
|
const key = `${rel.relationshiptypeid}:${lo}:${hi}`
|
|
const existing = byPair.get(key)
|
|
if (existing) {
|
|
existing.rowIds.push(rel.relationshipid)
|
|
if (!existing.notes && rel.notes) existing.notes = rel.notes
|
|
} else {
|
|
byPair.set(key, {
|
|
key,
|
|
rowIds: [rel.relationshipid],
|
|
peer,
|
|
relationshiptypeid: rel.relationshiptypeid,
|
|
relationshiptypename: rel.relationshiptypename,
|
|
notes: rel.notes || null,
|
|
})
|
|
}
|
|
}
|
|
return Array.from(byPair.values())
|
|
})
|
|
|
|
// Directional types keep one entry per stored row with arrow phrasing.
|
|
const directionalItems = computed(() => {
|
|
const items = []
|
|
for (const rel of outgoing.value) {
|
|
if (rel.isdirectional === false) continue
|
|
items.push({
|
|
key: `out-${rel.relationshipid}`,
|
|
rowIds: [rel.relationshipid],
|
|
peer: rel.targetasset,
|
|
direction: 'outgoing',
|
|
relationshiptypeid: rel.relationshiptypeid,
|
|
relationshiptypename: rel.relationshiptypename,
|
|
notes: rel.notes || null,
|
|
})
|
|
}
|
|
for (const rel of incoming.value) {
|
|
if (rel.isdirectional === false) continue
|
|
items.push({
|
|
key: `in-${rel.relationshipid}`,
|
|
rowIds: [rel.relationshipid],
|
|
peer: rel.sourceasset,
|
|
direction: 'incoming',
|
|
relationshiptypeid: rel.relationshiptypeid,
|
|
relationshiptypename: rel.relationshiptypename,
|
|
notes: rel.notes || null,
|
|
})
|
|
}
|
|
return items
|
|
})
|
|
|
|
const canSave = computed(() => {
|
|
return newRel.value.relationshiptypeid && newRel.value.targetAssetId && resolvedAssetId.value
|
|
})
|
|
|
|
onMounted(async () => {
|
|
await resolveAssetId()
|
|
if (resolvedAssetId.value) {
|
|
await Promise.all([loadRelationships(), loadRelationshipTypes()])
|
|
}
|
|
})
|
|
|
|
watch(() => props.assetid, async () => {
|
|
await resolveAssetId()
|
|
if (resolvedAssetId.value) {
|
|
await loadRelationships()
|
|
}
|
|
})
|
|
|
|
watch(() => props.machinenumber, async () => {
|
|
await resolveAssetId()
|
|
if (resolvedAssetId.value) {
|
|
await loadRelationships()
|
|
}
|
|
})
|
|
|
|
async function resolveAssetId() {
|
|
// If assetid is provided directly, use it
|
|
if (props.assetid) {
|
|
resolvedAssetId.value = props.assetid
|
|
lookupFailed.value = false
|
|
return
|
|
}
|
|
|
|
// Otherwise, try to look up by machine number
|
|
if (props.machinenumber) {
|
|
try {
|
|
const response = await assetsApi.lookup(props.machinenumber)
|
|
resolvedAssetId.value = response.data.data?.assetid
|
|
lookupFailed.value = !resolvedAssetId.value
|
|
} catch (error) {
|
|
resolvedAssetId.value = null
|
|
lookupFailed.value = true
|
|
loading.value = false
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadRelationships() {
|
|
if (!resolvedAssetId.value) return
|
|
|
|
loading.value = true
|
|
try {
|
|
const response = await assetsApi.getRelationships(resolvedAssetId.value)
|
|
outgoing.value = response.data.data?.outgoing || []
|
|
incoming.value = response.data.data?.incoming || []
|
|
} catch (error) {
|
|
console.error('Failed to load relationships:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadRelationshipTypes() {
|
|
try {
|
|
const response = await relationshipTypesApi.list()
|
|
relationshipTypes.value = response.data.data || []
|
|
} catch (error) {
|
|
console.error('Failed to load relationship types:', error)
|
|
}
|
|
}
|
|
|
|
function searchAssets() {
|
|
clearTimeout(searchTimeout)
|
|
if (!assetSearchQuery.value || assetSearchQuery.value.length < 2) {
|
|
assetSearchResults.value = []
|
|
return
|
|
}
|
|
|
|
searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await assetsApi.search(assetSearchQuery.value, { perpage: 10 })
|
|
// Filter out the current asset
|
|
assetSearchResults.value = (response.data.data || []).filter(
|
|
a => a.assetid !== resolvedAssetId.value
|
|
)
|
|
} catch (error) {
|
|
console.error('Failed to search assets:', error)
|
|
}
|
|
}, 300)
|
|
}
|
|
|
|
function selectAsset(asset) {
|
|
selectedAsset.value = asset
|
|
newRel.value.targetAssetId = asset.assetid
|
|
assetSearchQuery.value = ''
|
|
assetSearchResults.value = []
|
|
}
|
|
|
|
function clearSelectedAsset() {
|
|
selectedAsset.value = null
|
|
newRel.value.targetAssetId = null
|
|
}
|
|
|
|
async function saveRelationship() {
|
|
if (!canSave.value) return
|
|
|
|
try {
|
|
const data = {
|
|
relationshiptypeid: parseInt(newRel.value.relationshiptypeid),
|
|
notes: newRel.value.notes || null
|
|
}
|
|
|
|
if (newRel.value.direction === 'outgoing') {
|
|
data.sourceassetid = resolvedAssetId.value
|
|
data.targetassetid = newRel.value.targetAssetId
|
|
} else {
|
|
data.sourceassetid = newRel.value.targetAssetId
|
|
data.targetassetid = resolvedAssetId.value
|
|
}
|
|
|
|
await assetsApi.createRelationship(data)
|
|
await loadRelationships()
|
|
closeModal()
|
|
emit('updated')
|
|
} catch (error) {
|
|
console.error('Failed to create relationship:', error)
|
|
toast.error(apiError(error, 'Failed to create relationship'))
|
|
}
|
|
}
|
|
|
|
// Delete every collapsed direction row behind a displayed entry.
|
|
async function deleteItem(item) {
|
|
if (!confirm('Remove this relationship?')) return
|
|
|
|
try {
|
|
await Promise.all(item.rowIds.map(id => assetsApi.deleteRelationship(id)))
|
|
await loadRelationships()
|
|
emit('updated')
|
|
} catch (error) {
|
|
console.error('Failed to delete relationship:', error)
|
|
toast.error('Failed to delete relationship')
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
showAddModal.value = false
|
|
newRel.value = {
|
|
direction: 'outgoing',
|
|
relationshiptypeid: '',
|
|
targetAssetId: null,
|
|
notes: ''
|
|
}
|
|
selectedAsset.value = null
|
|
assetSearchQuery.value = ''
|
|
assetSearchResults.value = []
|
|
}
|
|
|
|
function getAssetIcon(assettype) {
|
|
const icons = {
|
|
'machine': Cog,
|
|
'computer': Monitor,
|
|
'printer': Printer,
|
|
'network_device': Globe,
|
|
'measuring_tool': Ruler
|
|
}
|
|
return icons[assettype] || Package
|
|
}
|
|
|
|
function getAssetRoute(asset) {
|
|
if (!asset) return '#'
|
|
|
|
const routeMap = {
|
|
'machine': '/machines',
|
|
'computer': '/pcs',
|
|
'printer': '/printers',
|
|
'network_device': '/network',
|
|
'measuring_tool': '/measuringtools'
|
|
}
|
|
|
|
// serializer sends assettypename; old shape used assettype
|
|
const typename = asset.assettypename || asset.assettype
|
|
const basePath = routeMap[typename] || '/assets'
|
|
|
|
// pluginid is the extension-table id the detail routes key on
|
|
const id = asset.pluginid
|
|
|| asset.typedata?.networkdeviceid
|
|
|| asset.typedata?.machineid
|
|
|| asset.assetid
|
|
return `${basePath}/${id}`
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.relationships-section {
|
|
background: var(--bg-card);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
padding: 1.25rem;
|
|
/* Match .section-card spacing so relationships is its own card with a gap
|
|
below (not visually merged with the Notes card) and does not split across
|
|
a multicol break. */
|
|
margin-bottom: 25px;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.section-title {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.empty-state {
|
|
color: var(--text-light);
|
|
font-style: italic;
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
.relationship-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.relationship-group:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.group-title {
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
color: var(--text-light);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin: 0 0 0.75rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.relationship-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.relationship-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
background: var(--bg);
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.rel-icon {
|
|
font-size: 1.25rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.rel-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.rel-name {
|
|
font-weight: 500;
|
|
color: var(--link);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.rel-name:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.rel-line {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.rel-arrow {
|
|
font-family: monospace;
|
|
font-weight: 600;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.rel-from {
|
|
font-size: 0.8rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.rel-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.badge-outline {
|
|
background: transparent;
|
|
border: 1px solid var(--primary);
|
|
color: var(--primary);
|
|
font-size: 0.7rem;
|
|
padding: 0.15rem 0.4rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.rel-type-badge {
|
|
font-size: 0.75rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.rel-notes {
|
|
font-size: 0.8rem;
|
|
color: var(--text-light);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.btn-icon.delete {
|
|
background: none;
|
|
border: none;
|
|
color: var(--danger);
|
|
font-size: 1.25rem;
|
|
cursor: pointer;
|
|
padding: 0.25rem;
|
|
line-height: 1;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.btn-icon.delete:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--bg-card-solid);
|
|
border-radius: 12px;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
max-height: 90vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.25rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.modal-header h3 {
|
|
margin: 0;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.modal-header .btn-icon {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
color: var(--text-light);
|
|
line-height: 1;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.25rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
padding: 1rem 1.25rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.form-group:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Asset Search */
|
|
.asset-search {
|
|
position: relative;
|
|
}
|
|
|
|
.search-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg-card-solid);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 10;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.search-result-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.6rem 0.75rem;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.search-result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.search-result-item:hover {
|
|
background: var(--bg);
|
|
}
|
|
|
|
.search-result-item.selected {
|
|
background: rgba(65, 129, 255, 0.15);
|
|
}
|
|
|
|
.result-icon {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.result-name {
|
|
flex: 1;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.result-type {
|
|
font-size: 0.75rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.selected-asset {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
padding: 0.5rem 0.75rem;
|
|
background: rgba(65, 129, 255, 0.1);
|
|
border: 1px solid var(--primary);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.selected-asset .btn-icon {
|
|
margin-left: auto;
|
|
background: none;
|
|
border: none;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
color: var(--text-light);
|
|
}
|
|
</style>
|