Compare commits

...

5 Commits

Author SHA1 Message Date
Julian Freeman
eb1d802f5b fix history restore 2025-12-01 11:10:08 -04:00
Julian Freeman
4d20f2075f improve response panel 2025-12-01 11:03:12 -04:00
Julian Freeman
8fc4dbbf93 modify sidebar 2025-12-01 10:39:35 -04:00
Julian Freeman
d6dda10d2f fix settings select 2025-12-01 09:49:56 -04:00
Julian Freeman
c5d82a710d custom select 2025-12-01 09:35:42 -04:00
10 changed files with 485 additions and 103 deletions

67
spec/feat-history-ui.md Normal file
View File

@@ -0,0 +1,67 @@
# Feature Upgrade: History UI Overhaul
**Context:**
We are polishing the "Sidebar" component of the "LiteRequest" application.
**Goal:** Improve the information density of the History list and add management features (Delete/Clear All).
## 1. UI/UX Changes
### 1.1 History Item Layout (Sidebar.vue)
* **Current State:** Shows Method, URL, and Timestamp.
* **New Layout:**
* **Remove:** Completely remove the Date/Time display to save space.
* **Flex Row:** The item should be a flex container (`flex items-center justify-between`).
* **Left Side (Info):**
* **Method Badge:** (Existing) e.g., "GET".
* **Status Badge (New):** Add a badge immediately after the Method.
* *Content:* The HTTP Status Code (e.g., 200, 404, 500).
* *Style:* Small, pill-shaped, lighter opacity background.
* *Color Logic:*
* 2xx: Text Green / Bg Green-500/20
* 3xx: Text Blue / Bg Blue-500/20
* 4xx: Text Orange / Bg Orange-500/20
* 5xx: Text Red / Bg Red-500/20
* **URL:** Truncate text if it's too long (`truncate` class).
* **Right Side (Actions):**
* **Delete Button (New):** A small "Trash" icon (`Trash2` from lucide-vue-next).
* *Behavior:* Visible on hover (group-hover) or always visible with low opacity.
* *Interaction:* Clicking this **must not** trigger the "Load Request" action of the list item (use `.stop` modifier).
### 1.2 "Clear All" Functionality
* **Location:** Add a "Clear" or "Trash" icon button in the **Header** of the Sidebar (next to the "History" tab label or the "LiteRequest" title).
* **Style:** Subtle button, turns red on hover.
* **Tooltip (Optional):** "Clear All History".
## 2. Logic & State Management
### 2.1 Store Updates (`stores/requestStore.ts`)
* Add two new actions:
1. `deleteHistoryItem(id: string)`: Removes a specific item from the `history` array by ID.
2. `clearHistory()`: Empties the `history` array completely.
* **Persistence:** Ensure `localStorage` is updated immediately after these actions.
## 3. Implementation Details
### A. Sidebar Component (`Sidebar.vue`)
* **Status Badge Implementation:**
* Create a helper function `getStatusColor(code)` inside the component to return the correct Tailwind classes.
* **Event Handling:**
* `<button @click.stop="store.deleteHistoryItem(item.id)">`
* The `.stop` modifier is critical to prevent opening the request while trying to delete it.
### B. Icons
* Import `Trash2` (for single item) and `Trash` (for clear all) from `lucide-vue-next`.
## 4. Implementation Steps for AI
Please generate the code updates in the following order:
1. **Store:** Add the `deleteHistoryItem` and `clearHistory` actions to `requestStore.ts`.
2. **Sidebar (Script):** Update the `<script setup>` to include the new icons and the status color helper function.
3. **Sidebar (Template):**
* Show the updated **Header** section (with "Clear All" button).
* Show the updated **History List Item** structure (Method + Status + URL + Delete Button).
---
**Instruction to AI:**
Generate the code to apply these UI improvements and functional additions to the History module.

View File

@@ -0,0 +1,59 @@
# UI Improvement: Resizable Panels & Copy Action
**Context:**
We are polishing the "Main Workspace" area of the LiteRequest application.
**Goal:**
1. Add a **"Copy Body" button** to the Response Panel for quick access to the result.
2. Make the vertical split between the **Request Panel** (top) and **Response Panel** (bottom) resizable by the user.
## 1. Feature: Response Copy Button
### 1.1 UI Changes
* **Location:** Inside the `ResponsePanel.vue`, in the **Meta Bar / Header** (the row displaying Status Code and Time), aligned to the right.
* **Icon:** Use the `Clipboard` icon from `lucide-vue-next`.
* **Interaction:**
* **Default:** Shows the `Clipboard` icon.
* **On Click:** Copies the content of the Response Body to the system clipboard.
* **Feedback:** After clicking, change the icon to `Check` (Checkmark) for 2 seconds to indicate success, then revert to `Clipboard`.
* **Style:** Small, ghost-style button (transparent background, changing color on hover).
### 1.2 Logic Implementation
* Use the browser API: `navigator.clipboard.writeText(props.body)`.
* Use a local ref `isCopied` (boolean) to handle the icon toggle state.
* Use `setTimeout` to reset `isCopied` back to `false` after 2000ms.
## 2. Feature: Resizable Split Pane
### 2.1 UI Changes
* **Location:** The parent component that contains `<RequestPanel />` and `<ResponsePanel />` (likely `App.vue` or `MainWorkspace.vue`).
* **Visuals:** Insert a **Resizer Handle** (a thin horizontal bar) between the two panels.
* *Height:* 4px - 6px.
* *Color:* `bg-slate-800` (hover: `bg-indigo-500`).
* *Cursor:* `cursor-row-resize`.
### 2.2 Logic Implementation
* **State:** Use a `ref` for `topPanelHeight` (initially `50%` or `50vh`).
* **Drag Logic:**
1. **`onMouseDown`** (on the Resizer): Set a `isDragging` flag to true. Add `mousemove` and `mouseup` event listeners to the `window` (global).
2. **`onMouseMove`** (on Window): Calculate the new percentage or pixel height based on `e.clientY` relative to the window height. Update `topPanelHeight`.
3. **`onMouseUp`** (on Window): Set `isDragging` to false. Remove the window event listeners.
* **Layout Application:**
* Apply `:style="{ height: topPanelHeight }"` to the **Request Panel** container.
* Apply `flex-1` (or calculated remaining height) to the **Response Panel** container.
* Ensure `min-height` is set (e.g., 200px) on both panels to prevent them from collapsing completely.
## 3. Implementation Steps for AI
Please generate the code updates in the following order:
1. **ResponsePanel.vue:**
* Update the template to include the Copy Button in the header.
* Add the script logic for `copyToClipboard`.
2. **Main Layout (App.vue or similar):**
* Refactor the template to wrap RequestPanel and ResponsePanel in the resizable structure.
* Implement the `useDraggable` logic (or manual event handlers) in `<script setup>`.
* Add the `<div class="resizer ...">` element styles.
---
**Instruction to AI:**
Generate the specific code to add the copy functionality and the resizable split view logic.

View File

@@ -1,78 +1,65 @@
<script setup lang="ts">
import { useRequestStore } from './stores/requestStore';
import RequestPanel from './components/RequestPanel.vue';
import ResponsePanel from './components/ResponsePanel.vue';
import MethodBadge from './components/MethodBadge.vue';
import Sidebar from './components/Sidebar.vue';
import SettingsModal from './components/SettingsModal.vue';
import { History, Layers, Zap, Settings } from 'lucide-vue-next';
import { ref } from 'vue';
import { ref, onUnmounted } from 'vue';
const store = useRequestStore();
const showSettings = ref(false);
const topPanelHeight = ref(50); // Percentage
const isDragging = ref(false);
const startDrag = () => {
isDragging.value = true;
window.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', stopDrag);
};
const onDrag = (e: MouseEvent) => {
if (!isDragging.value) return;
const containerHeight = window.innerHeight;
let newPercentage = (e.clientY / containerHeight) * 100;
// Clamp between 20% and 80%
if (newPercentage < 20) newPercentage = 20;
if (newPercentage > 80) newPercentage = 80;
topPanelHeight.value = newPercentage;
};
const stopDrag = () => {
isDragging.value = false;
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', stopDrag);
};
onUnmounted(() => {
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', stopDrag);
});
</script>
<template>
<div class="flex h-screen w-full bg-slate-950 text-slate-200 font-sans overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 flex-shrink-0 border-r border-slate-800 flex flex-col bg-slate-950">
<!-- Header -->
<div class="h-14 flex items-center px-4 border-b border-slate-800 gap-2">
<div class="bg-indigo-500/20 p-1.5 rounded-lg">
<Zap class="w-5 h-5 text-indigo-400" />
</div>
<span class="font-bold text-slate-100 tracking-tight">LiteRequest</span>
</div>
<!-- Nav Tabs -->
<div class="flex p-2 gap-1">
<button class="flex-1 flex items-center justify-center gap-2 py-1.5 text-xs font-medium bg-slate-900 text-slate-200 rounded border border-slate-800 shadow-sm">
<History class="w-3.5 h-3.5" /> History
</button>
<button class="flex-1 flex items-center justify-center gap-2 py-1.5 text-xs font-medium text-slate-500 hover:bg-slate-900 hover:text-slate-300 rounded transition-colors">
<Layers class="w-3.5 h-3.5" /> Collections
</button>
</div>
<!-- History List -->
<div class="flex-1 overflow-y-auto">
<div v-if="store.history.length === 0" class="p-8 text-center text-slate-600 text-xs">
No history yet. Make a request!
</div>
<div v-else class="flex flex-col">
<button
v-for="item in store.history"
:key="item.timestamp"
@click="store.loadRequest(item)"
class="text-left px-3 py-3 border-b border-slate-800/50 hover:bg-slate-900 transition-colors group flex flex-col gap-1.5"
>
<div class="flex items-center gap-2 overflow-hidden w-full">
<MethodBadge :method="item.method" />
<span class="text-xs text-slate-400 truncate font-mono opacity-75">{{ new Date(item.timestamp).toLocaleTimeString() }}</span>
</div>
<div class="text-sm text-slate-300 truncate px-1 font-medium" :title="item.url">
{{ item.url || 'No URL' }}
</div>
</button>
</div>
</div>
<!-- Settings -->
<div class="p-2 border-t border-slate-800">
<button
@click="showSettings = true"
class="w-full flex items-center gap-2 px-3 py-2 text-xs font-medium text-slate-400 hover:text-slate-200 hover:bg-slate-900 rounded transition-colors"
>
<Settings class="w-4 h-4" /> Settings
</button>
</div>
</aside>
<Sidebar @open-settings="showSettings = true" />
<!-- Main Workspace -->
<main class="flex-1 flex flex-col min-w-0 bg-slate-900">
<div class="flex-1 min-h-0">
<main class="flex-1 flex flex-col min-w-0 bg-slate-900 h-full relative">
<!-- Request Panel (Top) -->
<div class="min-h-0" :style="{ height: topPanelHeight + '%' }">
<RequestPanel />
</div>
<div class="h-1/2 border-t border-slate-800 min-h-0">
<!-- Resizer Handle -->
<div
class="h-1 bg-slate-800 hover:bg-indigo-500 cursor-row-resize transition-colors z-10 flex-shrink-0"
@mousedown="startDrag"
></div>
<!-- Response Panel (Bottom) -->
<div class="flex-1 min-h-0 overflow-hidden">
<ResponsePanel />
</div>
</main>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useRequestStore } from '../stores/requestStore';
import { ShieldCheck, Key, User, Lock, Fingerprint } from 'lucide-vue-next';
import CustomSelect from './CustomSelect.vue';
import { ref } from 'vue';
const store = useRequestStore();
@@ -21,15 +22,11 @@ const showApiKey = ref(false);
<!-- Type Selector -->
<div class="flex flex-col gap-2">
<label class="text-xs font-bold uppercase text-slate-500 tracking-wider">Authentication Type</label>
<div class="relative">
<select
v-model="store.activeRequest.auth.type"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none appearance-none cursor-pointer"
>
<option v-for="t in authTypes" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
<ShieldCheck class="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
</div>
<CustomSelect
v-model="store.activeRequest.auth.type"
:options="authTypes"
:full-width="true"
/>
</div>
<!-- Dynamic Content -->

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ChevronDown, Check } from 'lucide-vue-next';
interface Option {
value: string;
label?: string;
}
const props = defineProps<{
modelValue: string;
options: (string | Option)[];
placeholder?: string;
fullWidth?: boolean;
triggerClass?: string;
}>();
const emit = defineEmits(['update:modelValue']);
const isOpen = ref(false);
const containerRef = ref<HTMLElement | null>(null);
const normalizedOptions = computed(() => {
return props.options.map(opt => {
if (typeof opt === 'string') {
return { value: opt, label: opt };
}
return opt;
});
});
const selectedLabel = computed(() => {
const found = normalizedOptions.value.find(o => o.value === props.modelValue);
return found ? (found.label || found.value) : (props.modelValue || props.placeholder || '');
});
const toggle = () => { isOpen.value = !isOpen.value;
};
const select = (value: string) => {
emit('update:modelValue', value);
isOpen.value = false;
};
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
isOpen.value = false;
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
</script>
<template>
<div
ref="containerRef"
class="relative text-sm"
:class="{ 'w-full': fullWidth }"
>
<!-- Trigger -->
<button
type="button"
@click="toggle"
class="flex items-center justify-between gap-2 px-3 py-2 transition-colors text-slate-200 focus:outline-none"
:class="[
triggerClass ? triggerClass : 'bg-slate-950 border border-slate-800 rounded-lg hover:border-slate-700 focus:ring-1 focus:ring-indigo-500/50',
{ 'w-full': fullWidth, 'border-indigo-500 ring-1 ring-indigo-500/50': isOpen && !triggerClass }
]"
>
<span class="truncate font-bold">{{ selectedLabel }}</span>
<ChevronDown class="w-4 h-4 text-slate-500 transition-transform duration-200" :class="{ 'rotate-180': isOpen }" />
</button>
<!-- Dropdown Menu -->
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<div
v-if="isOpen"
class="absolute top-full left-0 mt-1 w-full min-w-[120px] max-h-60 overflow-y-auto bg-slate-900 border border-slate-800 rounded-lg shadow-xl z-50 py-1"
>
<button
v-for="opt in normalizedOptions"
:key="opt.value"
@click="select(opt.value)"
class="w-full text-left px-3 py-2 text-sm flex items-center justify-between group hover:bg-slate-800 transition-colors"
:class="modelValue === opt.value ? 'text-indigo-400 bg-indigo-500/10' : 'text-slate-300'"
>
<span class="truncate font-medium">{{ opt.label || opt.value }}</span>
<Check v-if="modelValue === opt.value" class="w-3.5 h-3.5 text-indigo-500" />
</button>
</div>
</transition>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import { useRequestStore } from '../stores/requestStore';
import { useSettingsStore } from '../stores/settingsStore';
import KeyValueEditor from './KeyValueEditor.vue';
import AuthPanel from './AuthPanel.vue';
import CustomSelect from './CustomSelect.vue';
import { invoke } from '@tauri-apps/api/core';
import { Codemirror } from 'vue-codemirror';
import { json } from '@codemirror/lang-json';
@@ -84,18 +85,17 @@ const executeRequest = async () => {
<div class="flex flex-col h-full bg-slate-900">
<!-- Top Bar -->
<div class="p-4 border-b border-slate-800 flex gap-2 items-center">
<div class="flex-1 flex items-center bg-slate-950 rounded-lg border border-slate-800 focus-within:border-indigo-500/50 focus-within:ring-1 focus-within:ring-indigo-500/50 transition-all overflow-hidden">
<select
v-model="store.activeRequest.method"
class="bg-slate-900 text-xs font-bold px-3 py-2 text-white border-r border-slate-800 focus:outline-none hover:bg-slate-800 cursor-pointer uppercase appearance-none"
>
<option v-for="m in methods" :key="m" :value="m" class="bg-slate-900 text-white">{{ m }}</option>
</select>
<div class="flex-1 flex items-center bg-slate-950 rounded-lg border border-slate-800 focus-within:border-indigo-500/50 focus-within:ring-1 focus-within:ring-indigo-500/50 transition-all overflow-visible z-20">
<CustomSelect
v-model="store.activeRequest.method"
:options="methods"
triggerClass="bg-transparent text-xs font-bold px-3 py-2 text-slate-200 border-r border-slate-800 focus:outline-none hover:bg-slate-900 cursor-pointer uppercase h-full min-w-[90px]"
/>
<input
type="text"
v-model="store.activeRequest.url"
placeholder="https://api.example.com/v1/users"
class="flex-1 bg-transparent border-none focus:ring-0 text-sm text-slate-200 px-3 py-2 placeholder-slate-600"
class="flex-1 bg-transparent border-none focus:ring-0 text-sm text-slate-200 px-3 py-2 placeholder-slate-600 h-full"
@keydown.enter="executeRequest"
>
</div>

View File

@@ -1,14 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useRequestStore } from '../stores/requestStore';
import { useSettingsStore } from '../stores/settingsStore';
import { Codemirror } from 'vue-codemirror';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view'; // Import EditorView
import { EditorView } from '@codemirror/view';
import { Clipboard, Check } from 'lucide-vue-next';
const store = useRequestStore();
const settings = useSettingsStore();
const isCopied = ref(false);
const extensions = computed(() => {
const theme = EditorView.theme({
@@ -43,22 +45,46 @@ const statusColor = computed(() => {
if (s >= 400) return 'text-rose-400';
return 'text-amber-400';
});
const copyToClipboard = async () => {
if (!formattedBody.value) return;
try {
await navigator.clipboard.writeText(formattedBody.value);
isCopied.value = true;
setTimeout(() => {
isCopied.value = false;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
</script>
<template>
<div class="flex flex-col h-full border-t border-slate-800 bg-slate-900">
<div v-if="store.activeRequest.response" class="flex flex-col h-full">
<!-- Meta Bar -->
<div class="px-4 py-2 border-b border-slate-800 flex gap-4 text-xs items-center bg-slate-950/50">
<div class="font-mono">
Status: <span :class="['font-bold', statusColor]">{{ store.activeRequest.response.status }}</span>
</div>
<div class="text-slate-500">
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
</div>
<div class="text-slate-500">
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
<div class="px-4 py-2 border-b border-slate-800 flex justify-between text-xs items-center bg-slate-950/50">
<div class="flex gap-4 items-center">
<div class="font-mono">
Status: <span :class="['font-bold', statusColor]">{{ store.activeRequest.response.status }}</span>
</div>
<div class="text-slate-500">
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
</div>
<div class="text-slate-500">
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
</div>
</div>
<button
@click="copyToClipboard"
class="p-1.5 text-slate-400 hover:text-indigo-400 hover:bg-indigo-500/10 rounded transition-colors"
title="Copy Body"
>
<Check v-if="isCopied" class="w-3.5 h-3.5" />
<Clipboard v-else class="w-3.5 h-3.5" />
</button>
</div>
<!-- Output -->

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { useSettingsStore } from '../stores/settingsStore';
import CustomSelect from './CustomSelect.vue';
import { X } from 'lucide-vue-next';
import { computed } from 'vue';
const emit = defineEmits(['close']);
const settings = useSettingsStore();
@@ -12,13 +14,20 @@ const fontOptions = [
"Monaco, Menlo, 'Ubuntu Mono', monospace",
"Arial, sans-serif"
];
const displayFontOptions = computed(() => {
return fontOptions.map(font => ({
value: font,
label: font.split(',')[0].replace(/['"]/g, '')
}));
});
</script>
<template>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="emit('close')">
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md animate-in fade-in zoom-in-95 duration-200">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/50">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/50 rounded-t-xl">
<h2 class="text-lg font-semibold text-slate-100">Editor Settings</h2>
<button
@click="emit('close')"
@@ -52,18 +61,14 @@ const fontOptions = [
<div class="space-y-3">
<label class="text-sm font-medium text-slate-300">Font Family</label>
<div class="space-y-2">
<select
<CustomSelect
v-model="settings.editorFontFamily"
class="w-full bg-slate-950 border border-slate-700 text-slate-300 text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 block p-2.5"
>
<option v-for="font in fontOptions" :key="font" :value="font">
{{ font.split(',')[0].replace(/['"]/g, '') }}
</option>
<option value="custom">Custom...</option>
</select>
:options="displayFontOptions"
:full-width="true"
placeholder="Select or enter font family"
/>
<input
v-if="settings.editorFontFamily === 'custom' || !fontOptions.includes(settings.editorFontFamily)"
v-if="!fontOptions.includes(settings.editorFontFamily)"
type="text"
v-model="settings.editorFontFamily"
placeholder="e.g. 'JetBrains Mono', monospace"
@@ -74,7 +79,7 @@ const fontOptions = [
</div>
<!-- Footer -->
<div class="px-6 py-4 bg-slate-950/50 border-t border-slate-800 flex justify-end">
<div class="px-6 py-4 bg-slate-950/50 border-t border-slate-800 flex justify-end rounded-b-xl">
<button
@click="emit('close')"
class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 text-sm font-medium rounded-lg transition-colors border border-slate-700"

104
src/components/Sidebar.vue Normal file
View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import { useRequestStore } from '../stores/requestStore';
import MethodBadge from './MethodBadge.vue';
import { Zap, Settings, Trash, Trash2, Plus } from 'lucide-vue-next';
const emit = defineEmits(['open-settings']);
const store = useRequestStore();
const getStatusColor = (code?: number) => {
if (!code) return 'text-slate-500 bg-slate-800/50';
if (code >= 200 && code < 300) return 'text-emerald-400 bg-emerald-500/10';
if (code >= 300 && code < 400) return 'text-blue-400 bg-blue-500/10';
if (code >= 400 && code < 500) return 'text-amber-400 bg-amber-500/10';
if (code >= 500) return 'text-rose-400 bg-rose-500/10';
return 'text-slate-400 bg-slate-500/10';
};
</script>
<template>
<aside class="w-64 flex-shrink-0 border-r border-slate-800 flex flex-col bg-slate-950">
<!-- Header -->
<div class="h-14 flex items-center justify-between px-4 border-b border-slate-800">
<div class="flex items-center gap-2">
<div class="bg-indigo-500/20 p-1.5 rounded-lg">
<Zap class="w-5 h-5 text-indigo-400" />
</div>
<span class="font-bold text-slate-100 tracking-tight">LiteRequest</span>
</div>
</div>
<!-- Action Buttons -->
<div class="flex p-2 gap-2 border-b border-slate-800/50">
<button
@click="store.resetActiveRequest()"
class="flex-1 flex items-center justify-center gap-2 py-1.5 text-xs font-medium bg-indigo-600 hover:bg-indigo-500 text-white rounded transition-all shadow-lg shadow-indigo-900/20"
>
<Plus class="w-3.5 h-3.5" /> New Request
</button>
<button
@click="store.clearHistory()"
class="flex-none flex items-center justify-center gap-2 px-3 py-1.5 text-xs font-medium bg-slate-800 hover:bg-red-500/10 hover:text-red-400 text-slate-400 rounded transition-all border border-slate-700 hover:border-red-500/50"
title="Clear History"
>
<Trash class="w-3.5 h-3.5" />
</button>
</div>
<!-- History List -->
<div class="flex-1 overflow-y-auto">
<div v-if="store.history.length === 0" class="p-8 text-center text-slate-600 text-xs">
No history yet. Make a request!
</div>
<div v-else class="flex flex-col">
<button
v-for="item in store.history"
:key="item.id"
@click="store.loadRequest(item)"
class="group flex flex-col text-left px-3 py-2 border-b border-slate-800/50 hover:bg-slate-900 transition-colors w-full"
>
<!-- First Row: Method, Status, Delete Button -->
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<MethodBadge :method="item.method" />
<!-- Status Badge -->
<span
v-if="item.response?.status"
class="text-[10px] font-mono font-bold px-1.5 py-0.5 rounded-full"
:class="getStatusColor(item.response?.status)"
>
{{ item.response.status }}
</span>
</div>
<!-- Delete Button -->
<button
@click.stop="store.deleteHistoryItem(item.id)"
class="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 text-slate-500 hover:text-red-400 hover:bg-slate-800 rounded-md cursor-pointer flex-shrink-0"
>
<Trash2 class="w-3.5 h-3.5" />
</button>
</div>
<!-- Second Row: URL -->
<div class="mt-1">
<span class="text-xs text-slate-400 truncate font-medium block" :title="item.url">
{{ item.url || '/' }}
</span>
</div>
</button>
</div>
</div>
<!-- Settings -->
<div class="p-2 border-t border-slate-800">
<button
@click="emit('open-settings')"
class="w-full flex items-center gap-2 px-3 py-2 text-xs font-medium text-slate-400 hover:text-slate-200 hover:bg-slate-900 rounded transition-colors"
>
<Settings class="w-4 h-4" /> Settings
</button>
</div>
</aside>
</template>

View File

@@ -85,11 +85,40 @@ export const useRequestStore = defineStore('request', () => {
history.value = [];
};
const deleteHistoryItem = (id: string) => {
const index = history.value.findIndex(item => item.id === id);
if (index !== -1) {
history.value.splice(index, 1);
}
};
const resetActiveRequest = () => {
activeRequest.value = {
id: crypto.randomUUID(),
method: 'GET',
url: '',
params: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
headers: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
body: '',
auth: {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
},
timestamp: Date.now(),
};
};
const loadRequest = (req: RequestData) => {
// Deep copy
const loaded = JSON.parse(JSON.stringify(req));
loaded.id = crypto.randomUUID();
loaded.response = undefined;
// loaded.response = undefined; // Keep response for history viewing
// Ensure at least one empty row for editing
if (loaded.params.length === 0) loaded.params.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
@@ -118,6 +147,8 @@ export const useRequestStore = defineStore('request', () => {
activeRequest,
addToHistory,
clearHistory,
loadRequest
deleteHistoryItem,
loadRequest,
resetActiveRequest
};
});