All checks were successful
Test Asgard Runner / test (push) Successful in 2s
10 plugin-config views (LootBuilder, RaidableBases, Teleport, Kits, Gather, AutoDoors, FurnaceSplitter, BetterChat, TimedExecute, PluginConfigs landing) + 5 child components (loot sidebar/item-editor/group-editor/item-picker, teleport PermissionGroupEditor) re-skinned onto DS components + tokens. All config logic preserved (path-traversal get/set, apply-to-server, import-from-server, CRUD, multiplier logic, per-store status derivation). Presentation-only. Build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
194 lines
4.5 KiB
Vue
194 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { rustContainers, containerCategories } from '@/data/rust-containers'
|
|
import Icon from '@/components/ds/core/Icon.vue'
|
|
import DsInput from '@/components/ds/forms/Input.vue'
|
|
|
|
const props = defineProps<{
|
|
lootTable: Record<string, any>
|
|
selected: string | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
select: [prefab: string]
|
|
}>()
|
|
|
|
const searchQuery = ref('')
|
|
|
|
// Map container categories to DS icon names
|
|
const categoryIcons: Record<string, string> = {
|
|
crates: 'box',
|
|
barrels: 'flask-conical',
|
|
military: 'shield',
|
|
npcs: 'users',
|
|
other: 'info',
|
|
}
|
|
|
|
const categoryLabels: Record<string, string> = {
|
|
crates: 'Crates',
|
|
barrels: 'Barrels',
|
|
military: 'Military',
|
|
npcs: 'NPCs',
|
|
other: 'Other',
|
|
}
|
|
|
|
const filteredContainers = computed(() => {
|
|
const q = searchQuery.value.toLowerCase()
|
|
if (!q) return rustContainers
|
|
return rustContainers.filter(c => c.name.toLowerCase().includes(q) || c.prefab.toLowerCase().includes(q))
|
|
})
|
|
|
|
const groupedContainers = computed(() => {
|
|
const groups: Record<string, typeof rustContainers> = {}
|
|
for (const cat of containerCategories) {
|
|
const items = filteredContainers.value.filter(c => c.category === cat)
|
|
if (items.length > 0) groups[cat] = items
|
|
}
|
|
return groups
|
|
})
|
|
|
|
function isConfigured(prefab: string): boolean {
|
|
const entry = props.lootTable[prefab]
|
|
if (!entry) return false
|
|
const hasItems = entry.UngroupedItems && Object.keys(entry.UngroupedItems).length > 0
|
|
const hasGuaranteed = entry.GuaranteedItems && Object.keys(entry.GuaranteedItems).length > 0
|
|
const hasProfiles = entry.LootProfiles && entry.LootProfiles.length > 0
|
|
return hasItems || hasGuaranteed || hasProfiles
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<aside class="lcs-root">
|
|
<!-- Search -->
|
|
<div class="lcs-search">
|
|
<DsInput
|
|
v-model="searchQuery"
|
|
icon="search"
|
|
placeholder="Search containers…"
|
|
size="sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Container list -->
|
|
<div class="lcs-list">
|
|
<template v-for="(containers, category) in groupedContainers" :key="category">
|
|
<!-- Category heading -->
|
|
<div class="lcs-cat">
|
|
<Icon
|
|
:name="categoryIcons[category] ?? 'box'"
|
|
:size="12"
|
|
class="lcs-cat__icon"
|
|
/>
|
|
<span class="lcs-cat__label">{{ categoryLabels[category] ?? category }}</span>
|
|
</div>
|
|
|
|
<!-- Container rows -->
|
|
<button
|
|
v-for="c in containers"
|
|
:key="c.prefab"
|
|
class="lcs-item"
|
|
:class="{ 'lcs-item--active': selected === c.prefab }"
|
|
@click="emit('select', c.prefab)"
|
|
>
|
|
<span class="lcs-item__name">{{ c.name }}</span>
|
|
<span v-if="isConfigured(c.prefab)" class="lcs-item__dot" />
|
|
</button>
|
|
</template>
|
|
|
|
<div v-if="Object.keys(groupedContainers).length === 0" class="lcs-empty">
|
|
No containers match
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.lcs-root {
|
|
width: 240px;
|
|
flex: none;
|
|
background: var(--surface-base);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--ring-default);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.lcs-search {
|
|
padding: 10px 10px 8px;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.lcs-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 6px 0;
|
|
}
|
|
|
|
/* Category heading */
|
|
.lcs-cat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 10px 12px 4px;
|
|
}
|
|
.lcs-cat__icon {
|
|
color: var(--text-muted);
|
|
flex: none;
|
|
}
|
|
.lcs-cat__label {
|
|
font-size: var(--text-2xs, 10px);
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.09em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Container row */
|
|
.lcs-item {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 12px;
|
|
background: transparent;
|
|
border: 0;
|
|
cursor: pointer;
|
|
font-family: var(--font-sans);
|
|
font-size: var(--text-sm);
|
|
color: var(--text-secondary);
|
|
text-align: left;
|
|
transition: var(--transition-colors);
|
|
border-radius: 0;
|
|
}
|
|
.lcs-item:hover {
|
|
background: var(--surface-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
.lcs-item--active {
|
|
background: var(--accent-soft);
|
|
color: var(--accent-text);
|
|
}
|
|
.lcs-item__name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.lcs-item__dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
flex: none;
|
|
}
|
|
|
|
.lcs-empty {
|
|
padding: 20px 12px;
|
|
text-align: center;
|
|
font-size: var(--text-sm);
|
|
color: var(--text-muted);
|
|
}
|
|
</style>
|