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
|
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]
|
#[tauri::command]
|
||||||
async fn execute_request(
|
async fn execute_request(
|
||||||
method: String,
|
method: String,
|
||||||
@@ -17,6 +46,7 @@ async fn execute_request(
|
|||||||
headers: HashMap<String, String>,
|
headers: HashMap<String, String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
query_params: Option<HashMap<String, String>>,
|
query_params: Option<HashMap<String, String>>,
|
||||||
|
auth: Option<AuthConfig>,
|
||||||
) -> Result<HttpResponse, String> {
|
) -> Result<HttpResponse, String> {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
@@ -37,6 +67,31 @@ async fn execute_request(
|
|||||||
request_builder = request_builder.header(key, value);
|
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
|
// Add Body
|
||||||
if let Some(b) = body {
|
if let Some(b) = body {
|
||||||
if !b.is_empty() {
|
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 RequestPanel from './components/RequestPanel.vue';
|
||||||
import ResponsePanel from './components/ResponsePanel.vue';
|
import ResponsePanel from './components/ResponsePanel.vue';
|
||||||
import MethodBadge from './components/MethodBadge.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 store = useRequestStore();
|
||||||
|
const showSettings = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -52,6 +55,16 @@ const store = useRequestStore();
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Workspace -->
|
<!-- Main Workspace -->
|
||||||
@@ -63,5 +76,7 @@ const store = useRequestStore();
|
|||||||
<ResponsePanel />
|
<ResponsePanel />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<SettingsModal v-if="showSettings" @close="showSettings = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRequestStore } from '../stores/requestStore';
|
import { useRequestStore } from '../stores/requestStore';
|
||||||
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
import KeyValueEditor from './KeyValueEditor.vue';
|
import KeyValueEditor from './KeyValueEditor.vue';
|
||||||
|
import AuthPanel from './AuthPanel.vue';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
import { Play, Loader2 } from 'lucide-vue-next';
|
import { Play, Loader2 } from 'lucide-vue-next';
|
||||||
|
|
||||||
const store = useRequestStore();
|
const store = useRequestStore();
|
||||||
|
const settings = useSettingsStore();
|
||||||
const activeTab = ref('params');
|
const activeTab = ref('params');
|
||||||
const isLoading = ref(false);
|
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'];
|
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||||||
|
|
||||||
@@ -36,6 +50,7 @@ const executeRequest = async () => {
|
|||||||
headers,
|
headers,
|
||||||
body: store.activeRequest.body || null,
|
body: store.activeRequest.body || null,
|
||||||
queryParams: params,
|
queryParams: params,
|
||||||
|
auth: store.activeRequest.auth,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update Store
|
// 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">
|
<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
|
<select
|
||||||
v-model="store.activeRequest.method"
|
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>
|
</select>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -93,7 +108,7 @@ const executeRequest = async () => {
|
|||||||
<!-- Configuration Tabs -->
|
<!-- Configuration Tabs -->
|
||||||
<div class="flex border-b border-slate-800">
|
<div class="flex border-b border-slate-800">
|
||||||
<button
|
<button
|
||||||
v-for="tab in ['params', 'headers', 'body']"
|
v-for="tab in ['params', 'headers', 'body', 'auth']"
|
||||||
:key="tab"
|
:key="tab"
|
||||||
@click="activeTab = tab"
|
@click="activeTab = tab"
|
||||||
class="px-4 py-2 text-xs font-medium uppercase tracking-wide border-b-2 transition-colors"
|
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
|
<Codemirror
|
||||||
v-model="store.activeRequest.body"
|
v-model="store.activeRequest.body"
|
||||||
placeholder="Request Body (JSON)"
|
placeholder="Request Body (JSON)"
|
||||||
:style="{ height: '100%' }"
|
:style="{
|
||||||
|
height: '100%',
|
||||||
|
fontSize: settings.editorFontSize + 'px',
|
||||||
|
fontFamily: settings.editorFontFamily
|
||||||
|
}"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
:indent-with-tab="true"
|
:indent-with-tab="true"
|
||||||
:tab-size="2"
|
:tab-size="2"
|
||||||
:extensions="extensions"
|
:extensions="extensions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<KeepAlive>
|
||||||
|
<AuthPanel
|
||||||
|
v-if="activeTab === 'auth'"
|
||||||
|
/>
|
||||||
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRequestStore } from '../stores/requestStore';
|
import { useRequestStore } from '../stores/requestStore';
|
||||||
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view'; // Import EditorView
|
||||||
|
|
||||||
const store = useRequestStore();
|
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(() => {
|
const formattedBody = computed(() => {
|
||||||
if (!store.activeRequest.response) return '';
|
if (!store.activeRequest.response) return '';
|
||||||
@@ -48,7 +60,11 @@ const statusColor = computed(() => {
|
|||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<Codemirror
|
<Codemirror
|
||||||
:model-value="formattedBody"
|
:model-value="formattedBody"
|
||||||
:style="{ height: '100%' }"
|
:style="{
|
||||||
|
height: '100%',
|
||||||
|
fontSize: settings.editorFontSize + 'px',
|
||||||
|
fontFamily: settings.editorFontFamily
|
||||||
|
}"
|
||||||
:extensions="extensions"
|
:extensions="extensions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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;
|
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 {
|
export interface RequestData {
|
||||||
id: string;
|
id: string;
|
||||||
method: string;
|
method: string;
|
||||||
@@ -15,6 +22,7 @@ export interface RequestData {
|
|||||||
params: KeyValue[];
|
params: KeyValue[];
|
||||||
headers: KeyValue[];
|
headers: KeyValue[];
|
||||||
body: string;
|
body: string;
|
||||||
|
auth: AuthState;
|
||||||
response?: {
|
response?: {
|
||||||
status: number;
|
status: number;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
@@ -39,6 +47,12 @@ export const useRequestStore = defineStore('request', () => {
|
|||||||
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
|
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
|
||||||
],
|
],
|
||||||
body: '',
|
body: '',
|
||||||
|
auth: {
|
||||||
|
type: 'none',
|
||||||
|
basic: { username: '', password: '' },
|
||||||
|
bearer: { token: '' },
|
||||||
|
apiKey: { key: '', value: '', addTo: 'header' }
|
||||||
|
},
|
||||||
timestamp: Date.now(),
|
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.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 });
|
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;
|
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 {
|
#app {
|
||||||
@apply h-full w-full;
|
@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