add rough left side of frontend

This commit is contained in:
E44
2025-10-19 23:05:45 +02:00
parent 296c8bd04b
commit 19cc4c4415
16 changed files with 5410 additions and 3 deletions
+4172
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -30,9 +30,14 @@
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-dnd-action": "^0.9.65",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4"
},
"dependencies": {
"@thisux/sveltednd": "^0.0.20",
"lucide-svelte": "^0.545.0"
}
}
+13
View File
@@ -1,2 +1,15 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
/* Adding more color steps */
@theme {
--color-stone-150: oklch(0.9465 0.002 77.571);
--color-stone-250: oklch(0.896 0.004 52.542);
--color-stone-350: oklch(0.711 0.009 57.218);
--color-stone-450: oklch(0.631 0.0115 57.165);
--color-stone-550: oklch(0.4985 0.012 65.855);
--color-stone-650: oklch(0.409 0.0105 70.599);
--color-stone-750: oklch(0.321 0.0085 50.928);
--color-stone-850: oklch(0.242 0.0065 45.171);
}
@@ -0,0 +1,37 @@
<script lang="ts">
import { get_shifted_color } from '../ts/stores/ui_behavior';
import type { MenuOption } from '../ts/types';
let {
className = '',
bg = 'bg-stone-700',
hover_bg = get_shifted_color(bg, 100),
active_bg = get_shifted_color(bg, 200),
disabled = false,
click_function = (e: MouseEvent) => {},
menu_options = null,
children
} = $props<{
className?: string;
bg?: string;
hover_bg?: string;
active_bg?: string;
disabled?: boolean;
click_function?: (e: MouseEvent) => void;
menu_options?: MenuOption[]|null;
children?: any;
}>();
if (menu_options !== null) {
}
</script>
<button
class="{className} {bg} {disabled ? "text-stone-500 cursor-not-allowed" : "hover:"+hover_bg+" active:"+active_bg+" cursor-pointer"} p-2 rounded-xl flex justify-center items-center transition-colors duration-200"
{disabled}
onclick={click_function}
>
{@render children()}
</button>
@@ -0,0 +1,16 @@
<script lang="ts">
import { GripHorizontal } from 'lucide-svelte';
import { dragHandle } from 'svelte-dnd-action';
let { bg, className = "" } = $props<{
bg: string;
className?: string;
}>();
</script>
<div
use:dragHandle
class="{className} bg-transparent hover:{bg} active:{bg} p-2 rounded-xl duration-200 transition-colors"
>
<GripHorizontal />
</div>
@@ -0,0 +1,111 @@
<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 { cubicOut } from 'svelte/easing';
import { flip } from 'svelte/animate';
import DisplayObject from './DisplayObject.svelte';
import {
add_empty_display_group,
all_displays_of_group_selected,
is_selected,
remove_empty_display_groups,
select,
select_all_of_group,
selected_display_ids,
set_new_display_group_data
} from '../ts/stores/displays';
import DNDGrip from './DNDGrip.svelte';
import { fade } from 'svelte/transition';
let { display_group } = $props<{
display_group: DisplayGroup;
}>();
let hovering_selectable = $state(false);
function select_all_of_this_group() {
const new_value = !all_displays_of_group_selected(display_group, $selected_display_ids);
select_all_of_group(display_group, new_value);
}
function handle_consider(e: CustomEvent) {
const { items, info } = e.detail;
if (items.length !== 1 && info.trigger === TRIGGERS.DRAG_STARTED) {
$is_display_drag = true;
add_empty_display_group();
}
set_new_display_group_data(display_group.id, items);
}
function handle_finalize(e: CustomEvent) {
remove_empty_display_groups();
$is_display_drag = false;
set_new_display_group_data(display_group.id, e.detail.items);
}
</script>
<div
transition:fade={{ duration: 100 }}
class="{get_selectable_color_classes(
all_displays_of_group_selected(display_group, $selected_display_ids),
{
bg: true,
hover: hovering_selectable,
active: hovering_selectable,
text: true
},
-150,
-50
)} transition-colors duration-200 rounded-2xl flex flex-row cursor-pointer"
>
<div
class="flex flex-col min-w-0 pl-2 py-2 gap-2 w-full"
use:dragHandleZone={{
items: display_group.data,
type: 'item',
flipDurationMs: dnd_flip_duration_ms,
dropTargetStyle: { outline: 'none' },
}}
onconsider={handle_consider}
onfinalize={handle_finalize}
>
{#each display_group.data as display (display.id)}
<!-- Each Group -->
<section
animate:flip={{ duration: $is_group_drag ? 0 : dnd_flip_duration_ms, easing: cubicOut }}
class="outline-none"
role="figure"
>
<DisplayObject {display} />
</section>
{/each}
{#if display_group.data.length === 0}
<div class="min-h-10 h-full w-full pl-2 py-2 flex justify-center items-center">
Hier in leere neue Gruppe ablegen
</div>
{/if}
</div>
<div
class="flex items-center justify-center px-2"
onclick={(e) => select_all_of_this_group()}
role="button"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') select_all_of_this_group();
}}
onmouseenter={() => (hovering_selectable = true)}
onmouseleave={() => (hovering_selectable = false)}
>
<DNDGrip
bg={get_selectable_color_classes(
all_displays_of_group_selected(display_group, $selected_display_ids),
{
bg: true
},
-150
)}
/>
</div>
</div>
@@ -0,0 +1,136 @@
<script lang="ts">
import Button from './Button.svelte';
import { is_selected, select, selected_display_ids } from '../ts/stores/displays';
import {
display_screen_height,
get_selectable_color_classes,
pinned_display_id
} from '../ts/stores/ui_behavior';
import DNDGrip from './DNDGrip.svelte';
import {
ArrowUpFromLine,
Eye,
Menu,
Pin,
PinOff,
SquareArrowOutDownLeft,
SquareArrowOutUpRight,
VideoOff
} from 'lucide-svelte';
import { fade } from 'svelte/transition';
import OnlineState from './OnlineState.svelte';
let { display } = $props<{
display: Display;
}>();
let hovering_unselectable = $state(false);
function onclick(e: Event) {
select(display.id);
e.stopPropagation();
}
function on_preview_click(e: MouseEvent) {
if ($pinned_display_id === display.id) {
$pinned_display_id = null;
} else {
$pinned_display_id = display.id;
}
e.stopPropagation();
}
</script>
<div
role="button"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onclick(e);
}}
{onclick}
class="p-1 {get_selectable_color_classes(is_selected(display.id, $selected_display_ids), {
bg: true,
hover: true,
active: !hovering_unselectable,
text: true
})} rounded-xl flex flex-row justify-between h-{$display_screen_height} overflow-hidde transition-colors duration-100 gap-2 cursor-pointer w-full overflow-hidden text-stone-200"
>
<div class="flex flex-row gap-4 min-w-0 flex-1">
<!-- Left Preview Screen -->
<button
class="group relative aspect-16/9 {$pinned_display_id === display.id
? 'bg-stone-800'
: 'bg-black'} h-full rounded-lg overflow-hidden cursor-pointer text-stone-200 transition-colors duration-200"
onmouseenter={() => (hovering_unselectable = true)}
onmouseleave={() => (hovering_unselectable = false)}
onclick={on_preview_click}
>
<div class="flex h-full w-full items-center justify-center">
{#if $pinned_display_id === display.id}
<div class="size-[50%]">
<Pin class="size-full" />
</div>
{:else}
<!-- No Signal -->
<VideoOff class="size-[30%]" />
{/if}
</div>
<!-- Hover Effect -->
<span
class="pointer-events-none absolute inset-0 {$pinned_display_id === display.id
? 'bg-stone-700'
: 'bg-stone-700/70'} opacity-0 transition-opacity duration-200 flex items-center justify-center group-hover:opacity-100"
>
{#if $pinned_display_id === display.id}
<PinOff class="size-[50%]" aria-hidden="true" />
{:else}
<Pin class="size-[50%]" aria-hidden="true" />
{/if}
</span>
</button>
<!-- Middle Text Block -->
<div
class="h-full flex flex-col justify-center gap-1 select-none
min-w-0 basis-0 flex-1"
>
<div class="text-xl font-bold truncate w-full" title={display.name}>
{display.name}
</div>
<OnlineState
selected={is_selected(display.id, $selected_display_ids)}
status={display.status}
/>
</div>
</div>
<!-- Right Controls -->
<div class="flex flex-row h-full items-center gap-2 pr-3">
<DNDGrip
bg={get_selectable_color_classes(is_selected(display.id, $selected_display_ids), {
bg: true
})}
/>
<div
role="figure"
onmouseenter={() => (hovering_unselectable = true)}
onmouseleave={() => (hovering_unselectable = false)}
>
<Button
bg="bg-transparent"
hover_bg={get_selectable_color_classes(is_selected(display.id, $selected_display_ids), {
bg: true
})}
active_bg={get_selectable_color_classes(is_selected(display.id, $selected_display_ids), {
bg: true
})}
click_function={(e) => {
e.stopPropagation();
}}
>
<Menu />
</Button>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
<script lang="ts">
let { selected, status, className = "" } = $props<{
selected: boolean;
status: string;
className?: string;
}>();
function get_text_color(selected: boolean, status: string) {
switch (status) {
case 'Online':
return selected ? 'text-green-700' : 'text-green-400';
case 'Lädt':
return selected ? 'text-amber-700' : 'text-amber-400';
case 'Offline':
return selected ? 'text-red-700' : 'text-red-400';
default:
return selected ? 'text-stone-700' : 'text-stone-400';
}
}
</script>
<div class="{get_text_color(selected, status)} {className} transition-colors duration-100">
{status}
</div>
@@ -0,0 +1,279 @@
<div id="splash">
<svg id="splash-svg" viewBox="0 0 950 550">
<!-- Paths -->
<!-- Disabled Paths -->
<path class="splash-stroke" d="M 385 350 V 270 H 100 V 112.5" />
<path class="splash-stroke" d="M 445 350 V 210 H 350 V 112.5" />
<path class="splash-stroke" d="M 505 350 V 210 H 600 V 112.5" />
<path class="splash-stroke" d="M 565 350 V 270 H 850 V 112.5" />
<!-- Masks -->
<defs>
<mask id="m1" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<path class="splash-mask" pathLength="100" d="M 385 350 V 270 H 100 V 112.5" />
</mask>
<mask id="m2" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<path class="splash-mask" pathLength="100" d="M 445 350 V 210 H 350 V 112.5" />
</mask>
<mask id="m3" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<path class="splash-mask" pathLength="100" d="M 505 350 V 210 H 600 V 112.5" />
</mask>
<mask id="m4" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<path class="splash-mask" pathLength="100" d="M 565 350 V 270 H 850 V 112.5" />
</mask>
</defs>
<!-- Active Paths -->
<path class="splash-stroke-active" mask="url(#m1)" d="M 385 350 V 270 H 100 V 112.5" />
<path class="splash-stroke-active" mask="url(#m2)" d="M 445 350 V 210 H 350 V 112.5" />
<path class="splash-stroke-active" mask="url(#m3)" d="M 505 350 V 210 H 600 V 112.5" />
<path class="splash-stroke-active" mask="url(#m4)" d="M 565 350 V 270 H 850 V 112.5" />
<!-- Displays -->
<rect x="0" y="0" width="200" height="112.5" rx="8" ry="8" class="splash-monitor" />
<rect x="250" y="0" width="200" height="112.5" rx="8" ry="8" class="splash-monitor" />
<rect x="500" y="0" width="200" height="112.5" rx="8" ry="8" class="splash-monitor" />
<rect x="750" y="0" width="200" height="112.5" rx="8" ry="8" class="splash-monitor" />
<!-- Text -->
<text class="splash-text" x="100" y="66.25" font-size="90">Mu</text>
<text class="splash-text" x="350" y="66.25" font-size="90">Di</text>
<text class="splash-text" x="600" y="66.25" font-size="90">C</text>
<text class="splash-text" x="850" y="66.25" font-size="90">S</text>
<!-- Controller -->
<rect
x="325"
y="350"
width="300"
height="168.75"
rx="8"
ry="8"
class="splash-control-monitor"
/>
<rect x="335" y="390" width="135" height="120" rx="8" ry="8" class="splash-other-button" />
<rect x="480" y="390" width="135" height="55" rx="8" ry="8" class="splash-other-button" />
<rect x="480" y="455" width="135" height="55" rx="8" ry="8" class="splash-start-button" />
<!-- Window Controls -->
<path class="window-controls" d="M 600 360 L 615 375" />
<path class="window-controls" d="M 600 375 L 615 360" />
<path class="window-controls" fill="none" d="M 575 360 H 590 V 375 H 575 V 360" />
<path class="window-controls" d="M 550 375 H 565" />
<svg
class="cursor"
width="50"
height="50"
viewBox="0 0 24 24"
x="510"
y="480"
aria-hidden="true"
>
<g
stroke="#ffffff"
fill="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="#000000" d="M 6.5 10 H 18 V 19 H 6" />
<path d="M22 14a8 8 0 0 1-8 8" />
<path d="M18 11v-1a2 2 0 0 0-2-2a2 2 0 0 0-2 2" />
<path d="M14 10V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1" />
<path d="M10 9.5V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v10" />
<path
d="M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"
/>
</g>
</svg>
</svg>
</div>
<style>
:root {
--base-stroke: #444444;
--base-stroke-active: #109e00;
--splash-bg: oklch(21.6% 0.006 56.043);
--monitor-dark: #000000;
--monitor-active: #ffffff;
--window-controls: #cccccc;
--start-button-inactive: #cccccc;
--start-button-hover: #77a3b1;
--start-button-active: #2c819b;
--other-button: oklch(55.3% 0.013 58.071);
--splash-control-monitor: oklch(44.4% 0.011 73.639);
--stroke-strength: 8;
--dash: 16;
--gap: 20;
}
#splash {
position: fixed;
inset: 0;
background: var(--splash-bg);
color: white;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: splash-fade 1s ease 4s forwards;
height: 100vh;
width: 100vw;
}
#splash-svg {
max-height: 40vh;
max-width: 80vw;
opacity: 0;
animation: fade-in 1s ease 500ms forwards;
}
.splash-monitor {
aspect-ratio: 16/9;
rx: 8;
ry: 8;
width: 200;
fill: var(--monitor-dark);
}
.splash-control-monitor {
fill: var(--splash-control-monitor);
}
.splash-other-button {
fill: var(--other-button);
}
.splash-stroke,
.splash-stroke-active {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: var(--stroke-strength);
stroke-dasharray: var(--dash) var(--gap);
}
.window-controls {
stroke: var(--window-controls);
stroke-width: 3;
}
.splash-stroke {
stroke: var(--base-stroke);
}
.splash-stroke-active {
stroke: var(--base-stroke-active);
}
.splash-mask {
fill: none;
stroke: #fff;
stroke-linecap: round;
stroke-width: var(--stroke-strength);
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw 1s ease-in-out 1.35s forwards;
}
.splash-text {
text-anchor: middle;
dominant-baseline: middle;
fill: var(--monitor-dark);
user-select: none;
}
.splash-monitor {
fill: var(--monitor-dark);
animation: activate-monitor 0ms ease 2.35s forwards;
}
.splash-start-button {
fill: var(--start-button-inactive);
animation: start-button-hover-press 1s ease-in-out 450ms forwards;
}
.cursor {
animation: move-mouse 1s ease-in-out 0ms forwards;
}
@keyframes fade-in {
to {
opacity: 1;
}
}
@keyframes move-mouse {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-100px);
}
100% {
transform: translateX(0px);
}
}
@keyframes start-button-hover-press {
0% {
fill: var(--start-button-inactive);
}
30% {
fill: var(--start-button-inactive);
}
40% {
fill: var(--start-button-hover);
}
70% {
fill: var(--start-button-hover);
}
80% {
fill: var(--start-button-active);
}
90% {
fill: var(--start-button-active);
}
100% {
fill: var(--start-button-hover);
}
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes activate-monitor {
to {
fill: var(--monitor-active);
}
}
@keyframes splash-fade {
to {
opacity: 0;
visibility: hidden;
}
}
rect {
vector-effect: non-scaling-stroke;
}
/* verhindert, dass die Linien beim Skalieren dicker werden */
.cursor * {
vector-effect: non-scaling-stroke;
}
</style>
+214 -2
View File
@@ -1,2 +1,214 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script lang="ts">
import { Menu, Minus, PinOff, Plus, Settings, Square, VideoOff, X } from 'lucide-svelte';
import Button from '../components/Button.svelte';
import SplashScreen from '../components/SplashScreen.svelte';
import {
change_display_screen_height,
display_screen_height,
dnd_flip_duration_ms,
get_selectable_color_classes,
is_display_drag,
is_group_drag,
next_step_possible,
pinned_display_id
} from '../ts/stores/ui_behavior';
import { dragHandleZone } from 'svelte-dnd-action';
import {
all_displays_of_group_selected,
displays,
get_display_by_id,
is_selected,
select_all_of_group,
selected_display_ids
} from '../ts/stores/displays';
import { cubicOut } from 'svelte/easing';
import { flip } from 'svelte/animate';
import DisplayGroupObject from '../components/DisplayGroupObject.svelte';
import { blur, draw, fade, fly, scale, slide } from 'svelte/transition';
import OnlineState from '../components/OnlineState.svelte';
import type { DisplayGroup } from '../ts/types';
let displays_scroll_box: HTMLElement;
function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
const new_value = !all_selected(current_displays, current_selected_display_ids);
for (const display_group of current_displays) {
select_all_of_group(display_group, new_value);
}
}
function all_selected(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
for (const display_group of current_displays) {
if (!all_displays_of_group_selected(display_group, current_selected_display_ids)) {
return false;
}
}
return true;
}
function on_wheel(e: WheelEvent) {
if (!$is_group_drag && !$is_display_drag) return;
if (!displays_scroll_box) return;
// apply custom scroll feature
e.preventDefault();
(displays_scroll_box as HTMLElement).scrollBy?.({
top: e.deltaY,
behavior: 'auto'
});
}
</script>
<svelte:window on:wheel={on_wheel} />
<main class="bg-stone-900 h-dvh w-dvw text-stone-200 p-4 gap-4 grid grid-rows-[3rem_auto]">
<!-- <SplashScreen></SplashScreen> -->
<div class="w-[calc(100dvw-(8*var(--spacing)))] flex justify-between">
<span class="text-4xl font-bold content-center h-full"> PLG MuDiCS </span>
<Button className="aspect-square" bg="bg-stone-800">
<Settings></Settings>
</Button>
</div>
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
<div class="h-[calc(100dvh-3rem-(12*var(--spacing)))] overflow-hidden flex flex-col gap-2">
{#if $pinned_display_id}
<!-- Pinned Item -->
<div in:fade={{ duration: 140 }} out:fade={{ duration: 120 }}>
<div
class="grid grid-rows-[2.5rem_auto] overflow-hidden will-change-[height,opacity] rounded-2xl"
transition:slide={{ duration: 260, easing: cubicOut }}
>
<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={get_display_by_id($pinned_display_id)?.name}
>
{get_display_by_id($pinned_display_id)?.name}
</span>
<div class="flex flex-row gap-1">
<OnlineState
selected={false}
status={get_display_by_id($pinned_display_id)?.status ?? ''}
className="flex items-center px-2"
/>
<Button
className="aspect-square !p-1"
bg="bg-stone-600"
click_function={() => {
change_display_screen_height(1);
}}
>
<Menu />
</Button>
<Button
className="aspect-square !p-1"
bg="bg-stone-600"
click_function={() => {
$pinned_display_id = null;
}}
>
<PinOff />
</Button>
</div>
</div>
<div
class="w-full max-h-[30dvh] aspect-16/9 bg-stone-800 flex justify-center items-center"
>
<div class="aspect-16/9 h-full bg-black flex justify-center items-center">
<VideoOff class="size-[20%]" />
</div>
</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">
<span class="text-xl font-bold pl-2 content-center"> Bereits verbundene Displays </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-ro">
<Button
className="aspect-square !p-1 rounded-r-none"
bg="bg-stone-600"
disabled={next_step_possible($display_screen_height, 1)}
click_function={() => {
change_display_screen_height(1);
}}
>
<Plus />
</Button>
<Button
className="aspect-square !p-1 rounded-l-none"
bg="bg-stone-600"
disabled={next_step_possible($display_screen_height, -1)}
click_function={() => {
change_display_screen_height(-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;
}}
>
{#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} />
</section>
{/each}
</div>
</div>
</div>
</div>
<div class="col-start-2 h-[calc(100dvh-3rem-(12*var(--spacing)))] bg-stone-800 rounded-2xl">
ok
</div>
</div>
</main>
@@ -0,0 +1,35 @@
<script lang="ts">
let isHoveringChild = false;
function handleClickParent() {
if (!isHoveringChild) {
console.log('Parent clicked');
}
}
function handleClickChild(event: MouseEvent) {
console.log('Child clicked');
// Verhindert, dass das Event nach oben propagiert
event.stopPropagation();
}
</script>
<div
class={`relative rounded-lg p-6 transition-colors duration-200
${isHoveringChild ? 'bg-gray-200' : 'bg-gray-300 hover:bg-gray-400 active:bg-gray-500'}
`}
on:click={handleClickParent}
>
<p>Ich bin das Haupt-Div</p>
<div
class="no-display-selectable mt-4 p-4 bg-white border rounded shadow cursor-pointer"
on:mouseenter={() => (isHoveringChild = true)}
on:mouseleave={() => (isHoveringChild = false)}
on:click={handleClickChild}
>
Ich bin das Kind-Div (no-display-selectable)
</div>
<p class="mt-4">Noch mehr Inhalt...</p>
</div>
+73
View File
@@ -0,0 +1,73 @@
module.exports = {
content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: [
'bg-stone-50',
'bg-stone-100',
'bg-stone-150',
'bg-stone-200',
'bg-stone-250',
'bg-stone-300',
'bg-stone-350',
'bg-stone-400',
'bg-stone-450',
'bg-stone-500',
'bg-stone-550',
'bg-stone-600',
'bg-stone-650',
'bg-stone-700',
'bg-stone-750',
'bg-stone-800',
'bg-stone-850',
'bg-stone-900',
'bg-stone-950',
'hover:bg-stone-50',
'hover:bg-stone-100',
'hover:bg-stone-150',
'hover:bg-stone-200',
'hover:bg-stone-250',
'hover:bg-stone-300',
'hover:bg-stone-350',
'hover:bg-stone-400',
'hover:bg-stone-450',
'hover:bg-stone-500',
'hover:bg-stone-550',
'hover:bg-stone-600',
'hover:bg-stone-650',
'hover:bg-stone-700',
'hover:bg-stone-750',
'hover:bg-stone-800',
'hover:bg-stone-850',
'hover:bg-stone-900',
'hover:bg-stone-950',
'active:bg-stone-50',
'active:bg-stone-100',
'active:bg-stone-150',
'active:bg-stone-200',
'active:bg-stone-250',
'active:bg-stone-300',
'active:bg-stone-350',
'active:bg-stone-400',
'active:bg-stone-450',
'active:bg-stone-500',
'active:bg-stone-550',
'active:bg-stone-600',
'active:bg-stone-650',
'active:bg-stone-700',
'active:bg-stone-750',
'active:bg-stone-800',
'active:bg-stone-850',
'active:bg-stone-900',
'active:bg-stone-950',
'h-5',
'h-10',
'h-15',
'h-20',
'h-25',
'h-30',
'h-35',
'h-40',
],
}
+108
View File
@@ -0,0 +1,108 @@
import { get, writable, type Writable } from "svelte/store";
export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
id: crypto.randomUUID(),
data: []
}]);
export const selected_display_ids: Writable<string[]> = writable<string[]>([]);
add_testing_displays();
function add_display(ip: string, mac: string, name: string, status: string) {
displays.update((displays: DisplayGroup[]) => {
displays[0].data.push({ id: crypto.randomUUID(), ip, mac, name, status });
return displays;
});
}
export function select(display_id: string, new_value: boolean | null = null) {
selected_display_ids.update((all_ids: string[]) => {
if (all_ids.includes(display_id)) {
const index = all_ids.indexOf(display_id);
if (index > -1 && new_value !== true) {
all_ids.splice(index, 1);
}
} else if (new_value !== false) {
all_ids.push(display_id);
}
return all_ids;
});
}
export function is_selected(display_id: string, current_selected_display_ids: string[]): boolean {
return current_selected_display_ids.includes(display_id);
}
export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) {
if (display_group.data.length === 0) return false;
for (const display of display_group.data) {
if (!is_selected(display.id, current_selected_displays)) {
return false;
}
}
return true;
}
export function select_all_of_group(display_group: DisplayGroup, new_value: boolean | null = null) {
for (const display of display_group.data) {
select(display.id, new_value);
}
}
export function set_new_display_group_data(display_group_id: string, new_data: Display[]) {
displays.update((displays: DisplayGroup[]) => {
for (const display_group of displays) {
if (display_group.id === display_group_id) {
display_group.data = new_data;
}
}
return displays;
});
}
export function get_display_by_id(display_id: string) {
const displays_array = get(displays);
for (const display_group of displays_array) {
for (const display of display_group.data) {
if (display.id === display_id) {
return display;
}
}
}
return null;
}
export function add_empty_display_group() {
displays.update((displays: DisplayGroup[]) => {
displays.push({
id: crypto.randomUUID(),
data: [],
});
return displays;
});
}
export function remove_empty_display_groups() {
displays.update((displays: DisplayGroup[]) => {
for (let i = displays.length - 1; i >= 0; i--) {
if (displays[i].data.length === 0) {
displays.splice(i, 1);
}
}
return displays;
});
}
function add_testing_displays() {
const names = ["Vorne Rechts", "Vorne Links", "Vorne Mitte", "Fernseher Rechts", "Fernseher Bühne", "UIUIUIUIUIUIUISEHRLANGERTEXT DER IST WIRKLICH LANG, DER TEXT, so lang, dass er wirklich nirgendswo hinpasst, nichtmal da oben /\\"];
for (const name of names) {
add_display("192.168.1.42", "00:1A:2B:3C:4D:5E", name, "Offline");
}
}
@@ -0,0 +1,57 @@
import type { NumericRange } from "@sveltejs/kit";
import { get, writable, type Writable } from "svelte/store";
const screen_height_step_size = 5;
export const dnd_flip_duration_ms = 300;
const min_display_screen_height = 15;
const max_display_screen_height = 40;
export const display_screen_height: Writable<number> = writable<number>(25);
export const is_group_drag: Writable<boolean> = writable<boolean>(false);
export const is_display_drag: Writable<boolean> = writable<boolean>(false);
export const pinned_display_id: Writable<string | null> = writable<string | null>(null);
export function change_display_screen_height(factor: number) {
display_screen_height.update((current_height) => {
const new_size = current_height + (factor * screen_height_step_size);
if (new_size > max_display_screen_height || new_size < min_display_screen_height) {
return current_height;
} else {
return new_size;
}
});
}
export function next_step_possible(current_height: number, factor: number) {
const new_size = current_height + (factor * screen_height_step_size);
return new_size > max_display_screen_height || new_size < min_display_screen_height;
}
export function get_selectable_color_classes(
selected: boolean,
returning_classes: { bg?: boolean; hover?: boolean; active?: boolean; text?: boolean } = {},
base_bg_distance: number = 0,
shifted_distance: number = 0
) {
let base_bg = selected ? 'bg-stone-400' : 'bg-stone-600';
const base_text = selected ? 'text-stone-950' : 'text-stone-200';
base_bg = get_shifted_color(base_bg, base_bg_distance);
const { bg = false, hover = false, active = false, text = false } = returning_classes;
const out: string[] = [];
if (bg) out.push(base_bg);
if (hover) out.push('hover:' + get_shifted_color(base_bg, 100 + shifted_distance));
if (active) out.push('active:' + get_shifted_color(base_bg, 150 + shifted_distance));
if (text) out.push(base_text);
return out.join(' ');
}
export function get_shifted_color(base_color: string, distance: number): string {
return base_color.replace(/(\d+)(?=(?:\/\d+)?$)/, (m: string) => String(+Number(m) - distance));
}
+22
View File
@@ -0,0 +1,22 @@
import type { Component } from "lucide-svelte";
export type Display = {
id: string;
ip: string;
mac: string;
name: string;
status: string;
}
export type DisplayGroup = {
id: string;
data: Display[];
};
export type MenuOption = {
icon?: Component;
name: string;
on_select?: () => void;
disabled?: boolean;
}
+108 -1
View File
@@ -1 +1,108 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 950 550"
width="950" height="550">
<!-- Disabled Paths -->
<path d="M 385 350 V 270 H 100 V 112.5"
fill="none"
stroke="#444444"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<path d="M 445 350 V 210 H 350 V 112.5"
fill="none"
stroke="#444444"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<path d="M 505 350 V 210 H 600 V 112.5"
fill="none"
stroke="#444444"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<path d="M 565 350 V 270 H 850 V 112.5"
fill="none"
stroke="#444444"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<!-- Active Paths (final state) -->
<path d="M 385 350 V 270 H 100 V 112.5"
fill="none"
stroke="#109e00"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<path d="M 445 350 V 210 H 350 V 112.5"
fill="none"
stroke="#109e00"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<path d="M 505 350 V 210 H 600 V 112.5"
fill="none"
stroke="#109e00"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<path d="M 565 350 V 270 H 850 V 112.5"
fill="none"
stroke="#109e00"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 20"/>
<!-- Displays -->
<rect x="0" y="0" width="200" height="112.5" rx="8" ry="8"
fill="#ffffff" vector-effect="non-scaling-stroke"/>
<rect x="250" y="0" width="200" height="112.5" rx="8" ry="8"
fill="#ffffff" vector-effect="non-scaling-stroke"/>
<rect x="500" y="0" width="200" height="112.5" rx="8" ry="8"
fill="#ffffff" vector-effect="non-scaling-stroke"/>
<rect x="750" y="0" width="200" height="112.5" rx="8" ry="8"
fill="#ffffff" vector-effect="non-scaling-stroke"/>
<!-- Controller -->
<rect x="325" y="350" width="300" height="168.75" rx="8" ry="8"
fill="#686868" vector-effect="non-scaling-stroke"/>
<rect x="335" y="390" width="135" height="120" rx="8" ry="8"
fill="#888888" vector-effect="non-scaling-stroke"/>
<rect x="480" y="390" width="135" height="55" rx="8" ry="8"
fill="#888888" vector-effect="non-scaling-stroke"/>
<rect x="480" y="455" width="135" height="55" rx="8" ry="8"
fill="#77a3b1" vector-effect="non-scaling-stroke"/>
<!-- Window Controls -->
<path d="M 600 360 L 615 375"
stroke="#CCCCCC" stroke-width="3" fill="none"/>
<path d="M 600 375 L 615 360"
stroke="#CCCCCC" stroke-width="3" fill="none"/>
<path d="M 575 360 H 590 V 375 H 575 V 360"
stroke="#CCCCCC" stroke-width="3" fill="none"/>
<path d="M 550 375 H 565"
stroke="#CCCCCC" stroke-width="3" fill="none"/>
<!-- Cursor (final state, no animation) -->
<svg width="50" height="50" viewBox="0 0 24 24" x="510" y="480" aria-hidden="true">
<g stroke="#ffffff" fill="#000000" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M 6.5 10 H 18 V 19 H 6"/>
<path d="M22 14a8 8 0 0 1-8 8"/>
<path d="M18 11v-1a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/>
<path d="M14 10V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1"/>
<path d="M10 9.5V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v10"/>
<path d="M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB