/ app / components / Ui / ListModal.vue
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>