generated from nhcarrigan/template
feat: resizable character panel and full-height sprite
- Change sprite to use full height instead of full width for better scaling - Add draggable divider between character panel and terminal - Persist panel width preference in config (min: 200px, max: 600px) Closes #10
This commit is contained in:
@@ -64,6 +64,9 @@ pub struct HikariConfig {
|
|||||||
|
|
||||||
#[serde(default = "default_update_checks_enabled")]
|
#[serde(default = "default_update_checks_enabled")]
|
||||||
pub update_checks_enabled: bool,
|
pub update_checks_enabled: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub character_panel_width: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HikariConfig {
|
impl Default for HikariConfig {
|
||||||
@@ -81,6 +84,7 @@ impl Default for HikariConfig {
|
|||||||
notification_volume: 0.7,
|
notification_volume: 0.7,
|
||||||
always_on_top: false,
|
always_on_top: false,
|
||||||
update_checks_enabled: true,
|
update_checks_enabled: true,
|
||||||
|
character_panel_width: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +130,7 @@ mod tests {
|
|||||||
assert!(config.greeting_custom_prompt.is_none());
|
assert!(config.greeting_custom_prompt.is_none());
|
||||||
assert!(!config.always_on_top);
|
assert!(!config.always_on_top);
|
||||||
assert!(config.update_checks_enabled);
|
assert!(config.update_checks_enabled);
|
||||||
|
assert!(config.character_panel_width.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -143,6 +148,7 @@ mod tests {
|
|||||||
notification_volume: 0.7,
|
notification_volume: 0.7,
|
||||||
always_on_top: true,
|
always_on_top: true,
|
||||||
update_checks_enabled: true,
|
update_checks_enabled: true,
|
||||||
|
character_panel_width: Some(400),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
|||||||
@@ -57,30 +57,34 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="anime-girl-container flex flex-col items-center justify-end h-full p-4">
|
<div
|
||||||
<div class="character-frame relative {getBackgroundGlow()} w-full max-w-md">
|
class="anime-girl-container flex flex-col items-center justify-between h-full p-4 overflow-hidden"
|
||||||
<div class="sprite-container {getAnimationClass()}">
|
>
|
||||||
|
<div
|
||||||
|
class="character-frame relative {getBackgroundGlow()} flex-1 flex items-center justify-center min-h-0"
|
||||||
|
>
|
||||||
|
<div class="sprite-container {getAnimationClass()} h-full flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
src="/sprites/{info.spriteFile}"
|
src="/sprites/{info.spriteFile}"
|
||||||
alt="Hikari - {info.label}"
|
alt="Hikari - {info.label}"
|
||||||
class="character-sprite w-full h-auto object-contain"
|
class="character-sprite h-full w-auto max-w-full object-contain"
|
||||||
onerror={(e) => {
|
onerror={(e) => {
|
||||||
const target = e.currentTarget as HTMLImageElement;
|
const target = e.currentTarget as HTMLImageElement;
|
||||||
target.src = "/sprites/placeholder.svg";
|
target.src = "/sprites/placeholder.svg";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="state-indicator absolute -bottom-2 left-1/2 transform -translate-x-1/2">
|
<div class="state-indicator mt-2">
|
||||||
<div
|
<div
|
||||||
class="px-3 py-1 rounded-full text-xs font-medium bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--accent-primary)]"
|
class="px-3 py-1 rounded-full text-xs font-medium bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--accent-primary)]"
|
||||||
>
|
>
|
||||||
{info.label}
|
{info.label}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="speech-bubble mt-4 max-w-xs">
|
<div class="speech-bubble mt-2 max-w-xs flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
class="relative bg-[var(--bg-secondary)] rounded-lg px-4 py-2 border border-[var(--border-color)]"
|
class="relative bg-[var(--bg-secondary)] rounded-lg px-4 py-2 border border-[var(--border-color)]"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
notification_volume: 0.7,
|
notification_volume: 0.7,
|
||||||
always_on_top: false,
|
always_on_top: false,
|
||||||
update_checks_enabled: true,
|
update_checks_enabled: true,
|
||||||
|
character_panel_width: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
notification_volume: 0.5,
|
notification_volume: 0.5,
|
||||||
always_on_top: false,
|
always_on_top: false,
|
||||||
update_checks_enabled: true,
|
update_checks_enabled: true,
|
||||||
|
character_panel_width: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface HikariConfig {
|
|||||||
notification_volume: number;
|
notification_volume: number;
|
||||||
always_on_top: boolean;
|
always_on_top: boolean;
|
||||||
update_checks_enabled: boolean;
|
update_checks_enabled: boolean;
|
||||||
|
character_panel_width: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: HikariConfig = {
|
const defaultConfig: HikariConfig = {
|
||||||
@@ -31,6 +32,7 @@ const defaultConfig: HikariConfig = {
|
|||||||
notification_volume: 0.7,
|
notification_volume: 0.7,
|
||||||
always_on_top: false,
|
always_on_top: false,
|
||||||
update_checks_enabled: true,
|
update_checks_enabled: true,
|
||||||
|
character_panel_width: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function createConfigStore() {
|
function createConfigStore() {
|
||||||
|
|||||||
+50
-3
@@ -24,6 +24,35 @@
|
|||||||
let updateNotification: UpdateNotification;
|
let updateNotification: UpdateNotification;
|
||||||
let achievementPanelOpen = $state(false);
|
let achievementPanelOpen = $state(false);
|
||||||
|
|
||||||
|
// Resizable panel state
|
||||||
|
let panelWidth = $state(320); // Default width in pixels
|
||||||
|
let isResizing = $state(false);
|
||||||
|
const MIN_PANEL_WIDTH = 200;
|
||||||
|
const MAX_PANEL_WIDTH = 600;
|
||||||
|
|
||||||
|
function startResize(event: MouseEvent) {
|
||||||
|
isResizing = true;
|
||||||
|
event.preventDefault();
|
||||||
|
document.addEventListener("mousemove", handleResize);
|
||||||
|
document.addEventListener("mouseup", stopResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize(event: MouseEvent) {
|
||||||
|
if (!isResizing) return;
|
||||||
|
const newWidth = event.clientX;
|
||||||
|
panelWidth = Math.max(MIN_PANEL_WIDTH, Math.min(MAX_PANEL_WIDTH, newWidth));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
if (isResizing) {
|
||||||
|
isResizing = false;
|
||||||
|
document.removeEventListener("mousemove", handleResize);
|
||||||
|
document.removeEventListener("mouseup", stopResize);
|
||||||
|
// Save the panel width to config
|
||||||
|
configStore.updateConfig({ character_panel_width: panelWidth });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Global keyboard shortcuts
|
// Global keyboard shortcuts
|
||||||
function handleGlobalKeydown(event: KeyboardEvent) {
|
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||||
// Don't trigger shortcuts when typing in inputs (except for specific ones)
|
// Don't trigger shortcuts when typing in inputs (except for specific ones)
|
||||||
@@ -105,6 +134,11 @@
|
|||||||
await window.setAlwaysOnTop(true);
|
await window.setAlwaysOnTop(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load saved panel width
|
||||||
|
if (config.character_panel_width) {
|
||||||
|
panelWidth = config.character_panel_width;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize notification settings sync
|
// Initialize notification settings sync
|
||||||
initNotificationSync();
|
initNotificationSync();
|
||||||
|
|
||||||
@@ -134,13 +168,22 @@
|
|||||||
<main class="flex-1 flex overflow-hidden">
|
<main class="flex-1 flex overflow-hidden">
|
||||||
<!-- Left panel: Character display -->
|
<!-- Left panel: Character display -->
|
||||||
<div
|
<div
|
||||||
class="character-panel w-1/3 flex flex-col items-center justify-center border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50"
|
class="character-panel flex flex-col items-center justify-center bg-[var(--bg-secondary)]/50"
|
||||||
|
style="width: {panelWidth}px; min-width: {MIN_PANEL_WIDTH}px; max-width: {MAX_PANEL_WIDTH}px;"
|
||||||
>
|
>
|
||||||
<AnimeGirl />
|
<AnimeGirl />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize handle -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="resize-handle w-1 cursor-col-resize bg-[var(--border-color)] hover:bg-[var(--accent-primary)] transition-colors flex-shrink-0"
|
||||||
|
class:bg-[var(--accent-primary)]={isResizing}
|
||||||
|
onmousedown={startResize}
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- Right panel: Terminal and input -->
|
<!-- Right panel: Terminal and input -->
|
||||||
<div class="terminal-panel flex-1 flex flex-col">
|
<div class="terminal-panel flex-1 flex flex-col min-w-0">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
<InputBar />
|
<InputBar />
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +212,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.character-panel {
|
.character-panel {
|
||||||
min-width: 320px;
|
|
||||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover,
|
||||||
|
.resize-handle:active {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user