feat(redesign): re-skin plugin-config editors + Loot Builder to DS (Phase D batch 3)
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>
This commit is contained in:
Vantz Stockwell
2026-06-11 02:46:16 -04:00
parent b42a2d7ea7
commit 376ed9a98d
15 changed files with 4990 additions and 4006 deletions

View File

@@ -11,19 +11,13 @@ import { useFurnaceSplitterStore } from '@/stores/furnacesplitter'
import { useBetterChatStore } from '@/stores/betterchat'
import { useTimedExecuteStore } from '@/stores/timedexecute'
import { useRaidableBasesStore } from '@/stores/raidablebases'
import {
Crosshair,
Navigation2,
Pickaxe,
DoorOpen,
Gift,
Flame,
MessageSquare,
Clock,
Swords,
Search,
ArrowRight,
} from 'lucide-vue-next'
import Panel from '@/components/ds/data/Panel.vue'
import Button from '@/components/ds/core/Button.vue'
import Icon from '@/components/ds/core/Icon.vue'
import Badge from '@/components/ds/core/Badge.vue'
import Input from '@/components/ds/forms/Input.vue'
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
const router = useRouter()
const auth = useAuthStore()
@@ -45,7 +39,7 @@ interface PluginDef {
key: string
name: string
description: string
icon: any
icon: string
path: string
permission: string
getConfigs: () => any[]
@@ -53,15 +47,15 @@ interface PluginDef {
}
const plugins: PluginDef[] = [
{ key: 'loot', name: 'Loot Tables', description: 'Configure loot container drop tables and item probabilities', icon: Crosshair, path: '/loot-builder', permission: 'loot.view', getConfigs: () => lootStore.profiles, fetchFn: () => lootStore.fetchProfiles() },
{ key: 'teleport', name: 'Teleport', description: 'Home locations, TPR cooldowns, and VIP teleport settings', icon: Navigation2, path: '/teleport-config', permission: 'teleport.view', getConfigs: () => teleportStore.configs, fetchFn: () => teleportStore.fetchConfigs() },
{ key: 'gather', name: 'Gather Rates', description: 'Resource gathering multipliers and pickup rates', icon: Pickaxe, path: '/gather-manager', permission: 'gather.view', getConfigs: () => gatherStore.configs, fetchFn: () => gatherStore.fetchConfigs() },
{ key: 'autodoors', name: 'Auto Doors', description: 'Automatic door closing delays and permissions', icon: DoorOpen, path: '/autodoors', permission: 'autodoors.view', getConfigs: () => autoDoorsStore.configs, fetchFn: () => autoDoorsStore.fetchConfigs() },
{ key: 'kits', name: 'Kits', description: 'Player kits with items, cooldowns, and permissions', icon: Gift, path: '/kits', permission: 'kits.view', getConfigs: () => kitsStore.configs, fetchFn: () => kitsStore.fetchConfigs() },
{ key: 'furnacesplitter', name: 'Furnace Splitter', description: 'Automatic furnace ore splitting and smelting config', icon: Flame, path: '/furnace-splitter', permission: 'furnacesplitter.view', getConfigs: () => furnaceSplitterStore.configs, fetchFn: () => furnaceSplitterStore.fetchConfigs() },
{ key: 'betterchat', name: 'Better Chat', description: 'Chat formatting, group colors, and title prefixes', icon: MessageSquare, path: '/better-chat', permission: 'betterchat.view', getConfigs: () => betterChatStore.configs, fetchFn: () => betterChatStore.fetchConfigs() },
{ key: 'timedexecute', name: 'Timed Execute', description: 'Scheduled, real-time, and event-driven command execution', icon: Clock, path: '/timed-execute', permission: 'timedexecute.view', getConfigs: () => timedExecuteStore.configs, fetchFn: () => timedExecuteStore.fetchConfigs() },
{ key: 'raidablebases', name: 'Raidable Bases', description: 'PVE raid events, difficulty, NPCs, and loot settings', icon: Swords, path: '/raidable-bases', permission: 'raidablebases.view', getConfigs: () => raidableBasesStore.configs, fetchFn: () => raidableBasesStore.fetchConfigs() },
{ key: 'loot', name: 'Loot tables', description: 'Configure loot container drop tables and item probabilities', icon: 'crosshair', path: '/loot-builder', permission: 'loot.view', getConfigs: () => lootStore.profiles, fetchFn: () => lootStore.fetchProfiles() },
{ key: 'teleport', name: 'Teleport', description: 'Home locations, TPR cooldowns, and VIP teleport settings', icon: 'navigation', path: '/teleport-config', permission: 'teleport.view', getConfigs: () => teleportStore.configs, fetchFn: () => teleportStore.fetchConfigs() },
{ key: 'gather', name: 'Gather rates', description: 'Resource gathering multipliers and pickup rates', icon: 'pickaxe', path: '/gather-manager', permission: 'gather.view', getConfigs: () => gatherStore.configs, fetchFn: () => gatherStore.fetchConfigs() },
{ key: 'autodoors', name: 'Auto doors', description: 'Automatic door closing delays and permissions', icon: 'door-open', path: '/autodoors', permission: 'autodoors.view', getConfigs: () => autoDoorsStore.configs, fetchFn: () => autoDoorsStore.fetchConfigs() },
{ key: 'kits', name: 'Kits', description: 'Player kits with items, cooldowns, and permissions', icon: 'gift', path: '/kits', permission: 'kits.view', getConfigs: () => kitsStore.configs, fetchFn: () => kitsStore.fetchConfigs() },
{ key: 'furnacesplitter', name: 'Furnace splitter', description: 'Automatic furnace ore splitting and smelting config', icon: 'flame', path: '/furnace-splitter', permission: 'furnacesplitter.view', getConfigs: () => furnaceSplitterStore.configs, fetchFn: () => furnaceSplitterStore.fetchConfigs() },
{ key: 'betterchat', name: 'Better Chat', description: 'Chat formatting, group colors, and title prefixes', icon: 'message-square', path: '/better-chat', permission: 'betterchat.view', getConfigs: () => betterChatStore.configs, fetchFn: () => betterChatStore.fetchConfigs() },
{ key: 'timedexecute', name: 'Timed Execute', description: 'Scheduled, real-time, and event-driven command execution', icon: 'clock', path: '/timed-execute', permission: 'timedexecute.view', getConfigs: () => timedExecuteStore.configs, fetchFn: () => timedExecuteStore.fetchConfigs() },
{ key: 'raidablebases', name: 'Raidable Bases', description: 'PVE raid events, difficulty, NPCs, and loot settings', icon: 'swords', path: '/raidable-bases', permission: 'raidablebases.view', getConfigs: () => raidableBasesStore.configs, fetchFn: () => raidableBasesStore.fetchConfigs() },
]
const visiblePlugins = computed(() =>
@@ -76,12 +70,12 @@ const filteredPlugins = computed(() => {
)
})
function getStatus(plugin: PluginDef): { label: string; color: string } {
function getStatus(plugin: PluginDef): { label: string; tone: 'online' | 'info' | 'neutral' } {
const configs = plugin.getConfigs()
if (!configs || configs.length === 0) return { label: 'Not Configured', color: 'neutral' }
if (!configs || configs.length === 0) return { label: 'Not configured', tone: 'neutral' }
const hasActive = configs.some((c: any) => c.is_active)
if (hasActive) return { label: 'Active', color: 'green' }
return { label: 'Configured', color: 'blue' }
if (hasActive) return { label: 'Active', tone: 'online' }
return { label: 'Configured', tone: 'info' }
}
function getConfigCount(plugin: PluginDef): string {
@@ -90,6 +84,12 @@ function getConfigCount(plugin: PluginDef): string {
return `${configs.length} profile${configs.length !== 1 ? 's' : ''}`
}
// Search model — DS Input uses string v-model
const searchModel = computed<string>({
get: () => searchQuery.value,
set: (v: string | undefined) => { searchQuery.value = v ?? '' },
})
onMounted(async () => {
const fetches = visiblePlugins.value.map(p => p.fetchFn().catch(() => {}))
await Promise.all(fetches)
@@ -98,88 +98,142 @@ onMounted(async () => {
</script>
<template>
<div class="p-6 max-w-7xl mx-auto">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Plugin Configs</h1>
<p class="text-neutral-400 mt-1">Configure and manage your server plugins</p>
<div class="pc">
<!-- Page head -->
<div class="pc__head">
<div class="pc__head-id">
<div class="pc__head-chip">
<Icon name="puzzle" :size="20" :stroke-width="2" />
</div>
<div>
<div class="t-eyebrow">Plugin management</div>
<h1 class="pc__title">Plugin configs</h1>
</div>
</div>
</div>
<!-- Search -->
<div class="relative mb-6">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
<input
v-model="searchQuery"
type="text"
placeholder="Search plugins..."
class="w-full pl-10 pr-4 py-2 bg-neutral-900 border border-neutral-800 rounded-lg text-sm text-neutral-200 placeholder-neutral-500 focus:outline-none focus:border-oxide-500 transition-colors"
/>
</div>
<Input
v-model="searchModel"
icon="search"
placeholder="Search plugins…"
/>
<!-- Loading -->
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Loading skeleton -->
<div v-if="loading" class="pc__grid">
<div
v-for="i in visiblePlugins.length"
:key="i"
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5 animate-pulse"
class="pc__skeleton"
>
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 bg-neutral-800 rounded-lg" />
<div class="flex-1">
<div class="h-4 w-24 bg-neutral-800 rounded" />
<div class="h-3 w-16 bg-neutral-800 rounded mt-2" />
</div>
<div class="pc__skel-icon" />
<div class="pc__skel-lines">
<div class="pc__skel-line pc__skel-line--name" />
<div class="pc__skel-line pc__skel-line--sub" />
</div>
<div class="h-3 w-full bg-neutral-800 rounded mt-3" />
<div class="h-3 w-2/3 bg-neutral-800 rounded mt-2" />
</div>
</div>
<!-- Cards -->
<div v-else-if="filteredPlugins.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Cards grid -->
<div v-else-if="filteredPlugins.length" class="pc__grid">
<div
v-for="plugin in filteredPlugins"
:key="plugin.key"
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5 hover:border-neutral-700 transition-colors group"
class="pc__card"
>
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-neutral-800 rounded-lg flex items-center justify-center group-hover:bg-oxide-500/10 transition-colors">
<component :is="plugin.icon" class="w-5 h-5 text-neutral-400 group-hover:text-oxide-400 transition-colors" />
<!-- Card head -->
<div class="pc__card-head">
<div class="pc__card-id">
<div class="pc__card-icon">
<Icon :name="plugin.icon" :size="18" :stroke-width="2" />
</div>
<div>
<h3 class="text-sm font-semibold text-white">{{ plugin.name }}</h3>
<span class="text-xs text-neutral-500">{{ getConfigCount(plugin) }}</span>
<div class="pc__card-name">{{ plugin.name }}</div>
<div class="pc__card-count">{{ getConfigCount(plugin) }}</div>
</div>
</div>
<!-- Status badge -->
<span
class="text-[10px] font-medium px-2 py-0.5 rounded-full"
:class="{
'bg-green-500/10 text-green-400': getStatus(plugin).color === 'green',
'bg-blue-500/10 text-blue-400': getStatus(plugin).color === 'blue',
'bg-neutral-800 text-neutral-500': getStatus(plugin).color === 'neutral',
}"
>
{{ getStatus(plugin).label }}
</span>
<Badge :tone="getStatus(plugin).tone">{{ getStatus(plugin).label }}</Badge>
</div>
<p class="text-xs text-neutral-400 mb-4 line-clamp-2">{{ plugin.description }}</p>
<!-- Description -->
<p class="pc__card-desc">{{ plugin.description }}</p>
<button
<!-- Configure button -->
<Button
variant="secondary"
size="sm"
icon-right="chevron-right"
:block="true"
@click="router.push(plugin.path)"
class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-neutral-800 hover:bg-oxide-500/10 text-neutral-300 hover:text-oxide-400 text-xs font-medium rounded-lg transition-colors"
>
Configure
<ArrowRight class="w-3 h-3" />
</button>
>Configure</Button>
</div>
</div>
<!-- Empty state -->
<div v-else class="text-center py-12">
<p class="text-neutral-500">No plugins match your search.</p>
</div>
<!-- Empty state (search miss) -->
<Panel v-else>
<EmptyState
icon="search"
title="No plugins match"
description="Try a different search term."
/>
</Panel>
</div>
</template>
<style scoped>
/* ---- Page shell ---- */
.pc { max-width: 1100px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
/* ---- Page head ---- */
.pc__head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.pc__head-id { display: flex; align-items: center; gap: 12px; }
.pc__head-chip {
width: 40px; height: 40px; flex: none; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
color: var(--accent); background: var(--accent-soft);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.pc__title {
font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.02em;
color: var(--text-primary); margin-top: 3px;
}
/* ---- Grid ---- */
.pc__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 900px) { .pc__grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 540px) { .pc__grid { grid-template-columns: 1fr; } }
/* ---- Card ---- */
.pc__card {
background: var(--surface-base); border-radius: var(--radius-lg); box-shadow: var(--ring-default);
padding: 16px; display: flex; flex-direction: column; gap: 12px; min-width: 0;
transition: var(--transition-colors);
}
.pc__card:hover { background: var(--surface-raised); }
.pc__card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.pc__card-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
.pc__card-icon {
width: 36px; height: 36px; flex: none; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
color: var(--text-tertiary); background: var(--surface-raised-2);
box-shadow: var(--ring-default); transition: var(--transition-colors);
}
.pc__card:hover .pc__card-icon { color: var(--accent-text); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-border); }
.pc__card-name { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); }
.pc__card-count { font-size: var(--text-xs); color: var(--text-muted); margin-top: 2px; font-variant-numeric: tabular-nums; }
.pc__card-desc { font-size: var(--text-xs); color: var(--text-tertiary); line-height: 1.5; flex: 1; }
/* ---- Skeleton ---- */
.pc__skeleton {
background: var(--surface-base); border-radius: var(--radius-lg); box-shadow: var(--ring-default);
padding: 16px; display: flex; align-items: flex-start; gap: 12px;
animation: pc-pulse 1.4s ease-in-out infinite;
}
@keyframes pc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
.pc__skel-icon { width: 36px; height: 36px; border-radius: var(--radius-md); background: var(--surface-raised-2); flex: none; }
.pc__skel-lines { flex: 1; display: flex; flex-direction: column; gap: 8px; padding-top: 4px; }
.pc__skel-line { height: 10px; border-radius: var(--radius-sm); background: var(--surface-raised-2); }
.pc__skel-line--name { width: 60%; }
.pc__skel-line--sub { width: 40%; }
</style>