feat(ui): add terminal theme picker with ANSI color swatches
ThemePicker modal shows all 7 built-in themes with 16-color ANSI previews and a live prompt sample. StatusBar theme name is now clickable to open the picker. Active theme is visually highlighted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5a86fe6e0c
commit
326fa9530f
@ -20,7 +20,13 @@
|
||||
|
||||
<!-- Right: terminal info -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span>Theme: Dracula</span>
|
||||
<button
|
||||
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
title="Change terminal theme"
|
||||
@click="emit('open-theme-picker')"
|
||||
>
|
||||
Theme: {{ activeThemeName }}
|
||||
</button>
|
||||
<span>UTF-8</span>
|
||||
<span>120×40</span>
|
||||
</div>
|
||||
@ -28,13 +34,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useSessionStore } from "@/stores/session.store";
|
||||
import { useConnectionStore } from "@/stores/connection.store";
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const activeThemeName = ref("Dracula");
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "open-theme-picker"): void;
|
||||
}>();
|
||||
|
||||
const connectionInfo = computed(() => {
|
||||
const session = sessionStore.activeSession;
|
||||
if (!session) return "";
|
||||
@ -44,4 +56,10 @@ const connectionInfo = computed(() => {
|
||||
|
||||
return `root@${conn.hostname}:${conn.port}`;
|
||||
});
|
||||
|
||||
function setThemeName(name: string): void {
|
||||
activeThemeName.value = name;
|
||||
}
|
||||
|
||||
defineExpose({ setThemeName, activeThemeName });
|
||||
</script>
|
||||
|
||||
200
frontend/src/components/common/ThemePicker.vue
Normal file
200
frontend/src/components/common/ThemePicker.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@click.self="close"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/50" @click="close" />
|
||||
|
||||
<!-- Picker panel -->
|
||||
<div class="relative w-full max-w-md bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
|
||||
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">Terminal Theme</h3>
|
||||
<button
|
||||
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@click="close"
|
||||
>
|
||||
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.749.749 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.749.749 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Theme list -->
|
||||
<div class="max-h-96 overflow-y-auto py-2">
|
||||
<button
|
||||
v-for="theme in themes"
|
||||
:key="theme.name"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors cursor-pointer"
|
||||
:class="activeTheme === theme.name ? 'bg-[#1f6feb]/20' : 'hover:bg-[#30363d]'"
|
||||
@click="selectTheme(theme)"
|
||||
>
|
||||
<!-- Active indicator -->
|
||||
<span class="w-4 flex items-center justify-center shrink-0">
|
||||
<svg
|
||||
v-if="activeTheme === theme.name"
|
||||
class="w-4 h-4 text-[#3fb950]"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- Theme info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm text-[var(--wraith-text-primary)]">{{ theme.name }}</div>
|
||||
<!-- Color swatch: 16 ANSI colors as small blocks -->
|
||||
<div class="flex gap-0.5 mt-1.5">
|
||||
<span
|
||||
v-for="(color, i) in themeColors(theme)"
|
||||
:key="i"
|
||||
class="w-4 h-3 rounded-sm"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview: fg on bg -->
|
||||
<div
|
||||
class="px-2 py-1 rounded text-[10px] font-mono shrink-0"
|
||||
:style="{ backgroundColor: theme.background, color: theme.foreground }"
|
||||
>
|
||||
~/wraith $
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
export interface ThemeDefinition {
|
||||
name: string;
|
||||
foreground: string;
|
||||
background: string;
|
||||
cursor: string;
|
||||
black: string;
|
||||
red: string;
|
||||
green: string;
|
||||
yellow: string;
|
||||
blue: string;
|
||||
magenta: string;
|
||||
cyan: string;
|
||||
white: string;
|
||||
brightBlack: string;
|
||||
brightRed: string;
|
||||
brightGreen: string;
|
||||
brightYellow: string;
|
||||
brightBlue: string;
|
||||
brightMagenta: string;
|
||||
brightCyan: string;
|
||||
brightWhite: string;
|
||||
}
|
||||
|
||||
const visible = ref(false);
|
||||
const activeTheme = ref("Dracula");
|
||||
|
||||
// Built-in themes matching the Go theme.BuiltinThemes
|
||||
const themes: ThemeDefinition[] = [
|
||||
{
|
||||
name: "Dracula",
|
||||
foreground: "#f8f8f2", background: "#282a36", cursor: "#f8f8f2",
|
||||
black: "#21222c", red: "#ff5555", green: "#50fa7b", yellow: "#f1fa8c",
|
||||
blue: "#bd93f9", magenta: "#ff79c6", cyan: "#8be9fd", white: "#f8f8f2",
|
||||
brightBlack: "#6272a4", brightRed: "#ff6e6e", brightGreen: "#69ff94",
|
||||
brightYellow: "#ffffa5", brightBlue: "#d6acff", brightMagenta: "#ff92df",
|
||||
brightCyan: "#a4ffff", brightWhite: "#ffffff",
|
||||
},
|
||||
{
|
||||
name: "Nord",
|
||||
foreground: "#d8dee9", background: "#2e3440", cursor: "#d8dee9",
|
||||
black: "#3b4252", red: "#bf616a", green: "#a3be8c", yellow: "#ebcb8b",
|
||||
blue: "#81a1c1", magenta: "#b48ead", cyan: "#88c0d0", white: "#e5e9f0",
|
||||
brightBlack: "#4c566a", brightRed: "#bf616a", brightGreen: "#a3be8c",
|
||||
brightYellow: "#ebcb8b", brightBlue: "#81a1c1", brightMagenta: "#b48ead",
|
||||
brightCyan: "#8fbcbb", brightWhite: "#eceff4",
|
||||
},
|
||||
{
|
||||
name: "Monokai",
|
||||
foreground: "#f8f8f2", background: "#272822", cursor: "#f8f8f0",
|
||||
black: "#272822", red: "#f92672", green: "#a6e22e", yellow: "#f4bf75",
|
||||
blue: "#66d9ef", magenta: "#ae81ff", cyan: "#a1efe4", white: "#f8f8f2",
|
||||
brightBlack: "#75715e", brightRed: "#f92672", brightGreen: "#a6e22e",
|
||||
brightYellow: "#f4bf75", brightBlue: "#66d9ef", brightMagenta: "#ae81ff",
|
||||
brightCyan: "#a1efe4", brightWhite: "#f9f8f5",
|
||||
},
|
||||
{
|
||||
name: "One Dark",
|
||||
foreground: "#abb2bf", background: "#282c34", cursor: "#528bff",
|
||||
black: "#282c34", red: "#e06c75", green: "#98c379", yellow: "#e5c07b",
|
||||
blue: "#61afef", magenta: "#c678dd", cyan: "#56b6c2", white: "#abb2bf",
|
||||
brightBlack: "#545862", brightRed: "#e06c75", brightGreen: "#98c379",
|
||||
brightYellow: "#e5c07b", brightBlue: "#61afef", brightMagenta: "#c678dd",
|
||||
brightCyan: "#56b6c2", brightWhite: "#c8ccd4",
|
||||
},
|
||||
{
|
||||
name: "Solarized Dark",
|
||||
foreground: "#839496", background: "#002b36", cursor: "#839496",
|
||||
black: "#073642", red: "#dc322f", green: "#859900", yellow: "#b58900",
|
||||
blue: "#268bd2", magenta: "#d33682", cyan: "#2aa198", white: "#eee8d5",
|
||||
brightBlack: "#002b36", brightRed: "#cb4b16", brightGreen: "#586e75",
|
||||
brightYellow: "#657b83", brightBlue: "#839496", brightMagenta: "#6c71c4",
|
||||
brightCyan: "#93a1a1", brightWhite: "#fdf6e3",
|
||||
},
|
||||
{
|
||||
name: "Gruvbox Dark",
|
||||
foreground: "#ebdbb2", background: "#282828", cursor: "#ebdbb2",
|
||||
black: "#282828", red: "#cc241d", green: "#98971a", yellow: "#d79921",
|
||||
blue: "#458588", magenta: "#b16286", cyan: "#689d6a", white: "#a89984",
|
||||
brightBlack: "#928374", brightRed: "#fb4934", brightGreen: "#b8bb26",
|
||||
brightYellow: "#fabd2f", brightBlue: "#83a598", brightMagenta: "#d3869b",
|
||||
brightCyan: "#8ec07c", brightWhite: "#ebdbb2",
|
||||
},
|
||||
{
|
||||
name: "MobaXTerm Classic",
|
||||
foreground: "#ececec", background: "#242424", cursor: "#b4b4c0",
|
||||
black: "#000000", red: "#aa4244", green: "#7e8d53", yellow: "#e4b46d",
|
||||
blue: "#6e9aba", magenta: "#9e5085", cyan: "#80d5cf", white: "#cccccc",
|
||||
brightBlack: "#808080", brightRed: "#cc7b7d", brightGreen: "#a5b17c",
|
||||
brightYellow: "#ecc995", brightBlue: "#96b6cd", brightMagenta: "#c083ac",
|
||||
brightCyan: "#a9e2de", brightWhite: "#cccccc",
|
||||
},
|
||||
];
|
||||
|
||||
function themeColors(theme: ThemeDefinition): string[] {
|
||||
return [
|
||||
theme.black, theme.red, theme.green, theme.yellow,
|
||||
theme.blue, theme.magenta, theme.cyan, theme.white,
|
||||
theme.brightBlack, theme.brightRed, theme.brightGreen, theme.brightYellow,
|
||||
theme.brightBlue, theme.brightMagenta, theme.brightCyan, theme.brightWhite,
|
||||
];
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select", theme: ThemeDefinition): void;
|
||||
}>();
|
||||
|
||||
function selectTheme(theme: ThemeDefinition): void {
|
||||
activeTheme.value = theme.name;
|
||||
emit("select", theme);
|
||||
// TODO: Apply theme to xterm.js via Wails binding — ThemeService.SetActive(theme.name)
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ open, close, visible, activeTheme });
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user