improve response panel
This commit is contained in:
59
spec/fix-response-panel.md
Normal file
59
spec/fix-response-panel.md
Normal 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.
|
||||||
49
src/App.vue
49
src/App.vue
@@ -3,9 +3,41 @@ import RequestPanel from './components/RequestPanel.vue';
|
|||||||
import ResponsePanel from './components/ResponsePanel.vue';
|
import ResponsePanel from './components/ResponsePanel.vue';
|
||||||
import Sidebar from './components/Sidebar.vue';
|
import Sidebar from './components/Sidebar.vue';
|
||||||
import SettingsModal from './components/SettingsModal.vue';
|
import SettingsModal from './components/SettingsModal.vue';
|
||||||
import { ref } from 'vue';
|
import { ref, onUnmounted } from 'vue';
|
||||||
|
|
||||||
const showSettings = ref(false);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -14,11 +46,20 @@ const showSettings = ref(false);
|
|||||||
<Sidebar @open-settings="showSettings = true" />
|
<Sidebar @open-settings="showSettings = true" />
|
||||||
|
|
||||||
<!-- Main Workspace -->
|
<!-- Main Workspace -->
|
||||||
<main class="flex-1 flex flex-col min-w-0 bg-slate-900">
|
<main class="flex-1 flex flex-col min-w-0 bg-slate-900 h-full relative">
|
||||||
<div class="flex-1 min-h-0">
|
<!-- Request Panel (Top) -->
|
||||||
|
<div class="min-h-0" :style="{ height: topPanelHeight + '%' }">
|
||||||
<RequestPanel />
|
<RequestPanel />
|
||||||
</div>
|
</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 />
|
<ResponsePanel />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRequestStore } from '../stores/requestStore';
|
import { useRequestStore } from '../stores/requestStore';
|
||||||
import { useSettingsStore } from '../stores/settingsStore';
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
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 store = useRequestStore();
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
const isCopied = ref(false);
|
||||||
|
|
||||||
const extensions = computed(() => {
|
const extensions = computed(() => {
|
||||||
const theme = EditorView.theme({
|
const theme = EditorView.theme({
|
||||||
@@ -43,22 +45,46 @@ const statusColor = computed(() => {
|
|||||||
if (s >= 400) return 'text-rose-400';
|
if (s >= 400) return 'text-rose-400';
|
||||||
return 'text-amber-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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full border-t border-slate-800 bg-slate-900">
|
<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">
|
<div v-if="store.activeRequest.response" class="flex flex-col h-full">
|
||||||
<!-- Meta Bar -->
|
<!-- 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="px-4 py-2 border-b border-slate-800 flex justify-between text-xs items-center bg-slate-950/50">
|
||||||
<div class="font-mono">
|
<div class="flex gap-4 items-center">
|
||||||
Status: <span :class="['font-bold', statusColor]">{{ store.activeRequest.response.status }}</span>
|
<div class="font-mono">
|
||||||
</div>
|
Status: <span :class="['font-bold', statusColor]">{{ store.activeRequest.response.status }}</span>
|
||||||
<div class="text-slate-500">
|
</div>
|
||||||
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
|
<div class="text-slate-500">
|
||||||
</div>
|
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
|
||||||
<div class="text-slate-500">
|
</div>
|
||||||
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
|
<div class="text-slate-500">
|
||||||
|
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Output -->
|
<!-- Output -->
|
||||||
|
|||||||
Reference in New Issue
Block a user