Compare commits

..

13 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
Julian Freeman
4f6c9f87e7 change icon 2025-12-01 09:28:35 -04:00
Julian Freeman
c2cf6fd644 fix font family settings 2025-12-01 09:03:01 -04:00
Julian Freeman
799c65a3d7 fix bug 2025-12-01 08:54:10 -04:00
Julian Freeman
18f023e6e3 fix bug, in the middle 2025-12-01 08:51:17 -04:00
Julian Freeman
ee586ae06f add auth 2025-12-01 08:41:44 -04:00
Julian Freeman
c7c7b5fc4b add font settings (but font family doesn't work) 2025-12-01 08:13:27 -04:00
Julian Freeman
813229aae9 fix 2025-12-01 07:28:26 -04:00
Julian Freeman
7541190a43 little modification 2025-12-01 07:24:35 -04:00
79 changed files with 1127 additions and 117 deletions

View File

@@ -1,7 +1,7 @@
# Tauri + Vue + TypeScript
# LiteRequest
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
A simple REST API client.
## Recommended IDE Setup
Generated by Gemini CLI.
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
App icon from https://iconarchive.com

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -2,9 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue + Typescript App</title>
<title>LiteRequest</title>
</head>
<body>

View File

@@ -1,5 +1,5 @@
{
"name": "rest-client",
"name": "lite-request",
"private": true,
"version": "0.1.0",
"type": "module",

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

61
spec/bug-fix-1.md Normal file
View File

@@ -0,0 +1,61 @@
# Bug Fixes & UI Polishing Tasks
**Context:**
We are refining the "LiteRequest" application. The core logic works, but there are specific UI bugs and a functional regression in the Settings module.
**Goal:** Fix visual inconsistencies and ensure the Settings (Font Size/Family) are correctly applied to the CodeMirror editor.
## Task List
### 1. Reorder Tabs in Request Panel
* **Issue:** The **"Auth"** tab is currently the first tab.
* **Fix:** Move the **"Auth"** tab to the **last position**.
* **Order:** `Params` -> `Headers` -> `Body` -> `Auth`.
* **Action:** Update `RequestPanel.vue` (both the tab navigation list and the conditional rendering of the content).
### 2. Fix Method Dropdown Styling (Dark Mode)
* **Issue:** The HTML `<select>` dropdown for HTTP Methods shows a white background when expanded, which clashes with the dark theme.
* **Fix:**
* Apply Tailwind classes `bg-slate-900` and `text-white` (or `text-slate-200`) explicitly to the `<option>` tags within the `<select>`.
* Ensure the `<select>` itself has `bg-slate-900`, `border-slate-700`, and `text-white`.
* *Note:* Browser native dropdowns are hard to style perfectly, but ensuring the background color is set on options fixes the glaring white box issue.
### 3. Fix Editor Background Color Mismatch
* **Issue:** The CodeMirror editor background is currently a generic gray (likely from a default theme), which looks disconnected from the App's `slate-900` / `slate-950` theme.
* **Fix:**
* In `Editor.vue`, modify the CodeMirror theme configuration.
* **Transparent Background:** Set the CodeMirror editor's background to `transparent` so it adopts the parent container's color.
* **Code Snippet Hint:**
```javascript
EditorView.theme({
"&": { backgroundColor: "transparent !important", height: "100%" },
".cm-gutters": { backgroundColor: "transparent !important" } // Match gutter too
})
```
* Ensure the parent container in `Editor.vue` has the correct Tailwind class (e.g., `bg-slate-900`).
### 4. Fix Font Settings Not Applying
* **Issue:** Changing Font Size or Family in Settings does not update the Editor view.
* **Root Cause:** CodeMirror 6 does not automatically react to CSS style changes on the parent container for everything (especially if a Theme is hardcoded).
* **Fix:**
* Update `Editor.vue`.
* Use a **Vue `watch` effect** to listen to changes in `settingsStore.editorFontSize` and `settingsStore.editorFontFamily`.
* **Implementation Strategy:**
* Instead of trying to update the theme, simply apply a reactive `style` object to the **wrapper `<div>`** of the CodeMirror component.
* **Crucial Step:** Ensure the CodeMirror theme allows inheritance.
* **Alternative (Better):** Re-dispatch a CodeMirror Effect or update the `EditorView` style via a `Compartment` if the wrapper style approach fails.
* *Simplified approach for Vue:* Bind `:style="{ fontSize: store.fontSize + 'px', fontFamily: store.fontFamily }"` to the outer div, and ensure the `.cm-editor` CSS allows inheritance (usually it does by default, but confirm standard text styles).
## Implementation Instructions for AI
Please generate the code corrections in the following order:
1. **`RequestPanel.vue`:** Show the updated template with the reordered Tabs.
2. **`RequestPanel.vue` (or wherever the Method Selector is):** Show the CSS/Tailwind fix for the Dropdown and Options.
3. **`Editor.vue`:**
* Show the updated CodeMirror configuration (making background transparent).
* Show the logical fix to apply Font Settings (using `watch` or reactive styles).
---
**Instruction to AI:**
Please generate the corrected code blocks for these specific files to resolve the visual bugs and settings regression.

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.

76
spec/feature-auth.md Normal file
View File

@@ -0,0 +1,76 @@
# Feature Request: Authentication Support
**Context:**
We are upgrading the "LiteRequest" Tauri + Vue application. Currently, it supports Headers, Params, and Body.
**Goal:** Add a dedicated **"Auth"** tab in the Request Configuration area to support common authentication methods.
## 1. Functional Requirements
### 1.1 Supported Auth Types
The user should be able to select one of the following from a dropdown:
1. **No Auth** (Default)
2. **Basic Auth**: Inputs for `Username` and `Password`.
3. **Bearer Token**: Input for `Token` (e.g., JWT).
4. **API Key**: Inputs for `Key`, `Value`, and a selection for `Add To` ("Header" or "Query Params").
### 1.2 State Management
* Update `stores/requestStore.ts` to include an `auth` state object.
* **Structure:**
```typescript
state: {
// ... existing state
auth: {
type: 'none', // 'none' | 'basic' | 'bearer' | 'api_key'
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' } // addTo: 'header' | 'query'
}
}
```
* **Persistence:** These values should be saved to `localStorage` along with the request history (careful note: in a real app we would encrypt this, but for this local MVP, plain text storage is acceptable).
## 2. UI/UX Design
### 2.1 Request Panel Update
* In `RequestPanel.vue`, add a new Tab labeled **"Auth"** (place it first, before "Params").
* **Layout:**
* **Type Selector:** A styled `<select>` or dropdown menu at the top of the tab content.
* **Dynamic Form:**
* If **No Auth**: Show a subtle message "This request does not use any authentication."
* If **Basic Auth**: Two fields (Username, Password). *Password field must use type="password" with a toggle to show/hide.*
* If **Bearer Token**: One large text area or input for the Token. Prefix it visually with "Bearer ".
* If **API Key**: Three inputs: Key (text), Value (text/password), Add To (Radio button or Select).
### 2.2 Aesthetic Guidelines
* Maintain the **Dark Mode** (Slate/Zinc) theme.
* Inputs should look identical to the existing inputs in the "Headers" tab (rounded, dark background, border-slate-700).
* Use `lucide-vue-next` icons (e.g., `ShieldCheck` icon for the Tab label).
## 3. Technical Implementation Details
### A. Frontend Logic (`requestStore.ts` & Component)
* The Frontend needs to package this Auth data and send it to the Rust backend.
* **Important:** Do *not* manually inject these into the `headers` list in the frontend UI (to avoid state sync issues). Pass the auth configuration as a separate object to the Rust command.
### B. Backend Logic (`main.rs`)
* Update the `execute_request` Tauri command signature to accept an `auth` argument.
* **Logic Update:**
* Use `reqwest` built-in methods:
* **Basic:** `.basic_auth(username, Some(password))`
* **Bearer:** `.bearer_auth(token)`
* **API Key:**
* If `addTo == "header"`: Manually `.header(key, value)`
* If `addTo == "query"`: Manually append to the query params.
## 4. Implementation Steps for AI
Please generate the code updates in the following order:
1. **Store Update:** Modify `stores/requestStore.ts` to add the `auth` state structure.
2. **UI Component:** Create a new component `components/AuthPanel.vue` (or code to be added inside `RequestPanel.vue`) that handles the form switching and inputs.
3. **Rust Backend:** Update `main.rs`. Define the `Auth` struct (using `serde::Deserialize`) and update the `execute_request` logic to apply the auth.
4. **Integration:** Show how to update the `execute_request` invocation in the frontend to pass this new data.
---
**Instruction to AI:**
Generate the code to implement these Authentication features. Ensure the UI matches the existing modern style (Tailwind CSS).

View File

@@ -0,0 +1,59 @@
# Feature Request: Editor Appearance Settings
**Context:**
We are continuing the development of the "LiteRequest" Tauri + Vue application. The application currently functions as a REST Client with a two-column layout.
**Goal:** Implement a "Settings" feature that allows users to customize the **Font Size** and **Font Family** of the CodeMirror editors (both Request Body and Response Output).
## 1. Functional Requirements
### 1.1 Settings Persistence
* Create a new Pinia store module (e.g., `stores/settingsStore.ts`) to manage application preferences.
* **State fields:**
* `editorFontSize`: Number (Default: 14).
* `editorFontFamily`: String (Default: "Fira Code, Consolas, monospace").
* **Persistence:** Use `localStorage` to save these preferences so they survive an app restart.
### 1.2 User Interface (Settings Modal)
* **Entry Point:** Add a "Settings" (Gear icon) button to the bottom of the **Sidebar**.
* **Modal Design:**
* Create a `SettingsModal.vue` component.
* Style: Centered modal with a dark backdrop (backdrop-blur). Matches the existing Slate/Zinc dark theme.
* **Controls:**
* **Font Size:** A number input or a range slider (e.g., 10px to 24px).
* **Font Family:** A text input (allowing users to type their installed fonts) OR a simple dropdown with common coding fonts.
* **Actions:** "Close" button (Changes should apply reactively/immediately, no need for a "Save" button).
### 1.3 Editor Integration
* Update the existing `Editor.vue` component (the CodeMirror wrapper).
* It must subscribe to the `settingsStore`.
* Apply the `fontSize` and `fontFamily` dynamically to the editor container or the CodeMirror instance.
## 2. Technical Implementation Details
### A. Store (`stores/settingsStore.ts`)
* Define the store using Pinia's Composition API.
* Use `useLocalStorage` from `@vueuse/core` (if available) or manual `localStorage.setItem` within a watcher.
### B. Sidebar Update (`Sidebar.vue`)
* Import `SettingsIcon` from `lucide-vue-next`.
* Add the button at the absolute bottom of the sidebar container.
* Add logic to toggle the visibility of the `SettingsModal`.
### C. Editor Update (`Editor.vue`)
* Bind the styles dynamically.
* *Hint for Implementation:* The easiest way to style CodeMirror 6 dynamically is to apply CSS variables to the wrapper `<div>` and ensure the CodeMirror theme uses those variables, or simply apply inline styles to the wrapper which CodeMirror usually inherits.
* Example: `<div :style="{ fontSize: settings.editorFontSize + 'px', fontFamily: settings.editorFontFamily }">`
## 3. Implementation Steps for AI
Please generate the code updates in the following order:
1. **Store:** Create `stores/settingsStore.ts`.
2. **Component (New):** Create `components/SettingsModal.vue`.
3. **Component (Update):** Show the modified code for `Sidebar.vue` (adding the button).
4. **Component (Update):** Show the modified code for `App.vue` (to include the Modal) or wherever the Modal should be placed.
5. **Component (Update):** Show the modified code for `Editor.vue` (applying the styles).
---
**Instruction to AI:**
Please generate the necessary code to implement these features based on the existing "LiteRequest" project structure. Focus on maintaining the modern dark aesthetic.

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.

26
src-tauri/Cargo.lock generated
View File

@@ -1956,6 +1956,19 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "lite-request"
version = "0.1.0"
dependencies = [
"reqwest",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tokio",
]
[[package]]
name = "litemap"
version = "0.8.1"
@@ -3129,19 +3142,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rest-client"
version = "0.1.0"
dependencies = [
"reqwest",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tokio",
]
[[package]]
name = "ring"
version = "0.17.14"

View File

@@ -1,7 +1,7 @@
[package]
name = "rest-client"
name = "lite-request"
version = "0.1.0"
description = "A Tauri App"
description = "A Simple REST API Client"
authors = ["you"]
edition = "2021"
@@ -11,7 +11,7 @@ edition = "2021"
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "rest_client_lib"
name = "lite_request_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -10,10 +10,33 @@ struct HttpResponse {
time_elapsed: u128, // milliseconds
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
#[derive(Serialize, Deserialize, Debug)]
struct BasicAuth {
username: String,
password: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct BearerAuth {
token: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct ApiKeyAuth {
key: String,
value: String,
#[serde(rename = "addTo")]
add_to: String, // "header" or "query"
}
#[derive(Serialize, Deserialize, Debug)]
struct AuthConfig {
#[serde(rename = "type")]
auth_type: String, // "none", "basic", "bearer", "api_key"
basic: BasicAuth,
bearer: BearerAuth,
#[serde(rename = "apiKey")]
api_key: ApiKeyAuth,
}
#[tauri::command]
@@ -23,6 +46,7 @@ async fn execute_request(
headers: HashMap<String, String>,
body: Option<String>,
query_params: Option<HashMap<String, String>>,
auth: Option<AuthConfig>,
) -> Result<HttpResponse, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
@@ -43,6 +67,31 @@ async fn execute_request(
request_builder = request_builder.header(key, value);
}
// Add Auth
if let Some(auth_config) = auth {
match auth_config.auth_type.as_str() {
"basic" => {
request_builder = request_builder.basic_auth(
auth_config.basic.username,
Some(auth_config.basic.password),
);
}
"bearer" => {
request_builder = request_builder.bearer_auth(auth_config.bearer.token);
}
"api_key" => {
let key = auth_config.api_key.key;
let value = auth_config.api_key.value;
if auth_config.api_key.add_to == "header" {
request_builder = request_builder.header(key, value);
} else if auth_config.api_key.add_to == "query" {
request_builder = request_builder.query(&[(key, value)]);
}
}
_ => {} // "none" or unknown
}
}
// Add Body
if let Some(b) = body {
if !b.is_empty() {
@@ -79,7 +128,7 @@ async fn execute_request(
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet, execute_request])
.invoke_handler(tauri::generate_handler![execute_request])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
rest_client_lib::run()
lite_request_lib::run()
}

View File

@@ -1,8 +1,8 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "rest-client",
"productName": "LiteRequest",
"version": "0.1.0",
"identifier": "top.volan.rest-client",
"identifier": "top.volan.lite-request",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
@@ -12,9 +12,9 @@
"app": {
"windows": [
{
"title": "rest-client",
"width": 800,
"height": 600
"title": "LiteRequest",
"width": 1200,
"height": 800
}
],
"security": {

View File

@@ -1,67 +1,69 @@
<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 { History, Layers, Zap } from 'lucide-vue-next';
import Sidebar from './components/Sidebar.vue';
import SettingsModal from './components/SettingsModal.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>
</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>
<SettingsModal v-if="showSettings" @close="showSettings = false" />
</div>
</template>

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,150 @@
<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();
const authTypes = [
{ value: 'none', label: 'No Auth' },
{ value: 'basic', label: 'Basic Auth' },
{ value: 'bearer', label: 'Bearer Token' },
{ value: 'api_key', label: 'API Key' },
];
const showPassword = ref(false);
const showApiKey = ref(false);
</script>
<template>
<div class="h-full p-4 flex flex-col gap-6 overflow-y-auto text-slate-300">
<!-- Type Selector -->
<div class="flex flex-col gap-2">
<label class="text-xs font-bold uppercase text-slate-500 tracking-wider">Authentication Type</label>
<CustomSelect
v-model="store.activeRequest.auth.type"
:options="authTypes"
:full-width="true"
/>
</div>
<!-- Dynamic Content -->
<div class="flex-1">
<!-- No Auth -->
<div v-if="store.activeRequest.auth.type === 'none'" class="flex flex-col items-center justify-center h-40 text-slate-600 gap-3">
<ShieldCheck class="w-12 h-12 opacity-20" />
<p class="text-sm">This request does not use any authentication.</p>
</div>
<!-- Basic Auth -->
<div v-else-if="store.activeRequest.auth.type === 'basic'" class="flex flex-col gap-4 animate-fade-in">
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Username</label>
<div class="relative">
<User class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
type="text"
v-model="store.activeRequest.auth.basic.username"
placeholder="username"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Password</label>
<div class="relative">
<Lock class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
:type="showPassword ? 'text' : 'password'"
v-model="store.activeRequest.auth.basic.password"
placeholder="password"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
<button
@click="showPassword = !showPassword"
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500 hover:text-indigo-400 font-medium"
>
{{ showPassword ? 'Hide' : 'Show' }}
</button>
</div>
</div>
</div>
<!-- Bearer Token -->
<div v-else-if="store.activeRequest.auth.type === 'bearer'" class="flex flex-col gap-4 animate-fade-in">
<div class="flex flex-col gap-1 h-full">
<label class="text-xs font-medium text-slate-400">Token</label>
<div class="relative flex-1 min-h-[120px]">
<div class="absolute top-3 left-3 text-xs font-mono text-slate-500 select-none">Bearer</div>
<textarea
v-model="store.activeRequest.auth.bearer.token"
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
class="w-full h-full bg-slate-950 border border-slate-800 rounded-lg pl-14 pr-3 py-2 text-sm font-mono text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none resize-none placeholder-slate-700 leading-relaxed"
></textarea>
</div>
</div>
</div>
<!-- API Key -->
<div v-else-if="store.activeRequest.auth.type === 'api_key'" class="flex flex-col gap-4 animate-fade-in">
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Key</label>
<div class="relative">
<Key class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
type="text"
v-model="store.activeRequest.auth.apiKey.key"
placeholder="x-api-key"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Value</label>
<div class="relative">
<Fingerprint class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
:type="showApiKey ? 'text' : 'password'"
v-model="store.activeRequest.auth.apiKey.value"
placeholder="value"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
<button
@click="showApiKey = !showApiKey"
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500 hover:text-indigo-400 font-medium"
>
{{ showApiKey ? 'Hide' : 'Show' }}
</button>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Add To</label>
<div class="flex gap-4 mt-1">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="store.activeRequest.auth.apiKey.addTo" value="header" class="accent-indigo-500" />
<span class="text-sm text-slate-300 group-hover:text-white">Header</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="store.activeRequest.auth.apiKey.addTo" value="query" class="accent-indigo-500" />
<span class="text-sm text-slate-300 group-hover:text-white">Query Params</span>
</label>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.animate-fade-in {
animation: fadeIn 0.2s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

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

@@ -1,18 +1,38 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, computed } from 'vue';
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';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
import { Play, Loader2 } from 'lucide-vue-next';
const store = useRequestStore();
const settings = useSettingsStore();
const activeTab = ref('params');
const isLoading = ref(false);
const extensions = [json(), oneDark];
const extensions = computed(() => {
const theme = EditorView.theme({
"&": {
backgroundColor: "transparent !important",
height: "100%",
},
".cm-content, .cm-gutter": {
fontFamily: `${settings.editorFontFamily} !important`
},
".cm-gutters": {
backgroundColor: "transparent !important"
}
});
return [json(), oneDark, theme];
});
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
@@ -36,6 +56,7 @@ const executeRequest = async () => {
headers,
body: store.activeRequest.body || null,
queryParams: params,
auth: store.activeRequest.auth,
});
// Update Store
@@ -64,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
<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"
class="bg-transparent text-xs font-bold px-3 py-2 text-slate-300 border-r border-slate-800 focus:outline-none hover:bg-slate-900 cursor-pointer uppercase appearance-none"
>
<option v-for="m in methods" :key="m" :value="m">{{ m }}</option>
</select>
: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>
@@ -93,7 +113,7 @@ const executeRequest = async () => {
<!-- Configuration Tabs -->
<div class="flex border-b border-slate-800">
<button
v-for="tab in ['params', 'headers', 'body']"
v-for="tab in ['params', 'headers', 'body', 'auth']"
:key="tab"
@click="activeTab = tab"
class="px-4 py-2 text-xs font-medium uppercase tracking-wide border-b-2 transition-colors"
@@ -121,13 +141,22 @@ const executeRequest = async () => {
<Codemirror
v-model="store.activeRequest.body"
placeholder="Request Body (JSON)"
:style="{ height: '100%' }"
:style="{
height: '100%',
fontSize: settings.editorFontSize + 'px',
fontFamily: settings.editorFontFamily
}"
:autofocus="true"
:indent-with-tab="true"
:tab-size="2"
:extensions="extensions"
/>
</div>
<KeepAlive>
<AuthPanel
v-if="activeTab === 'auth'"
/>
</KeepAlive>
</div>
</div>
</template>

View File

@@ -1,14 +1,33 @@
<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 { Clipboard, Check } from 'lucide-vue-next';
const store = useRequestStore();
const settings = useSettingsStore();
const isCopied = ref(false);
const extensions = [json(), oneDark, EditorView.editable.of(false)];
const extensions = computed(() => {
const theme = EditorView.theme({
"&": {
backgroundColor: "transparent !important",
height: "100%"
},
".cm-content, .cm-gutter": {
fontFamily: `${settings.editorFontFamily} !important`
},
".cm-gutters": {
backgroundColor: "transparent !important"
}
});
return [json(), oneDark, theme, EditorView.editable.of(false)];
});
const formattedBody = computed(() => {
if (!store.activeRequest.response) return '';
@@ -26,29 +45,57 @@ 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 -->
<div class="flex-1 overflow-hidden">
<Codemirror
:model-value="formattedBody"
:style="{ height: '100%' }"
:style="{
height: '100%',
fontSize: settings.editorFontSize + 'px',
fontFamily: settings.editorFontFamily
}"
:extensions="extensions"
/>
</div>

View File

@@ -0,0 +1,92 @@
<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();
const fontOptions = [
"Fira Code, Consolas, monospace",
"Consolas, 'Courier New', monospace",
"'Courier New', Courier, monospace",
"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 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 rounded-t-xl">
<h2 class="text-lg font-semibold text-slate-100">Editor Settings</h2>
<button
@click="emit('close')"
class="text-slate-400 hover:text-white transition-colors p-1 hover:bg-slate-800 rounded-md"
>
<X class="w-5 h-5" />
</button>
</div>
<!-- Content -->
<div class="p-6 space-y-6">
<!-- Font Size -->
<div class="space-y-3">
<div class="flex justify-between items-center">
<label class="text-sm font-medium text-slate-300">Font Size</label>
<span class="text-xs font-mono text-slate-500 bg-slate-950 px-2 py-1 rounded border border-slate-800">
{{ settings.editorFontSize }}px
</span>
</div>
<input
type="range"
v-model.number="settings.editorFontSize"
min="10"
max="24"
step="1"
class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500 hover:accent-indigo-400"
>
</div>
<!-- Font Family -->
<div class="space-y-3">
<label class="text-sm font-medium text-slate-300">Font Family</label>
<div class="space-y-2">
<CustomSelect
v-model="settings.editorFontFamily"
:options="displayFontOptions"
:full-width="true"
placeholder="Select or enter font family"
/>
<input
v-if="!fontOptions.includes(settings.editorFontFamily)"
type="text"
v-model="settings.editorFontFamily"
placeholder="e.g. 'JetBrains Mono', monospace"
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 font-mono"
>
</div>
</div>
</div>
<!-- Footer -->
<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"
>
Done
</button>
</div>
</div>
</div>
</template>

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

@@ -8,6 +8,13 @@ export interface KeyValue {
enabled: boolean;
}
export interface AuthState {
type: 'none' | 'basic' | 'bearer' | 'api_key';
basic: { username: '', password: '' };
bearer: { token: '' };
apiKey: { key: '', value: '', addTo: 'header' | 'query' };
}
export interface RequestData {
id: string;
method: string;
@@ -15,6 +22,7 @@ export interface RequestData {
params: KeyValue[];
headers: KeyValue[];
body: string;
auth: AuthState;
response?: {
status: number;
headers: Record<string, string>;
@@ -39,6 +47,12 @@ export const useRequestStore = defineStore('request', () => {
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
body: '',
auth: {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
},
timestamp: Date.now(),
});
@@ -71,16 +85,55 @@ 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 });
if (loaded.headers.length === 0) loaded.headers.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
// Ensure auth object exists (migration for old history)
if (!loaded.auth) {
loaded.auth = {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
};
}
activeRequest.value = loaded;
};
@@ -94,6 +147,8 @@ export const useRequestStore = defineStore('request', () => {
activeRequest,
addToHistory,
clearHistory,
loadRequest
deleteHistoryItem,
loadRequest,
resetActiveRequest
};
});

View File

@@ -0,0 +1,27 @@
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
export const useSettingsStore = defineStore('settings', () => {
// Initialize from localStorage or defaults
const editorFontSize = ref<number>(
Number(localStorage.getItem('editorFontSize')) || 14
);
const editorFontFamily = ref<string>(
localStorage.getItem('editorFontFamily') || 'Fira Code, Consolas, monospace'
);
// Watchers for persistence
watch(editorFontSize, (val) => {
localStorage.setItem('editorFontSize', val.toString());
});
watch(editorFontFamily, (val) => {
localStorage.setItem('editorFontFamily', val);
});
return {
editorFontSize,
editorFontFamily,
};
});

View File

@@ -9,3 +9,30 @@ body {
#app {
@apply h-full w-full;
}
/* Custom Scrollbar Styles */
/* For Webkit browsers (Chrome, Safari) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b; /* slate-800 */
}
::-webkit-scrollbar-thumb {
background: #475569; /* slate-600 */
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b; /* slate-500 */
}
/* For Firefox (requires 'scrollbar-width' and 'scrollbar-color' on elements) */
/* Applying to all elements for broader coverage */
* {
scrollbar-width: thin;
scrollbar-color: #475569 #1e293b; /* thumb track */
}