Compare commits
4 Commits
813229aae9
...
799c65a3d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
799c65a3d7 | ||
|
|
18f023e6e3 | ||
|
|
ee586ae06f | ||
|
|
c7c7b5fc4b |
61
spec/bug-fix-1.md
Normal file
61
spec/bug-fix-1.md
Normal 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.
|
||||
76
spec/feature-auth.md
Normal file
76
spec/feature-auth.md
Normal 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).
|
||||
59
spec/feature-editor-settings.md
Normal file
59
spec/feature-editor-settings.md
Normal 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.
|
||||
@@ -10,6 +10,35 @@ struct HttpResponse {
|
||||
time_elapsed: u128, // milliseconds
|
||||
}
|
||||
|
||||
#[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]
|
||||
async fn execute_request(
|
||||
method: String,
|
||||
@@ -17,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))
|
||||
@@ -37,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() {
|
||||
|
||||
17
src/App.vue
17
src/App.vue
@@ -3,9 +3,12 @@ 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 SettingsModal from './components/SettingsModal.vue';
|
||||
import { History, Layers, Zap, Settings } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const store = useRequestStore();
|
||||
const showSettings = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -52,6 +55,16 @@ const store = useRequestStore();
|
||||
</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>
|
||||
|
||||
<!-- Main Workspace -->
|
||||
@@ -63,5 +76,7 @@ const store = useRequestStore();
|
||||
<ResponsePanel />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SettingsModal v-if="showSettings" @close="showSettings = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
153
src/components/AuthPanel.vue
Normal file
153
src/components/AuthPanel.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequestStore } from '../stores/requestStore';
|
||||
import { ShieldCheck, Key, User, Lock, Fingerprint } from 'lucide-vue-next';
|
||||
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>
|
||||
<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>
|
||||
</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>
|
||||
@@ -1,18 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRequestStore } from '../stores/requestStore';
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import KeyValueEditor from './KeyValueEditor.vue';
|
||||
import AuthPanel from './AuthPanel.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 transparentTheme = EditorView.theme({
|
||||
"&": {
|
||||
backgroundColor: "transparent !important",
|
||||
height: "100%"
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "transparent !important"
|
||||
}
|
||||
});
|
||||
|
||||
const extensions = [json(), oneDark, transparentTheme];
|
||||
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||||
|
||||
@@ -36,6 +50,7 @@ const executeRequest = async () => {
|
||||
headers,
|
||||
body: store.activeRequest.body || null,
|
||||
queryParams: params,
|
||||
auth: store.activeRequest.auth,
|
||||
});
|
||||
|
||||
// Update Store
|
||||
@@ -67,9 +82,9 @@ const executeRequest = async () => {
|
||||
<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-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"
|
||||
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">{{ m }}</option>
|
||||
<option v-for="m in methods" :key="m" :value="m" class="bg-slate-900 text-white">{{ m }}</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
@@ -93,7 +108,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 +136,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>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } 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 } from '@codemirror/view'; // Import EditorView
|
||||
|
||||
const store = useRequestStore();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
const extensions = [json(), oneDark, EditorView.editable.of(false)];
|
||||
const transparentTheme = EditorView.theme({
|
||||
"&": {
|
||||
backgroundColor: "transparent !important",
|
||||
height: "100%"
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "transparent !important"
|
||||
}
|
||||
});
|
||||
|
||||
const extensions = [json(), oneDark, transparentTheme, EditorView.editable.of(false)];
|
||||
|
||||
const formattedBody = computed(() => {
|
||||
if (!store.activeRequest.response) return '';
|
||||
@@ -48,7 +60,11 @@ const statusColor = computed(() => {
|
||||
<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>
|
||||
|
||||
87
src/components/SettingsModal.vue
Normal file
87
src/components/SettingsModal.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import { X } from 'lucide-vue-next';
|
||||
|
||||
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"
|
||||
];
|
||||
</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">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/50">
|
||||
<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">
|
||||
<select
|
||||
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>
|
||||
|
||||
<input
|
||||
v-if="settings.editorFontFamily === 'custom' || !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">
|
||||
<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>
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -81,6 +95,16 @@ export const useRequestStore = defineStore('request', () => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
27
src/stores/settingsStore.ts
Normal file
27
src/stores/settingsStore.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user