mirror of
https://github.com/yamadashy/repomix.git
synced 2026-02-07 20:26:47 +01:00
- Add try-catch around structuredClone with JSON fallback - Fixes DataCloneError in environments where structuredClone fails - Ensures compatibility across different browser environments
374 lines
8.6 KiB
Vue
374 lines
8.6 KiB
Vue
<template>
|
|
<div class="file-selection-container">
|
|
<div class="file-selection-header">
|
|
<h3 class="file-selection-title">
|
|
<FileText :size="16" class="title-icon" />
|
|
File Selection
|
|
</h3>
|
|
<div class="file-selection-actions">
|
|
<button
|
|
type="button"
|
|
class="action-btn select-all"
|
|
@click="selectAll"
|
|
:disabled="!hasFiles"
|
|
aria-label="Select all files"
|
|
>
|
|
Select All
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="action-btn deselect-all"
|
|
@click="deselectAll"
|
|
:disabled="!hasFiles"
|
|
aria-label="Deselect all files"
|
|
>
|
|
Deselect All
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="action-btn repack"
|
|
@click="handleRepack"
|
|
:disabled="!hasSelectedFiles || loading"
|
|
:aria-label="loading ? 'Re-packing selected files' : `Re-pack ${selectedFiles.length} selected files`"
|
|
>
|
|
{{ loading ? 'Re-packing...' : 'Re-pack Selected' }}
|
|
<PackIcon v-if="!loading" :size="14" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="file-selection-stats">
|
|
<span class="stat-item">
|
|
{{ selectedFiles.length }} of {{ allFiles.length }} files selected
|
|
</span>
|
|
<span class="stat-separator">|</span>
|
|
<span class="stat-item">
|
|
{{ selectedTokens.toLocaleString() }} tokens
|
|
({{ totalTokens > 0 ? ((selectedTokens / totalTokens) * 100).toFixed(1) : '0.0' }}%)
|
|
</span>
|
|
</div>
|
|
|
|
<div class="file-list-container">
|
|
<div class="file-list-scroll">
|
|
<table class="file-table" aria-label="File selection table">
|
|
<thead>
|
|
<tr>
|
|
<th class="checkbox-column">
|
|
<input
|
|
type="checkbox"
|
|
:checked="selectedFiles.length === allFiles.length && allFiles.length > 0"
|
|
:indeterminate="selectedFiles.length > 0 && selectedFiles.length < allFiles.length"
|
|
@change="($event.target as HTMLInputElement).checked ? selectAll() : deselectAll()"
|
|
class="header-checkbox"
|
|
aria-label="Select or deselect all files"
|
|
/>
|
|
</th>
|
|
<th class="file-path-column">File Path</th>
|
|
<th class="tokens-column">Tokens</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="file in sortedFiles"
|
|
:key="file.path"
|
|
class="file-row"
|
|
:class="{ 'file-row-selected': file.selected }"
|
|
@click="toggleFileSelection(file, $event)"
|
|
>
|
|
<td class="checkbox-cell">
|
|
<input
|
|
type="checkbox"
|
|
v-model="file.selected"
|
|
class="file-checkbox"
|
|
:aria-label="`Select file ${file.path}`"
|
|
/>
|
|
</td>
|
|
<td class="file-path-cell">
|
|
<span class="file-path">{{ file.path }}</span>
|
|
</td>
|
|
<td class="tokens-cell">
|
|
<span class="file-tokens">{{ file.tokenCount.toLocaleString() }}</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<FileSelectionWarning
|
|
v-if="selectedFiles.length > FILE_SELECTION_WARNING_THRESHOLD"
|
|
:threshold="FILE_SELECTION_WARNING_THRESHOLD"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { FileText } from 'lucide-vue-next';
|
|
import { computed, ref, watch } from 'vue';
|
|
import { FILE_SELECTION_WARNING_THRESHOLD } from '../../constants/fileSelection';
|
|
import type { FileInfo } from '../api/client';
|
|
import FileSelectionWarning from './FileSelectionWarning.vue';
|
|
import PackIcon from './PackIcon.vue';
|
|
|
|
interface Props {
|
|
allFiles: FileInfo[];
|
|
loading?: boolean;
|
|
}
|
|
|
|
type Emits = (e: 'repack', selectedFiles: FileInfo[]) => void;
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
loading: false,
|
|
});
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// Local reactive state to avoid mutating props directly (Vue one-way data flow)
|
|
const localFiles = ref<FileInfo[]>([]);
|
|
|
|
// Sync props.allFiles to localFiles with deep copying to maintain reactivity
|
|
watch(
|
|
() => props.allFiles,
|
|
(newFiles) => {
|
|
// Deep clone to avoid mutating props - fallback to JSON method for compatibility
|
|
try {
|
|
localFiles.value = structuredClone(newFiles || []);
|
|
} catch {
|
|
localFiles.value = JSON.parse(JSON.stringify(newFiles || []));
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
const hasFiles = computed(() => localFiles.value.length > 0);
|
|
|
|
const selectedFiles = computed(() => localFiles.value.filter((file) => file.selected));
|
|
|
|
const hasSelectedFiles = computed(() => selectedFiles.value.length > 0);
|
|
|
|
const totalTokens = computed(() => localFiles.value.reduce((sum, file) => sum + file.tokenCount, 0));
|
|
|
|
const selectedTokens = computed(() => selectedFiles.value.reduce((sum, file) => sum + file.tokenCount, 0));
|
|
|
|
const sortedFiles = computed(() => [...localFiles.value].sort((a, b) => b.tokenCount - a.tokenCount));
|
|
|
|
const selectAll = () => {
|
|
for (const file of localFiles.value) {
|
|
file.selected = true;
|
|
}
|
|
};
|
|
|
|
const deselectAll = () => {
|
|
for (const file of localFiles.value) {
|
|
file.selected = false;
|
|
}
|
|
};
|
|
|
|
const handleRepack = () => {
|
|
if (hasSelectedFiles.value) {
|
|
emit('repack', selectedFiles.value);
|
|
}
|
|
};
|
|
|
|
const toggleFileSelection = (file: FileInfo, event?: Event) => {
|
|
// Prevent double-toggling when clicking directly on checkbox
|
|
if (event?.target && (event.target as HTMLInputElement).type === 'checkbox') {
|
|
return;
|
|
}
|
|
|
|
file.selected = !file.selected;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.file-selection-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--vp-c-border);
|
|
background: var(--vp-c-bg-soft);
|
|
border-radius: 8px 8px 0 0;
|
|
}
|
|
|
|
.file-selection-title {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--vp-c-text-1);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.title-icon {
|
|
color: var(--vp-c-text-2);
|
|
}
|
|
|
|
.file-selection-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 6px 12px;
|
|
border: 1px solid var(--vp-c-border);
|
|
border-radius: 4px;
|
|
background: var(--vp-c-bg);
|
|
color: var(--vp-c-text-1);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.action-btn:hover:not(:disabled) {
|
|
background: var(--vp-c-bg-soft);
|
|
border-color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.action-btn.repack {
|
|
background: var(--vp-c-brand-1);
|
|
color: white;
|
|
border-color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.action-btn.repack:hover:not(:disabled) {
|
|
background: var(--vp-c-brand-2);
|
|
}
|
|
|
|
.action-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.file-selection-stats {
|
|
padding: 12px 16px;
|
|
background: var(--vp-c-bg-alt);
|
|
border-bottom: 1px solid var(--vp-c-border);
|
|
font-size: 13px;
|
|
color: var(--vp-c-text-2);
|
|
}
|
|
|
|
.stat-item {
|
|
color: var(--vp-c-text-1);
|
|
}
|
|
|
|
.stat-separator {
|
|
margin: 0 8px;
|
|
color: var(--vp-c-text-3);
|
|
}
|
|
|
|
.file-list-container {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: var(--vp-c-bg);
|
|
}
|
|
|
|
.file-list-scroll {
|
|
padding: 0;
|
|
}
|
|
|
|
.file-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.file-table th {
|
|
background: var(--vp-c-bg-soft);
|
|
border-bottom: 1px solid var(--vp-c-border);
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--vp-c-text-2);
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
|
|
.checkbox-column {
|
|
width: 40px;
|
|
text-align: center;
|
|
}
|
|
|
|
.file-path-column {
|
|
width: 70%;
|
|
}
|
|
|
|
.file-table .tokens-column {
|
|
width: 30%;
|
|
text-align: left;
|
|
padding-right: 2rem;
|
|
}
|
|
|
|
.file-row {
|
|
transition: background-color 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-row:hover {
|
|
background: var(--vp-c-bg-soft);
|
|
}
|
|
|
|
.file-table td {
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid var(--vp-c-border-soft);
|
|
}
|
|
|
|
.checkbox-cell {
|
|
text-align: center;
|
|
}
|
|
|
|
.file-path-cell {
|
|
max-width: 0;
|
|
width: 100%;
|
|
}
|
|
|
|
.file-path {
|
|
font-size: 13px;
|
|
color: var(--vp-c-text-1);
|
|
word-break: break-all;
|
|
font-family: var(--vp-font-family-mono);
|
|
}
|
|
|
|
.file-table .tokens-cell {
|
|
text-align: left;
|
|
padding-right: 2rem;
|
|
}
|
|
|
|
.file-tokens {
|
|
font-size: 12px;
|
|
color: var(--vp-c-text-2);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.header-checkbox {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-checkbox {
|
|
cursor: pointer;
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
.file-selection-header {
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.file-selection-actions {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
</style>
|