...
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
.idea
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { LiveItem } from '../api/types.js';
|
||||
import { abbreviateNumber, fullNumber } from '../utils/format.js';
|
||||
|
||||
defineProps<{
|
||||
items: LiveItem[];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function openItem(item: LiveItem): void {
|
||||
router.push({ name: 'item', params: { name: item.name } });
|
||||
}
|
||||
|
||||
function label(item: LiveItem): string {
|
||||
return item.displayName ?? item.name;
|
||||
}
|
||||
|
||||
/* Deterministic hue from the mod id so each mod has its own fallback color. */
|
||||
function modHue(mod: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < mod.length; i++) {
|
||||
hash = (hash * 31 + mod.charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(hash) % 360;
|
||||
}
|
||||
|
||||
/* Two-letter glyph for the icon fallback — first letters of display name. */
|
||||
function fallbackGlyph(item: LiveItem): string {
|
||||
const source = item.displayName ?? item.name.split(':').pop() ?? item.name;
|
||||
const cleaned = source.replace(/[^a-zA-Z0-9]+/g, ' ').trim();
|
||||
if (!cleaned) return '?';
|
||||
const parts = cleaned.split(/\s+/);
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
function onIconError(event: Event): void {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.dataset.failed = 'true';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid panel">
|
||||
<div v-if="items.length === 0" class="grid__empty">
|
||||
No items match the current filters.
|
||||
</div>
|
||||
|
||||
<div v-else class="grid__cells">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item.name"
|
||||
type="button"
|
||||
class="cell"
|
||||
:title="`${label(item)}\n${item.name}\n${fullNumber(item.amount)}`"
|
||||
@click="openItem(item)"
|
||||
>
|
||||
<span class="cell__icon">
|
||||
<img
|
||||
v-if="item.icon"
|
||||
:src="item.icon"
|
||||
:alt="label(item)"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@error="onIconError"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="cell__fallback"
|
||||
:style="{
|
||||
background: `hsl(${modHue(item.mod)}, 45%, 30%)`,
|
||||
borderColor: `hsl(${modHue(item.mod)}, 55%, 45%)`,
|
||||
}"
|
||||
>
|
||||
{{ fallbackGlyph(item) }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="cell__count mono">{{ abbreviateNumber(item.amount) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grid {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.grid__cells {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.grid__empty {
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
padding: 40px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background: #1a232c;
|
||||
border: 1px solid #2a3744;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.1s ease,
|
||||
background-color 0.1s ease,
|
||||
transform 0.05s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cell:hover,
|
||||
.cell:focus-visible {
|
||||
border-color: var(--accent);
|
||||
background: #1f2c37;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cell:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.cell__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.cell__icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.cell__icon img[data-failed='true'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cell__fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.cell__count {
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 1px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-shadow:
|
||||
1px 1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px -1px 0 #000;
|
||||
pointer-events: none;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user