Compare commits
2 Commits
4f6c9f87e7
...
d6dda10d2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6dda10d2f | ||
|
|
c5d82a710d |
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRequestStore } from '../stores/requestStore';
|
import { useRequestStore } from '../stores/requestStore';
|
||||||
import { ShieldCheck, Key, User, Lock, Fingerprint } from 'lucide-vue-next';
|
import { ShieldCheck, Key, User, Lock, Fingerprint } from 'lucide-vue-next';
|
||||||
|
import CustomSelect from './CustomSelect.vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const store = useRequestStore();
|
const store = useRequestStore();
|
||||||
@@ -21,15 +22,11 @@ const showApiKey = ref(false);
|
|||||||
<!-- Type Selector -->
|
<!-- Type Selector -->
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs font-bold uppercase text-slate-500 tracking-wider">Authentication Type</label>
|
<label class="text-xs font-bold uppercase text-slate-500 tracking-wider">Authentication Type</label>
|
||||||
<div class="relative">
|
<CustomSelect
|
||||||
<select
|
v-model="store.activeRequest.auth.type"
|
||||||
v-model="store.activeRequest.auth.type"
|
:options="authTypes"
|
||||||
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"
|
:full-width="true"
|
||||||
>
|
/>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Dynamic Content -->
|
<!-- Dynamic Content -->
|
||||||
|
|||||||
106
src/components/CustomSelect.vue
Normal file
106
src/components/CustomSelect.vue
Normal 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>
|
||||||
@@ -4,6 +4,7 @@ import { useRequestStore } from '../stores/requestStore';
|
|||||||
import { useSettingsStore } from '../stores/settingsStore';
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
import KeyValueEditor from './KeyValueEditor.vue';
|
import KeyValueEditor from './KeyValueEditor.vue';
|
||||||
import AuthPanel from './AuthPanel.vue';
|
import AuthPanel from './AuthPanel.vue';
|
||||||
|
import CustomSelect from './CustomSelect.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';
|
||||||
@@ -84,18 +85,17 @@ const executeRequest = async () => {
|
|||||||
<div class="flex flex-col h-full bg-slate-900">
|
<div class="flex flex-col h-full bg-slate-900">
|
||||||
<!-- Top Bar -->
|
<!-- Top Bar -->
|
||||||
<div class="p-4 border-b border-slate-800 flex gap-2 items-center">
|
<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">
|
<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">
|
||||||
<select
|
<CustomSelect
|
||||||
v-model="store.activeRequest.method"
|
v-model="store.activeRequest.method"
|
||||||
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"
|
: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]"
|
||||||
<option v-for="m in methods" :key="m" :value="m" class="bg-slate-900 text-white">{{ m }}</option>
|
/>
|
||||||
</select>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="store.activeRequest.url"
|
v-model="store.activeRequest.url"
|
||||||
placeholder="https://api.example.com/v1/users"
|
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"
|
@keydown.enter="executeRequest"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSettingsStore } from '../stores/settingsStore';
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
|
import CustomSelect from './CustomSelect.vue';
|
||||||
import { X } from 'lucide-vue-next';
|
import { X } from 'lucide-vue-next';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
@@ -12,13 +14,20 @@ const fontOptions = [
|
|||||||
"Monaco, Menlo, 'Ubuntu Mono', monospace",
|
"Monaco, Menlo, 'Ubuntu Mono', monospace",
|
||||||
"Arial, sans-serif"
|
"Arial, sans-serif"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const displayFontOptions = computed(() => {
|
||||||
|
return fontOptions.map(font => ({
|
||||||
|
value: font,
|
||||||
|
label: font.split(',')[0].replace(/['"]/g, '')
|
||||||
|
}));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="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">
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/50">
|
<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>
|
<h2 class="text-lg font-semibold text-slate-100">Editor Settings</h2>
|
||||||
<button
|
<button
|
||||||
@click="emit('close')"
|
@click="emit('close')"
|
||||||
@@ -52,18 +61,14 @@ const fontOptions = [
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-sm font-medium text-slate-300">Font Family</label>
|
<label class="text-sm font-medium text-slate-300">Font Family</label>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<select
|
<CustomSelect
|
||||||
v-model="settings.editorFontFamily"
|
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"
|
:options="displayFontOptions"
|
||||||
>
|
:full-width="true"
|
||||||
<option v-for="font in fontOptions" :key="font" :value="font">
|
placeholder="Select or enter font family"
|
||||||
{{ font.split(',')[0].replace(/['"]/g, '') }}
|
/>
|
||||||
</option>
|
|
||||||
<option value="custom">Custom...</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-if="settings.editorFontFamily === 'custom' || !fontOptions.includes(settings.editorFontFamily)"
|
v-if="!fontOptions.includes(settings.editorFontFamily)"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="settings.editorFontFamily"
|
v-model="settings.editorFontFamily"
|
||||||
placeholder="e.g. 'JetBrains Mono', monospace"
|
placeholder="e.g. 'JetBrains Mono', monospace"
|
||||||
@@ -74,7 +79,7 @@ const fontOptions = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-6 py-4 bg-slate-950/50 border-t border-slate-800 flex justify-end">
|
<div class="px-6 py-4 bg-slate-950/50 border-t border-slate-800 flex justify-end rounded-b-xl">
|
||||||
<button
|
<button
|
||||||
@click="emit('close')"
|
@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"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user