feat(ui): add left/right arrow buttons to PageShell drag bar

Arrow buttons appear on hover at the left/right edges of the drag bar
to quickly reorder pages without dragging. Hidden when the page is
already first (no left arrow) or last (no right arrow).

- PageShell: onMoveLeft/onMoveRight props, CaretLeft/CaretRight icons
- AppPage: pass through move callbacks
- +page.svelte: handleMoveLeft/handleMoveRight with array swap + persist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 13:25:51 +02:00
parent 2529c91d58
commit ad9bbec393
3 changed files with 107 additions and 1 deletions

View file

@ -5,7 +5,15 @@
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { X, Minus, DotsSixVertical, CornersOut, CornersIn } from '@manacore/shared-icons';
import {
X,
Minus,
DotsSixVertical,
CornersOut,
CornersIn,
CaretLeft,
CaretRight,
} from '@manacore/shared-icons';
import type { Snippet, Component } from 'svelte';
interface Props {
@ -16,6 +24,8 @@
onMinimize?: () => void;
onMaximize?: () => void;
onResize?: (widthPx: number, heightPx?: number) => void;
onMoveLeft?: () => void;
onMoveRight?: () => void;
// Default header
title?: string;
color?: string;
@ -35,6 +45,8 @@
onMinimize,
onMaximize,
onResize,
onMoveLeft,
onMoveRight,
title = '',
color = '#6B7280',
icon: IconComponent,
@ -114,7 +126,33 @@
: ''}"
>
<div class="drag-handle-bar" draggable="true">
{#if onMoveLeft}
<button
class="move-btn move-left"
onclick={(e) => {
e.stopPropagation();
onMoveLeft();
}}
draggable="false"
title="Nach links"
>
<CaretLeft size={12} />
</button>
{/if}
<span class="drag-handle-icon"><DotsSixVertical size={14} /></span>
{#if onMoveRight}
<button
class="move-btn move-right"
onclick={(e) => {
e.stopPropagation();
onMoveRight();
}}
draggable="false"
title="Nach rechts"
>
<CaretRight size={12} />
</button>
{/if}
</div>
<!-- Header -->
@ -243,6 +281,7 @@
}
.drag-handle-bar {
position: relative;
display: flex;
justify-content: center;
align-items: center;
@ -269,6 +308,46 @@
:global(.dark) .drag-handle-bar:active {
background: rgba(255, 255, 255, 0.08);
}
.move-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: none;
border-radius: 50%;
background: transparent;
color: #d1d5db;
cursor: pointer;
opacity: 0;
transition:
opacity 0.15s,
color 0.15s,
background 0.15s;
}
.drag-handle-bar:hover .move-btn {
opacity: 1;
}
.move-btn:hover {
color: #6b7280;
background: rgba(0, 0, 0, 0.06);
}
.move-left {
left: 0.5rem;
}
.move-right {
right: 0.5rem;
}
:global(.dark) .move-btn {
color: #3f3b38;
}
:global(.dark) .move-btn:hover {
color: #9ca3af;
background: rgba(255, 255, 255, 0.08);
}
.drag-handle-icon {
display: flex;
align-items: center;

View file

@ -29,6 +29,8 @@
onMinimize?: () => void;
onMaximize?: () => void;
onResize?: (widthPx: number, heightPx?: number) => void;
onMoveLeft?: () => void;
onMoveRight?: () => void;
}
let {
@ -40,6 +42,8 @@
onMinimize,
onMaximize,
onResize,
onMoveLeft,
onMoveRight,
}: Props = $props();
let appEntry = $derived(getAppEntry(appId));
@ -203,6 +207,8 @@
{onMinimize}
{onMaximize}
{onResize}
{onMoveLeft}
{onMoveRight}
>
{#if loadError}
<div class="load-state">

View file

@ -135,6 +135,24 @@
persistState();
}
function handleMoveLeft(id: string) {
const idx = openApps.findIndex((a) => a.appId === id);
if (idx <= 0) return;
const apps = [...openApps];
[apps[idx - 1], apps[idx]] = [apps[idx], apps[idx - 1]];
openApps = apps;
persistState();
}
function handleMoveRight(id: string) {
const idx = openApps.findIndex((a) => a.appId === id);
if (idx === -1 || idx >= openApps.length - 1) return;
const apps = [...openApps];
[apps[idx], apps[idx + 1]] = [apps[idx + 1], apps[idx]];
openApps = apps;
persistState();
}
function handleReorder(fromId: string, toId: string) {
const fromIdx = openApps.findIndex((a) => a.appId === fromId);
const toIdx = openApps.findIndex((a) => a.appId === toId);
@ -166,6 +184,7 @@
addLabel="App hinzufügen"
>
{#snippet page(p)}
{@const idx = openApps.findIndex((a) => a.appId === p.id)}
<AppPage
appId={p.id}
widthPx={p.widthPx}
@ -175,6 +194,8 @@
onMinimize={() => handleMinimizeApp(p.id)}
onMaximize={() => handleMaximizeApp(p.id)}
onResize={(w, h) => handleResize(p.id, w, h)}
onMoveLeft={idx > 0 ? () => handleMoveLeft(p.id) : undefined}
onMoveRight={idx < openApps.length - 1 ? () => handleMoveRight(p.id) : undefined}
/>
{/snippet}
{#snippet picker()}