add svelte splitpanes for pinned display view

This commit is contained in:
E44
2025-11-08 21:47:44 +01:00
parent be3f309d0a
commit 6c6a2bdbee
6 changed files with 288 additions and 157 deletions
+23 -1
View File
@@ -9,7 +9,8 @@
"version": "0.0.1",
"dependencies": {
"@thisux/sveltednd": "^0.0.20",
"lucide-svelte": "^0.545.0"
"lucide-svelte": "^0.545.0",
"svelte-splitpanes": "^8.0.9"
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
@@ -2352,6 +2353,15 @@
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
},
"node_modules/esm-env-robust": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/esm-env-robust/-/esm-env-robust-0.0.3.tgz",
"integrity": "sha512-90Gnuw2DALOqlL1581VxP3GHPUNHX9U+fQ+8FNcTTFClhY5gEggAAnJ3q1b2Oq23knRsjv8YpNeMRPaMLUymOA==",
"license": "MIT",
"dependencies": {
"esm-env": "^1.0.0"
}
},
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -3848,6 +3858,18 @@
"node": ">=4"
}
},
"node_modules/svelte-splitpanes": {
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/svelte-splitpanes/-/svelte-splitpanes-8.0.9.tgz",
"integrity": "sha512-L3oLXTC99M191FInTXJ/f/2i0welRql1QuVbPaU8iy6nvCR6X9VyjHCsCpLqKGWHwqkWo/AM9CQ1c0nzlb+MkA==",
"license": "MIT",
"dependencies": {
"esm-env-robust": "0.0.3"
},
"peerDependencies": {
"svelte": "^4.2.19 || ^5.1.0"
}
},
"node_modules/tailwindcss": {
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
+2 -1
View File
@@ -38,6 +38,7 @@
},
"dependencies": {
"@thisux/sveltednd": "^0.0.20",
"lucide-svelte": "^0.545.0"
"lucide-svelte": "^0.545.0",
"svelte-splitpanes": "^8.0.9"
}
}
+78
View File
@@ -13,3 +13,81 @@
--color-stone-750: oklch(0.321 0.0085 50.928);
--color-stone-850: oklch(0.242 0.0065 45.171);
}
/* Splitpanes */
.splitpanes.mudics-stone-theme {
.splitpanes__pane {
@apply bg-stone-900;
}
.splitpanes__splitter {
@apply bg-transparent hover:bg-stone-600 transition-colors duration-200 shrink-0 relative box-border rounded mx-5;
/* box-sizing: border-box;
position: relative;
flex-shrink: 0; */
&:before,
&:after {
@apply bg-stone-750 transition-colors duration-200 absolute top-[50%] left-[50%] content-[''] rounded;
}
&:hover:before,
&:hover:after {
@apply bg-stone-300;
}
&:first-child {
/* cursor: auto; */
}
}
}
.mudics-stone-theme {
&.splitpanes .splitpanes .splitpanes__splitter {
@apply z-10;
}
&.splitpanes--vertical>.splitpanes__splitter,
.splitpanes--vertical>.splitpanes__splitter {
@apply w-2.5 cursor-col-resize;
&:before,
&:after {
@apply w-0.5 h-10 translate-y-[-50%];
}
&:before {
@apply ml-[calc(-0.75*var(--spacing))];
}
&:after {
@apply ml-[calc(0.25*var(--spacing))];
}
}
&.splitpanes--horizontal>.splitpanes__splitter,
.splitpanes--horizontal>.splitpanes__splitter {
@apply h-2 cursor-row-resize;
&:before,
&:after {
@apply h-0.5 w-10 translate-x-[-50%];
}
&:before {
@apply mt-[calc(-0.7*var(--spacing))];
}
&:after {
@apply mt-[calc(0.1*var(--spacing))];
}
}
}
@@ -1,6 +1,11 @@
<script lang="ts">
import { dragHandleZone, TRIGGERS } from 'svelte-dnd-action';
import { dnd_flip_duration_ms, get_selectable_color_classes, is_display_drag, is_group_drag } from '../ts/stores/ui_behavior';
import {
dnd_flip_duration_ms,
get_selectable_color_classes,
is_display_drag,
is_group_drag
} from '../ts/stores/ui_behavior';
import { cubicOut } from 'svelte/easing';
import { flip } from 'svelte/animate';
import DisplayObject from './DisplayObject.svelte';
@@ -16,9 +21,10 @@
import type { DisplayGroup, MenuOption } from '../ts/types';
import { selected_display_ids } from '../ts/stores/select';
let { display_group, get_display_menu_options } = $props<{
let { display_group, get_display_menu_options, close_pinned_display } = $props<{
display_group: DisplayGroup;
get_display_menu_options: (display_id: string) => MenuOption[]
get_display_menu_options: (display_id: string) => MenuOption[];
close_pinned_display: () => void;
}>();
let hovering_selectable = $state(false);
@@ -32,7 +38,7 @@
const { items, info } = e.detail;
if (items.length !== 1 && info.trigger === TRIGGERS.DRAG_STARTED) {
$is_display_drag = true;
$is_display_drag = true;
add_empty_display_group();
}
set_new_display_group_data(display_group.id, items);
@@ -40,7 +46,7 @@
function handle_finalize(e: CustomEvent) {
remove_empty_display_groups();
$is_display_drag = false;
$is_display_drag = false;
set_new_display_group_data(display_group.id, e.detail.items);
}
</script>
@@ -65,7 +71,7 @@
items: display_group.data,
type: 'item',
flipDurationMs: dnd_flip_duration_ms,
dropTargetStyle: { outline: 'none' },
dropTargetStyle: { outline: 'none' }
}}
onconsider={handle_consider}
onfinalize={handle_finalize}
@@ -77,7 +83,7 @@
class="outline-none"
role="figure"
>
<DisplayObject {display} {get_display_menu_options} />
<DisplayObject {display} {get_display_menu_options} {close_pinned_display} />
</section>
{/each}
@@ -12,9 +12,10 @@
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
import { update_screenshot } from '../ts/stores/displays';
let { display, get_display_menu_options } = $props<{
let { display, get_display_menu_options, close_pinned_display } = $props<{
display: Display;
get_display_menu_options: (display_id: string) => MenuOption[];
close_pinned_display: () => void;
}>();
let hovering_unselectable = $state(false);
@@ -26,11 +27,11 @@
function on_preview_click(e: MouseEvent) {
if ($pinned_display_id === display.id) {
$pinned_display_id = null;
close_pinned_display();
} else {
$pinned_display_id = display.id;
}
update_screenshot(display.id);
// update_screenshot(display.id); DEBUG
e.stopPropagation();
}
</script>
+168 -145
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { fade, scale, slide } from 'svelte/transition';
import { fade, scale } from 'svelte/transition';
import {
all_displays_of_group_selected,
displays,
@@ -25,6 +25,7 @@
import { dragHandleZone } from 'svelte-dnd-action';
import { flip } from 'svelte/animate';
import DisplayGroupObject from './DisplayGroupObject.svelte';
import { Pane, Splitpanes } from 'svelte-splitpanes';
let { handle_display_deletion, handle_display_editing } = $props<{
handle_display_deletion: (display_id: string) => void;
@@ -36,6 +37,14 @@
get_display_by_id($pinned_display_id || '', $displays)
);
let last_pinned_pane_size: number = 45;
let pinned_pane_size: number = $state(last_pinned_pane_size);
function close_pinned_display() {
last_pinned_pane_size = pinned_pane_size;
pinned_pane_size = 0;
}
function get_display_menu_options(display_id: string): MenuOption[] {
return [
{
@@ -72,6 +81,13 @@
return true;
}
function handle_splitpane_resize(e: any) {
if (e.detail[0].size === 0) {
$pinned_display_id = null;
pinned_pane_size = last_pinned_pane_size;
}
}
function on_wheel(e: WheelEvent) {
if (!$is_group_drag && !$is_display_drag) return;
if (!displays_scroll_box) return;
@@ -88,159 +104,166 @@
<svelte:window on:wheel={on_wheel} />
<div class="h-[calc(100dvh-3rem-(6*var(--spacing)))] flex flex-col gap-2">
{#if $pinned_display_id}
<!-- Pinned Item -->
<div in:fade={{ duration: 140 }} out:fade={{ duration: 120 }}>
<Splitpanes horizontal theme="mudics-stone-theme" on:resized={handle_splitpane_resize}>
{#if $pinned_display_id}
<!-- Pinned Item -->
<Pane maxSize={60} snapSize={20} bind:size={pinned_pane_size}>
<div class="h-full" transition:fade>
<div class="grid grid-rows-[2.5rem_auto] will-change-[height,opacity] h-full">
<div
class="bg-stone-700 rounded-t-2xl flex justify-between w-full p-1 min-w-0 basis-0 flex-1"
>
<span
class="text-xl font-bold pl-2 content-center truncate min-w-0"
title={pinned_display?.name}
>
{pinned_display?.name}
</span>
<div class="flex flex-row gap-1">
<div class="flex flex-row items-center mr-1">
<span class="text-stone-400"> Aktueller Status: </span>
<OnlineState
selected={false}
status={pinned_display?.status ?? null}
className="flex items-center px-2"
/>
</div>
<Button
className="aspect-square !p-1"
bg="bg-stone-600"
click_function={(e) => {
e.stopPropagation();
}}
menu_options={get_display_menu_options($pinned_display_id)}
>
<Menu />
</Button>
<Button
title="Bildschirm nicht mehr anpinnen"
className="aspect-square !p-1"
bg="bg-stone-600"
click_function={close_pinned_display}
>
<PinOff />
</Button>
</div>
</div>
<div
class="h-full bg-stone-800 rounded-b-2xl overflow-hidden flex justify-center items-center"
>
{#if pinned_display?.preview_url}
<img
src={pinned_display.preview_url}
alt="preview"
class="max-h-full max-w-full object-cover bg-black"
/>
{:else}
<div class="size-full bg-black flex justify-center items-center">
<VideoOff class="size-[20%]" />
</div>
{/if}
</div>
</div>
</div>
</Pane>
{/if}
<Pane>
<div
class="grid grid-rows-[2.5rem_auto] will-change-[height,opacity] overflow-hidden rounded-2xl"
transition:slide={{ duration: 260, easing: cubicOut }}
class="min-h-0 h-full grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl overflow-hidden"
>
<div class="bg-stone-700 flex justify-between w-full p-1 min-w-0 basis-0 flex-1">
<span
class="text-xl font-bold pl-2 content-center truncate min-w-0"
title={pinned_display?.name}
>
{pinned_display?.name}
<!-- Normal Heading Left -->
<div class="bg-stone-700 flex justify-between w-full p-1 gap-2 min-w-0">
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
Verbundene Bildschirme
</span>
<div class="flex flex-row gap-1">
<div class="flex flex-row items-center mr-1">
<span class="text-stone-400"> Aktueller Status: </span>
<OnlineState
selected={false}
status={pinned_display?.status ?? null}
className="flex items-center px-2"
/>
<button
class="gap-2 min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
all_selected($displays, $selected_display_ids),
{
bg: true,
hover: true,
active: true,
text: true
}
)}"
onclick={() => select_all($displays, $selected_display_ids)}
>
<span
>{all_selected($displays, $selected_display_ids)
? 'Alle abwählen'
: 'Alle auswählen'}</span
>
</button>
<div class="flex flex-row">
<Button
title="Bildschirme größer darstellen"
className="aspect-square !p-1 rounded-r-none"
bg="bg-stone-600"
disabled={!Boolean(next_height_step_size('display', $current_height, 1))}
click_function={() => {
change_height('display', 1);
}}
>
<Plus />
</Button>
<Button
title="Bildschirme kleiner darstellen"
className="aspect-square !p-1 rounded-l-none"
bg="bg-stone-600"
disabled={!Boolean(next_height_step_size('display', $current_height, -1))}
click_function={() => {
change_height('display', -1);
}}
>
<Minus />
</Button>
</div>
<Button
className="aspect-square !p-1"
bg="bg-stone-600"
click_function={(e) => {
e.stopPropagation();
}}
menu_options={get_display_menu_options($pinned_display_id)}
>
<Menu />
</Button>
<Button
title="Bildschirm nicht mehr anpinnen"
className="aspect-square !p-1"
bg="bg-stone-600"
click_function={() => {
$pinned_display_id = null;
}}
>
<PinOff />
</Button>
</div>
</div>
<div class="w-full h-[30dvh] bg-stone-800 flex justify-center items-center">
{#if pinned_display?.preview_url}
<img
src={pinned_display.preview_url}
alt="preview"
class="max-h-full max-w-full object-cover bg-black"
/>
{:else}
<div class="size-full bg-black flex justify-center items-center">
<VideoOff class="size-[20%]" />
</div>
{/if}
</div>
</div>
</div>
{/if}
<div class="min-h-0 h-full grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl overflow-hidden">
<!-- Normal Heading Left -->
<div class="bg-stone-700 flex justify-between w-full p-1 gap-2 min-w-0">
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
Verbundene Bildschirme
</span>
<div class="flex flex-row gap-1">
<button
class="gap-2 min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
all_selected($displays, $selected_display_ids),
{
bg: true,
hover: true,
active: true,
text: true
}
)}"
onclick={() => select_all($displays, $selected_display_ids)}
>
<span
>{all_selected($displays, $selected_display_ids)
? 'Alle abwählen'
: 'Alle auswählen'}</span
>
</button>
<div class="flex flex-row">
<Button
title="Bildschirme größer darstellen"
className="aspect-square !p-1 rounded-r-none"
bg="bg-stone-600"
disabled={!Boolean(next_height_step_size('display', $current_height, 1))}
click_function={() => {
change_height('display', 1);
<div class="min-h-0 overflow-y-auto" bind:this={displays_scroll_box}>
<div
class="min-h-full p-2 flex flex-col gap-4"
use:dragHandleZone={{
items: $displays,
type: 'group',
flipDurationMs: dnd_flip_duration_ms,
dropFromOthersDisabled: true,
dropTargetStyle: { outline: 'none' }
}}
onconsider={(e: CustomEvent) => {
$is_group_drag = true;
$displays = e.detail.items;
}}
onfinalize={(e: CustomEvent) => {
$displays = e.detail.items;
$is_group_drag = false;
}}
>
<Plus />
</Button>
<Button
title="Bildschirme kleiner darstellen"
className="aspect-square !p-1 rounded-l-none"
bg="bg-stone-600"
disabled={!Boolean(next_height_step_size('display', $current_height, -1))}
click_function={() => {
change_height('display', -1);
}}
>
<Minus />
</Button>
</div>
</div>
</div>
<div class="min-h-0 overflow-y-auto" bind:this={displays_scroll_box}>
<div
class="min-h-full p-2 flex flex-col gap-4"
use:dragHandleZone={{
items: $displays,
type: 'group',
flipDurationMs: dnd_flip_duration_ms,
dropFromOthersDisabled: true,
dropTargetStyle: { outline: 'none' }
}}
onconsider={(e: CustomEvent) => {
$is_group_drag = true;
$displays = e.detail.items;
}}
onfinalize={(e: CustomEvent) => {
$displays = e.detail.items;
$is_group_drag = false;
}}
>
{#if $displays.length === 1 && $displays[0].data.length === 0}
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf <Settings
class="inline pb-1"
/> und "Neuen Bildschirm hinzufügen".
{#if $displays.length === 1 && $displays[0].data.length === 0}
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf <Settings
class="inline pb-1"
/> und "Neuen Bildschirm hinzufügen".
</div>
{:else}
{#each $displays as display_group (display_group.id)}
<!-- Each Group -->
<section
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
class="outline-none"
>
<DisplayGroupObject {display_group} {get_display_menu_options} {close_pinned_display} />
</section>
{/each}
{/if}
</div>
{:else}
{#each $displays as display_group (display_group.id)}
<!-- Each Group -->
<section
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
class="outline-none"
>
<DisplayGroupObject {display_group} {get_display_menu_options} />
</section>
{/each}
{/if}
</div>
</div>
</div>
</div>
</Pane>
</Splitpanes>
</div>