add font settings (but font family doesn't work)
This commit is contained in:
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.
|
||||||
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>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<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 { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
@@ -9,6 +10,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
|||||||
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);
|
||||||
|
|
||||||
@@ -121,7 +123,11 @@ 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"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<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';
|
||||||
|
|
||||||
const store = useRequestStore();
|
const store = useRequestStore();
|
||||||
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
const extensions = [json(), oneDark, EditorView.editable.of(false)];
|
const extensions = [json(), oneDark, EditorView.editable.of(false)];
|
||||||
|
|
||||||
@@ -48,7 +50,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>
|
||||||
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user