mirror of
https://github.com/yamadashy/repomix.git
synced 2025-06-11 00:25:54 +03:00
- CLI: Change order from 'Total Chars, Total Tokens' to 'Total Tokens, Total Chars' - Website: Change order from 'Total Size, Total Tokens' to 'Total Tokens, Total Size' - Maintains consistency across interfaces by prioritizing token metrics
551 lines
13 KiB
Vue
551 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import ace, { type Ace } from 'ace-builds';
|
|
import themeTomorrowUrl from 'ace-builds/src-noconflict/theme-tomorrow?url';
|
|
import themeTomorrowNightUrl from 'ace-builds/src-noconflict/theme-tomorrow_night?url';
|
|
import { BarChart2, Copy, Download, GitFork, HeartHandshake, PackageSearch, Share, Star } from 'lucide-vue-next';
|
|
import { useData } from 'vitepress';
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
import { VAceEditor } from 'vue3-ace-editor';
|
|
import type { PackResult } from '../api/client';
|
|
import {
|
|
canShareFiles,
|
|
copyToClipboard,
|
|
downloadResult,
|
|
formatTimestamp,
|
|
getEditorOptions,
|
|
shareResult,
|
|
} from '../utils/resultViewer';
|
|
|
|
ace.config.setModuleUrl('ace/theme/tomorrow', themeTomorrowUrl);
|
|
ace.config.setModuleUrl('ace/theme/tomorrow_night', themeTomorrowNightUrl);
|
|
|
|
const lightTheme = 'tomorrow';
|
|
const darkTheme = 'tomorrow_night';
|
|
|
|
const props = defineProps<{
|
|
result: PackResult;
|
|
}>();
|
|
|
|
const copied = ref(false);
|
|
const shared = ref(false);
|
|
const canShare = ref(canShareFiles());
|
|
const { isDark } = useData();
|
|
const editorInstance = ref<Ace.Editor | null>(null);
|
|
const isMobile = ref(false);
|
|
const tooltipContainer = ref<HTMLElement | null>(null);
|
|
const tooltipContent = ref<HTMLElement | null>(null);
|
|
|
|
const editorOptions = computed(() => ({
|
|
...getEditorOptions(),
|
|
theme: isDark.value ? `ace/theme/${darkTheme}` : `ace/theme/${lightTheme}`,
|
|
}));
|
|
|
|
watch(isDark, (newIsDark) => {
|
|
if (editorInstance.value) {
|
|
editorInstance.value.setTheme(newIsDark ? `ace/theme/${darkTheme}` : `ace/theme/${lightTheme}`);
|
|
}
|
|
});
|
|
|
|
const formattedTimestamp = computed(() => {
|
|
return formatTimestamp(props.result.metadata.timestamp);
|
|
});
|
|
|
|
const handleCopy = async (event: Event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const success = await copyToClipboard(props.result.content, props.result.format);
|
|
if (success) {
|
|
copied.value = true;
|
|
setTimeout(() => {
|
|
copied.value = false;
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
const handleDownload = (event: Event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
downloadResult(props.result.content, props.result.format, props.result);
|
|
};
|
|
|
|
const handleShare = async (event: Event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// Only allow sharing on mobile devices with Web Share API support
|
|
if (!isMobile.value || !canShare.value) {
|
|
console.log('Share is only available on mobile devices');
|
|
return;
|
|
}
|
|
|
|
const success = await shareResult(props.result.content, props.result.format, props.result);
|
|
if (success) {
|
|
shared.value = true;
|
|
setTimeout(() => {
|
|
shared.value = false;
|
|
}, 2000);
|
|
} else {
|
|
console.log('Share was cancelled or failed');
|
|
}
|
|
};
|
|
|
|
const handleEditorMount = (editor: Ace.Editor) => {
|
|
editorInstance.value = editor;
|
|
};
|
|
|
|
const updateTooltipPosition = () => {
|
|
if (!tooltipContainer.value || !tooltipContent.value || isMobile.value) return;
|
|
|
|
const containerRect = tooltipContainer.value.getBoundingClientRect();
|
|
const tooltipEl = tooltipContent.value;
|
|
|
|
// Position above the button with proper spacing for the arrow (like existing tooltips)
|
|
tooltipEl.style.top = `${containerRect.top - 46}px`;
|
|
tooltipEl.style.left = `${containerRect.left + containerRect.width / 2}px`;
|
|
|
|
// Show tooltip (override CSS hover states)
|
|
tooltipEl.style.opacity = '1';
|
|
tooltipEl.style.visibility = 'visible';
|
|
};
|
|
|
|
const hideTooltip = () => {
|
|
if (tooltipContent.value) {
|
|
tooltipContent.value.style.opacity = '0';
|
|
tooltipContent.value.style.visibility = 'hidden';
|
|
}
|
|
};
|
|
|
|
const messages = [
|
|
{
|
|
type: 'sponsor',
|
|
link: 'https://github.com/sponsors/yamadashy',
|
|
icon: HeartHandshake,
|
|
text: 'Your support helps maintain and improve it. Thank you!',
|
|
color: '#b04386',
|
|
},
|
|
{
|
|
type: 'star',
|
|
link: 'https://github.com/yamadashy/repomix',
|
|
icon: Star,
|
|
text: 'If you like Repomix, please give us a star on GitHub!',
|
|
color: '#f1c40f',
|
|
},
|
|
];
|
|
|
|
const currentMessageIndex = ref(Math.floor(Math.random() * messages.length));
|
|
const supportMessage = computed(() => ({
|
|
type: messages[currentMessageIndex.value].type,
|
|
link: messages[currentMessageIndex.value].link,
|
|
icon: messages[currentMessageIndex.value].icon,
|
|
text: messages[currentMessageIndex.value].text,
|
|
color: messages[currentMessageIndex.value].color,
|
|
}));
|
|
|
|
const handleResize = () => {
|
|
isMobile.value = window.innerWidth <= 768;
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
// Hide tooltip on scroll to prevent detachment from button
|
|
if (tooltipContent.value) {
|
|
tooltipContent.value.style.opacity = '0';
|
|
tooltipContent.value.style.visibility = 'hidden';
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
isMobile.value = window.innerWidth <= 768;
|
|
window.addEventListener('resize', handleResize);
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', handleResize);
|
|
window.removeEventListener('scroll', handleScroll);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="content-wrapper">
|
|
<div class="metadata-panel">
|
|
<div class="metadata-section">
|
|
<h3><GitFork :size="16" class="section-icon" /> Repository Info</h3>
|
|
<dl>
|
|
<dt>Repository</dt>
|
|
<dd>{{ result.metadata.repository }}</dd>
|
|
<dt>Generated At</dt>
|
|
<dd>{{ formattedTimestamp }}</dd>
|
|
<dt>Format</dt>
|
|
<dd>{{ result.format }}</dd>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="metadata-section">
|
|
<h3><PackageSearch :size="16" class="section-icon" /> Pack Summary</h3>
|
|
<dl v-if="result.metadata.summary">
|
|
<dt>Total Files</dt>
|
|
<dd>{{ result.metadata.summary.totalFiles.toLocaleString() }} <span class="unit">files</span></dd>
|
|
<dt>Total Tokens</dt>
|
|
<dd>{{ result.metadata.summary.totalTokens.toLocaleString() }} <span class="unit">tokens</span></dd>
|
|
<dt>Total Size</dt>
|
|
<dd>{{ result.metadata.summary.totalCharacters.toLocaleString() }} <span class="unit">chars</span></dd>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="metadata-section" v-if="result.metadata.topFiles">
|
|
<h3><BarChart2 :size="16" class="section-icon" /> Top {{ result.metadata.topFiles.length }} Files</h3>
|
|
<ol class="top-files-list">
|
|
<li v-for="file in result.metadata.topFiles" :key="file.path">
|
|
<div class="file-path">{{ file.path }}</div>
|
|
<div class="file-stats">
|
|
{{ file.tokenCount.toLocaleString() }} <span class="unit">tokens</span> <span class="separator-unit">|</span> {{ file.charCount.toLocaleString() }} <span class="unit">chars</span> <span class="separator-unit">|</span> {{ ((file.tokenCount / result.metadata.summary.totalTokens) * 100).toFixed(1) }}<span class="unit">%</span>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="output-panel">
|
|
<div class="output-actions">
|
|
<button
|
|
class="action-button"
|
|
@click="handleCopy"
|
|
:class="{ copied }"
|
|
>
|
|
<Copy :size="16" />
|
|
{{ copied ? 'Copied!' : 'Copy' }}
|
|
</button>
|
|
<button
|
|
class="action-button"
|
|
@click="handleDownload"
|
|
>
|
|
<Download :size="16" />
|
|
Download
|
|
</button>
|
|
<div v-if="canShare" class="mobile-only" style="flex-basis: 100%"></div>
|
|
<div v-if="canShare" class="tooltip-container" ref="tooltipContainer" @mouseenter="updateTooltipPosition" @mouseleave="hideTooltip">
|
|
<button
|
|
class="action-button"
|
|
@click="handleShare"
|
|
:class="{ shared }"
|
|
:disabled="!isMobile"
|
|
aria-label="Share output via mobile apps"
|
|
>
|
|
<Share :size="16" />
|
|
{{ shared ? 'Shared!' : 'Open with your app' }}
|
|
</button>
|
|
<div class="tooltip-content desktop-only" ref="tooltipContent">
|
|
Only available on mobile devices
|
|
<div class="tooltip-arrow"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="editor-container">
|
|
<VAceEditor
|
|
v-model:value="result.content"
|
|
:lang="'text'"
|
|
:style="{ height: '100%', width: '100%' }"
|
|
:options="editorOptions"
|
|
@mount="handleEditorMount"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="support-notice">
|
|
<div class="support-message">
|
|
<a :href="supportMessage.link" target="_blank" rel="noopener noreferrer" class="support-link">
|
|
<component :is="supportMessage.icon" :size="14" class="support-icon" />
|
|
{{ supportMessage.text }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.content-wrapper {
|
|
display: grid;
|
|
grid-template-columns: 300px 1fr;
|
|
grid-template-rows: 445px auto;
|
|
}
|
|
|
|
.metadata-panel {
|
|
padding: 16px;
|
|
border-right: 1px solid var(--vp-c-border);
|
|
background: var(--vp-c-bg-soft);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.metadata-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.metadata-section:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.metadata-section h3 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin: 0 0 12px;
|
|
color: var(--vp-c-text-1);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.section-icon {
|
|
color: var(--vp-c-text-2);
|
|
}
|
|
|
|
dl {
|
|
margin: 0;
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
dt {
|
|
color: var(--vp-c-text-2);
|
|
font-weight: 500;
|
|
}
|
|
|
|
dd {
|
|
margin: 0;
|
|
color: var(--vp-c-text-1);
|
|
text-transform: lowercase;
|
|
}
|
|
|
|
.unit {
|
|
color: var(--vp-c-text-2);
|
|
margin-left: 0.3em;
|
|
}
|
|
|
|
.separator-unit {
|
|
color: var(--vp-c-text-3);
|
|
margin: 0 0.5em;
|
|
}
|
|
|
|
.top-files-list {
|
|
margin: 0;
|
|
padding: 0 0 0 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.top-files-list li {
|
|
margin-bottom: 8px;
|
|
border-left: 2px solid var(--vp-c-divider);
|
|
padding-left: 8px;
|
|
}
|
|
|
|
.file-path {
|
|
color: var(--vp-c-text-1);
|
|
margin-bottom: 2px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.file-stats {
|
|
font-size: 12px;
|
|
color: var(--vp-c-text-1);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.output-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
max-height: 500px;
|
|
background: var(--vp-c-bg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.output-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
padding: 12px;
|
|
background: var(--vp-c-bg);
|
|
border-bottom: 1px solid var(--vp-c-border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.action-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 12px;
|
|
border: 1px solid var(--vp-c-border);
|
|
border-radius: 6px;
|
|
background: var(--vp-c-bg-soft);
|
|
color: var(--vp-c-text-1);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.action-button:hover {
|
|
border-color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.action-button.copied {
|
|
background: var(--vp-c-brand-1);
|
|
color: white;
|
|
border-color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.action-button.shared {
|
|
background: var(--vp-c-brand-1);
|
|
color: white;
|
|
border-color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.editor-container {
|
|
height: 100%;
|
|
width: 100%;
|
|
font-family: var(--vp-font-family-mono);
|
|
}
|
|
|
|
.support-notice {
|
|
grid-column: 1 / -1;
|
|
padding: 8px;
|
|
background: var(--vp-c-bg-soft);
|
|
border-top: 1px solid var(--vp-c-border);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
min-height: 45px;
|
|
}
|
|
|
|
.support-message {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--vp-c-text-2);
|
|
font-size: 12px;
|
|
width: 100%;
|
|
}
|
|
|
|
.support-icon {
|
|
flex-shrink: 0;
|
|
transition: color 0.3s ease;
|
|
color: v-bind('supportMessage.color');
|
|
}
|
|
|
|
.support-link {
|
|
text-decoration: none;
|
|
font-weight: normal;
|
|
transition: color 0.3s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.support-link:hover {
|
|
color: var(--vp-c-brand-1);
|
|
}
|
|
|
|
.mobile-only {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.content-wrapper {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto minmax(500px, auto) auto;
|
|
height: auto;
|
|
}
|
|
|
|
.metadata-panel {
|
|
border-right: none;
|
|
border-bottom: 1px solid var(--vp-c-border);
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.output-panel {
|
|
height: 500px;
|
|
}
|
|
|
|
.support-notice {
|
|
padding: 16px;
|
|
}
|
|
|
|
.support-message {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.mobile-only {
|
|
display: inline-flex;
|
|
}
|
|
}
|
|
|
|
.tooltip-container {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.tooltip-content {
|
|
position: fixed;
|
|
transform: translateX(-50%);
|
|
margin-bottom: 8px;
|
|
padding: 8px 12px;
|
|
background: #333;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
white-space: nowrap;
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: opacity 0.2s, visibility 0.2s;
|
|
z-index: 9999;
|
|
pointer-events: none;
|
|
text-align: left;
|
|
}
|
|
|
|
.tooltip-arrow {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border-width: 8px;
|
|
border-style: solid;
|
|
border-color: #333 transparent transparent transparent;
|
|
}
|
|
|
|
.tooltip-container:hover .tooltip-content {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.desktop-only {
|
|
display: block;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.desktop-only {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.action-button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.action-button:disabled:hover {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Dark mode support for tooltip */
|
|
html.dark .tooltip-content {
|
|
background: #333;
|
|
color: #ffffff;
|
|
}
|
|
|
|
html.dark .tooltip-arrow {
|
|
border-color: #333 transparent transparent transparent;
|
|
}
|
|
</style>
|