mirror of
https://github.com/SrIzan10/lofi.git
synced 2026-06-06 00:56:53 +00:00
feat: window component
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<DialogPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none backdrop-blur-sm p-1 shadow-sm dark:bg-black/20 bg-white/20 transition border-white/20 dark:border-black/20"
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none backdrop-blur-sm p-1 shadow-sm dark:bg-black/20 bg-white/20 border-white/20 dark:border-black/20"
|
||||
>
|
||||
<X class="size-4" color="white" />
|
||||
<span class="sr-only">Close</span>
|
||||
|
||||
5
src/lib/components/ui/window/index.ts
Normal file
5
src/lib/components/ui/window/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Window from "./window.svelte";
|
||||
|
||||
export {
|
||||
Window,
|
||||
};
|
||||
143
src/lib/components/ui/window/window.svelte
Normal file
143
src/lib/components/ui/window/window.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { state as appState } from '$lib/state.svelte';
|
||||
|
||||
let {
|
||||
children = undefined,
|
||||
title = 'Window',
|
||||
width = 400,
|
||||
height = 300,
|
||||
initialX = undefined,
|
||||
initialY = undefined,
|
||||
showTitleBar = true,
|
||||
showCloseButton = true,
|
||||
onClose = () => {},
|
||||
initialZIndex = 50,
|
||||
}: {
|
||||
children?: Snippet;
|
||||
title?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
initialX?: number;
|
||||
initialY?: number;
|
||||
showTitleBar?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
onClose?: () => void;
|
||||
initialZIndex?: number;
|
||||
} = $props();
|
||||
|
||||
let zIndex = $state(initialZIndex);
|
||||
|
||||
function bringToFront() {
|
||||
zIndex = appState.windowZIndexCounter++;
|
||||
}
|
||||
|
||||
let x = $state(initialX ?? (typeof window !== 'undefined' ? (window.innerWidth - width) / 2 : 0));
|
||||
let y = $state(
|
||||
initialY ?? (typeof window !== 'undefined' ? (window.innerHeight - height) / 2 : 0)
|
||||
);
|
||||
|
||||
let windowRef: HTMLElement | undefined = $state();
|
||||
let headerRef: HTMLElement | undefined = $state();
|
||||
let isDragging = $state(false);
|
||||
let dragOffsetX = $state(0);
|
||||
let dragOffsetY = $state(0);
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
bringToFront();
|
||||
|
||||
if (
|
||||
!showTitleBar ||
|
||||
!headerRef ||
|
||||
!(event.target instanceof Node) ||
|
||||
!headerRef.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (headerRef.contains(event.target as Node)) {
|
||||
isDragging = true;
|
||||
dragOffsetX = event.clientX - x;
|
||||
dragOffsetY = event.clientY - y;
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (!isDragging) return;
|
||||
x = event.clientX - dragOffsetX;
|
||||
y = event.clientY - dragOffsetY;
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (typeof window !== 'undefined') {
|
||||
x = (window.innerWidth - width) / 2;
|
||||
y = (window.innerHeight - height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', handleResize);
|
||||
if (initialX === undefined) {
|
||||
x = (window.innerWidth - width) / 2;
|
||||
}
|
||||
if (initialY === undefined) {
|
||||
y = (window.innerHeight - height) / 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={windowRef}
|
||||
class="fixed flex flex-col bg-white/10 backdrop-blur-md border border-white/20 shadow-lg rounded-lg overflow-hidden"
|
||||
style="width: {width}px; height: {height}px; left: {x}px; top: {y}px; z-index: {zIndex};"
|
||||
onmousedown={handleMouseDown}
|
||||
role="dialog"
|
||||
tabindex="0"
|
||||
>
|
||||
{#if showTitleBar}
|
||||
<div
|
||||
bind:this={headerRef}
|
||||
class="h-8 px-3 flex items-center justify-between bg-black/10 border-b border-white/10 select-none"
|
||||
style="cursor: {isDragging ? 'grabbing' : 'grab'};"
|
||||
>
|
||||
<span class="text-sm font-medium text-white/90">{title}</span>
|
||||
{#if showCloseButton}
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="w-5 h-5 flex items-center justify-center text-white/70 hover:text-white hover:bg-red-500/50 rounded-sm transition-colors"
|
||||
aria-label="Close window"
|
||||
>
|
||||
<X class="size-4" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 p-1 overflow-auto bg-transparent" role="dialog" tabindex="0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -20,6 +20,8 @@ export const state = $state({
|
||||
stations: [] as Station[],
|
||||
backgrounds: [] as Background[],
|
||||
atmospheres: [] as Atmosphere[],
|
||||
|
||||
windowZIndexCounter: 50,
|
||||
|
||||
// in daemon.svelte
|
||||
togglePlay: (() => {}) as () => void,
|
||||
|
||||
Reference in New Issue
Block a user