ListModal.vue
1 <template> 2 <dialog :open="open" class="list-modal" @close="$emit('close')"> 3 <div class="header"> 4 <h2 class="title">{{ title }}</h2> 5 <button class="close-button" @click="$emit('close')"> 6 <LucideX /> 7 </button> 8 </div> 9 <div class="list-content" ref="listContentRef"> 10 <div 11 v-for="item in sortedItems" 12 :key="item.name" 13 class="item" 14 :style="{ 15 '--percentage': `${ 16 sortedItems.length > 0 && sortedItems[0].seconds > 0 17 ? ((item.seconds / sortedItems[0].seconds) * 100).toFixed(1) 18 : 0 19 }%`, 20 }"> 21 <div class="name">{{ item.name || "Unknown" }}</div> 22 <div class="percentage">{{ calculatePercentage(item.seconds) }}%</div> 23 <div class="time">{{ formatTime(item.seconds) }}</div> 24 </div> 25 </div> 26 </dialog> 27 </template> 28 29 <script setup lang="ts"> 30 import { computed, ref, watch } from "vue"; 31 import type { PropType } from "vue"; 32 import { LucideX } from "lucide-vue-next"; 33 34 type Item = { 35 name: string; 36 seconds: number; 37 }; 38 39 const props = defineProps({ 40 open: { 41 type: Boolean, 42 default: false, 43 }, 44 title: { 45 type: String, 46 required: true, 47 }, 48 items: { 49 type: Array as () => Item[], 50 required: true, 51 }, 52 totalSeconds: { 53 type: Number, 54 required: true, 55 }, 56 formatTime: { 57 type: Function as PropType<(seconds: number) => string>, 58 required: true, 59 }, 60 }); 61 62 defineEmits(["close"]); 63 64 const listContentRef = ref<HTMLElement | null>(null); 65 66 const sortedItems = computed(() => { 67 return [...props.items].sort((a, b) => b.seconds - a.seconds); 68 }); 69 70 const calculatePercentage = (seconds: number): string => { 71 if (props.totalSeconds === 0) return "0.0"; 72 return ((seconds / props.totalSeconds) * 100).toFixed(1); 73 }; 74 75 watch( 76 () => props.open, 77 (newOpen) => { 78 if (!newOpen && listContentRef.value) { 79 listContentRef.value.scrollTop = 0; 80 } 81 } 82 ); 83 </script> 84 85 <style scoped lang="scss"> 86 .list-modal { 87 position: fixed; 88 top: 50%; 89 left: 50%; 90 z-index: 1000; 91 transform: translate(-50%, -50%); 92 border: 1px solid var(--border); 93 padding: 24px; 94 max-width: 600px; 95 width: 90%; 96 background: var(--background); 97 color: var(--text); 98 display: flex; 99 flex-direction: column; 100 gap: 16px; 101 box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.251); 102 max-height: 80vh; 103 104 &:not([open]) { 105 display: none; 106 } 107 108 &::backdrop { 109 background-color: rgba(0, 0, 0, 0.5); 110 } 111 112 .header { 113 display: flex; 114 justify-content: space-between; 115 align-items: center; 116 width: 100%; 117 padding-bottom: 16px; 118 border-bottom: 1px solid var(--border); 119 120 .title { 121 font-size: 18px; 122 font-weight: 500; 123 margin: 0; 124 text-transform: uppercase; 125 color: var(--text-secondary); 126 } 127 128 .close-button { 129 background: none; 130 border: none; 131 color: var(--text-secondary); 132 cursor: pointer; 133 padding: 0; 134 display: flex; 135 align-items: center; 136 justify-content: center; 137 138 &:hover { 139 color: var(--text); 140 } 141 } 142 } 143 144 .list-content { 145 overflow-y: auto; 146 147 .item { 148 display: grid; 149 grid-template-columns: 1fr auto auto; 150 gap: 1rem; 151 align-items: center; 152 padding: 8px 10px; 153 position: relative; 154 overflow: hidden; 155 height: 34px; 156 margin-bottom: 4px; 157 158 &:last-child { 159 margin-bottom: 0; 160 } 161 162 &::before { 163 content: ""; 164 position: absolute; 165 top: 0; 166 left: 0; 167 height: 100%; 168 width: var(--percentage); 169 background: var(--element); 170 z-index: 0; 171 transition: width 0.3s ease-in-out; 172 } 173 174 .name, 175 .time, 176 .percentage { 177 position: relative; 178 z-index: 1; 179 white-space: nowrap; 180 } 181 182 .name { 183 font-weight: 500; 184 color: var(--text); 185 overflow: hidden; 186 text-overflow: ellipsis; 187 } 188 189 .time { 190 text-align: right; 191 color: var(--text); 192 font-weight: 500; 193 min-width: 64px; 194 } 195 196 .percentage { 197 text-align: right; 198 color: var(--text-secondary); 199 font-weight: 600; 200 } 201 } 202 } 203 } 204 </style>