Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions frontend/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@
@tailwind components;
@tailwind utilities;

/* Theme variables for light/dark mode */
:root {
--bg: #f4f4f5;
--text: #111827;
--panel: #ffffff;
--muted: #6b7280;
--border: #e5e7eb;
--primary: #2563eb;
}

/* Dark theme variables applied when the top-level `.dark` class is present */
.dark {
--bg: #0b1220;
--text: #e6eef8;
--panel: #0f1724;
--muted: #9ca3af;
--border: #172033;
--primary: #3b82f6;
}

/* Base page colors using variables so Tailwind utility classes still work alongside these defaults */
html, body {
background-color: var(--bg);
color: var(--text);
transition: background-color 160ms linear, color 160ms linear;
}

/* Form elements default to panel color in both themes */
input, select, textarea {
background-color: var(--panel);
color: var(--text);
border-color: var(--border);
transition: background-color 120ms linear, color 120ms linear, border-color 120ms linear;
}

/* Provide a sensible default for elements that used raw white in markup */
.panel-bg {
background-color: var(--panel);
color: var(--text);
}

/* Basic Reset */
body,
html {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/BottomConsole.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<span v-if="log.type !== 'info'" class="font-semibold">[{{ log.type.toUpperCase() }}] </span>
{{ log.message }}
</div>
<div v-if="logMessages.length === 0" class="text-gray-500">
<div v-if="logMessages.length === 0" class="text-gray-400">
Console is empty.
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CentralPanel.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<main class="flex-1 p-0 overflow-y-auto bg-white relative">
<main class="flex-1 p-0 overflow-y-auto bg-white dark:bg-slate-900 relative">
<Step1CopyStructure
v-if="currentStep === 1"
@action="handleAction"
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/CustomRulesModal.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
<template>
<div v-if="isVisible" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex justify-center items-center" @click.self="handleCancel">
<div class="relative mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-md bg-white">
<div class="relative mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-md bg-white dark:bg-slate-800">
<div class="mt-3 text-center">
<h3 class="text-lg leading-6 font-medium text-gray-900">{{ title }}</h3>
<div class="mt-2 px-7 py-3">
<textarea
v-model="editableRules"
rows="15"
class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm font-mono bg-gray-50"
class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm font-mono bg-white dark:bg-slate-700"
placeholder="Enter custom ignore patterns, one per line (e.g., *.log, node_modules/)"
></textarea>
<p class="text-xs text-gray-500 mt-1 text-left">{{ descriptionText }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 text-left">{{ descriptionText }}</p>
</div>
<div class="items-center px-4 py-3">
<button
@click="handleSave"
class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-auto hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-2"
class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-auto hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-2 dark:bg-blue-500 dark:hover:bg-blue-700"
>
Save
</button>
<button
@click="handleCancel"
class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md w-auto hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400"
class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md w-auto hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400 dark:bg-slate-700 dark:text-gray-100 dark:hover:bg-slate-600"
>
Cancel
</button>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/FileTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ function isEffectivelyExcludedByParent(node) {
.node-item:hover {
background-color: #f0f0f0;
}

/* Dark mode support */
:dark .node-item:hover {
background-color: #2d3748;
}

.toggler {
cursor: pointer;
width: 20px;
Expand All @@ -112,10 +118,22 @@ function isEffectivelyExcludedByParent(node) {
text-decoration: line-through;
color: #999;
}

/* Dark mode for excluded text */
:dark .excluded-node > .node-item > span:not(.toggler) {
color: #a0aec0;
}

/* Style for checkbox of an effectively excluded item (e.g. parent excluded) */
.exclude-checkbox:disabled + span {
color: #bbb; /* Lighter text for items under an excluded parent */
}

/* Dark mode for disabled checkbox text */
:dark .exclude-checkbox:disabled + span {
color: #718096;
}

.exclude-checkbox:disabled {
cursor: not-allowed;
}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/HorizontalStepper.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<nav class="bg-white shadow-md sticky top-0 z-10">
<nav class="bg-white dark:bg-slate-900 shadow-md sticky top-0 z-10">
<div class="container mx-auto px-4 py-3">
<ol class="flex items-center justify-between">
<li v-for="(step, index) in steps" :key="step.id"
Expand All @@ -10,22 +10,22 @@
@click.prevent="canNavigateToStep(step.id) ? $emit('navigate', step.id) : null"
:class="[
'flex flex-col items-center text-xs sm:text-sm font-medium text-center p-2 rounded-md w-full',
canNavigateToStep(step.id) ? 'cursor-pointer hover:bg-gray-100' : 'cursor-not-allowed opacity-60'
canNavigateToStep(step.id) ? 'cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-800' : 'cursor-not-allowed opacity-60'
]"
:disabled="!canNavigateToStep(step.id)"
:title="step.description"
>
<div
:class="[
'flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full border-2 mb-1',
currentStep === step.id ? 'border-blue-600 bg-blue-100 text-blue-700' :
(step.completed ? 'border-green-600 bg-green-100 text-green-700' : 'border-gray-400 bg-gray-50 text-gray-500 group-hover:border-gray-500')
currentStep === step.id ? 'border-blue-600 bg-blue-100 text-blue-700 dark:border-blue-400 dark:bg-blue-900 dark:text-blue-300' :
(step.completed ? 'border-green-600 bg-green-100 text-green-700 dark:border-green-500 dark:bg-green-900 dark:text-green-300' : 'border-gray-400 bg-gray-50 text-gray-500 group-hover:border-gray-500 dark:border-slate-600 dark:bg-slate-800 dark:text-gray-400')
]"
>
<span v-if="!(step.completed && currentStep !== step.id)">{{ step.id }}</span>
<svg v-else class="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
</div>
<span :class="['whitespace-nowrap', currentStep === step.id ? 'text-blue-600 font-semibold' : (step.completed ? 'text-green-600' : 'text-gray-500 group-hover:text-gray-700')]">
<span :class="['whitespace-nowrap', currentStep === step.id ? 'text-blue-600 dark:text-blue-300 font-semibold' : (step.completed ? 'text-green-600 dark:text-green-300' : 'text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-300')]">
{{ step.title }}
</span>
</button>
Expand Down
99 changes: 86 additions & 13 deletions frontend/src/components/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
@save="handleSaveCustomRules"
@cancel="handleCancelCustomRules"
/>
<aside class="w-64 md:w-72 lg:w-80 bg-gray-50 p-4 border-r border-gray-200 overflow-y-auto flex flex-col flex-shrink-0">
<aside class="w-64 md:w-72 lg:w-80 bg-gray-50 dark:bg-slate-900 p-4 border-r border-gray-200 overflow-y-auto flex flex-col flex-shrink-0">
<!-- Project Selection and File Tree -->
<div class="mb-6">
<button
@click="$emit('select-folder')"
class="w-full px-4 py-2 mb-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
class="w-full px-4 py-2 mb-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:bg-blue-600 dark:hover:bg-blue-700"
>
Select Project Folder
</button>
<div v-if="projectRoot" class="text-xs text-gray-600 mb-2 break-all">Selected: {{ projectRoot }}</div>
<div v-if="projectRoot" class="text-xs text-gray-600 dark:text-gray-400 mb-2 break-all">Selected: {{ projectRoot }}</div>

<div v-if="projectRoot" class="mb-2">
<label class="flex items-center text-sm text-gray-700" title="Uses .gitignore file if present in the project folder">
<label class="flex items-center text-sm text-gray-700 dark:text-gray-300" title="Uses .gitignore file if present in the project folder">
<input
type="checkbox"
:checked="useGitignore"
Expand All @@ -28,43 +28,43 @@
/>
Use .gitignore rules
</label>
<label class="flex items-center text-sm text-gray-700 mt-1" title="Uses ignore.glob file if present in the project folder">
<label class="flex items-center text-sm text-gray-700 dark:text-gray-300 mt-1" title="Uses ignore.glob file if present in the project folder">
<input
type="checkbox"
:checked="useCustomIgnore"
@change="$emit('toggle-custom-ignore', $event.target.checked)"
class="form-checkbox h-4 w-4 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500 mr-2"
/>
Use custom rules
<button @click="openCustomRulesModal" title="Edit custom ignore rules" class="ml-2 p-0.5 hover:bg-gray-200 rounded text-xs">⚙️</button>
<button @click="openCustomRulesModal" title="Edit custom ignore rules" class="ml-2 p-0.5 hover:bg-gray-200 dark:hover:bg-slate-700 rounded text-xs">⚙️</button>
</label>
</div>

<h2 class="text-lg font-semibold text-gray-700 mb-2">Project Files</h2>
<div class="border border-gray-300 rounded min-h-[200px] bg-white text-sm overflow-auto max-h-[50vh]">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">Project Files</h2>
<div class="border border-gray-300 rounded min-h-[200px] bg-white dark:bg-slate-800 text-sm overflow-auto max-h-[50vh]">
<FileTree
v-if="fileTreeNodes && fileTreeNodes.length"
:nodes="fileTreeNodes"
:project-root="projectRoot"
@toggle-exclude="(node) => $emit('toggle-exclude', node)"
/>
<p v-else-if="projectRoot && !loadingError" class="p-2 text-xs text-gray-500">Loading tree...</p>
<p v-else-if="!projectRoot" class="p-2 text-xs text-gray-500">Select a project folder to see files.</p>
<p v-else-if="projectRoot && !loadingError" class="p-2 text-xs text-gray-500 dark:text-gray-400">Loading tree...</p>
<p v-else-if="!projectRoot" class="p-2 text-xs text-gray-500 dark:text-gray-400">Select a project folder to see files.</p>
<p v-if="loadingError" class="p-2 text-xs text-red-500">{{ loadingError }}</p>
</div>
</div>

<!-- Stepper Navigation (can remain if needed for overall app flow) -->
<div v-if="steps && steps.length > 0">
<h2 class="text-lg font-semibold text-gray-700 mb-2">Steps</h2>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">Steps</h2>
<div class="space-y-1">
<div v-for="step in steps" :key="step.id">
<button
@click="canNavigateToStep(step.id) ? $emit('navigate', step.id) : null"
:title="step.description"
:class="[
'w-full text-left px-3 py-2 rounded-md text-sm font-medium flex justify-between items-center',
currentStep === step.id ? 'bg-blue-100 text-blue-700' : (step.completed ? 'bg-green-50 text-green-700 hover:bg-green-100' : 'text-gray-600 hover:bg-gray-100'),
currentStep === step.id ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300' : (step.completed ? 'bg-green-50 text-green-700 hover:bg-green-100 dark:hover:bg-slate-800 dark:bg-green-900 dark:text-green-300' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800'),
canNavigateToStep(step.id) ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'
]"
:disabled="!canNavigateToStep(step.id)"
Expand All @@ -76,11 +76,22 @@
</div>
</div>
</div>

<!-- Theme Toggle at Bottom of Sidebar (Single Cycling Button) -->
<div class="mt-auto pt-4 border-t border-gray-300 dark:border-slate-700 flex justify-center">
<button
@click="cycleTheme"
:title="`Theme: ${themeMode} (click to cycle)`"
class="px-4 py-2 rounded transition-colors text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-800 text-lg"
>
{{ themeIcon }}
</button>
</div>
</aside>
</template>

<script setup>
import { defineProps, defineEmits, ref } from 'vue';
import { defineProps, defineEmits, ref, onMounted, computed } from 'vue';
import FileTree from './FileTree.vue'; // Import the existing FileTree
import CustomRulesModal from './CustomRulesModal.vue';
import { GetCustomIgnoreRules, SetCustomIgnoreRules } from '../../wailsjs/go/main/App';
Expand All @@ -106,6 +117,68 @@ const emit = defineEmits(['navigate', 'select-folder', 'toggle-gitignore', 'togg
const isCustomRulesModalVisible = ref(false);
const currentCustomRulesForModal = ref('');

// Theme selector state (light | dark | system)
const themeMode = ref('system');

// Computed theme icon
const themeIcon = computed(() => {
switch (themeMode.value) {
case 'light': return '☀️';
case 'dark': return '🌙';
case 'system': return '🖥️';
default: return '🖥️';
}
});

// Cycle through themes: system → light → dark → system
function cycleTheme() {
const cycle = ['system', 'light', 'dark'];
const currentIndex = cycle.indexOf(themeMode.value);
const nextIndex = (currentIndex + 1) % cycle.length;
const nextMode = cycle[nextIndex];
setTheme(nextMode);
}

function initThemeMode() {
try {
const saved = localStorage.getItem('shotgun_theme');
if (saved === 'light' || saved === 'dark' || saved === 'system') {
themeMode.value = saved;
} else {
themeMode.value = 'system';
}
} catch (e) {
// fallback
themeMode.value = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
}
}

function setTheme(mode) {
if (!mode || (mode !== 'light' && mode !== 'dark' && mode !== 'system')) return;
try {
if (typeof window.setShotgunTheme === 'function') {
window.setShotgunTheme(mode);
} else {
// fallback
if (mode === 'dark') document.documentElement.classList.add('dark');
else if (mode === 'light') document.documentElement.classList.remove('dark');
else {
// system
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) document.documentElement.classList.add('dark'); else document.documentElement.classList.remove('dark');
}
try { localStorage.setItem('shotgun_theme', mode); } catch (e) {}
}
themeMode.value = mode;
} catch (e) {
// ignore
}
}

onMounted(() => {
initThemeMode();
});

async function openCustomRulesModal() {
try {
currentCustomRulesForModal.value = await GetCustomIgnoreRules();
Expand Down
Loading