mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-24 03:16:44 +02:00
refactor(workbench): redesign page cards — rounder corners, unified header, remove DnD
- Increase border-radius to 1.25rem for page cards and add button - Merge toolbar bar and header into single row (title left, actions right) - Remove drag-and-drop reorder in favor of arrow buttons - Make window action icons larger (24px bold) with more spacing - Title icon monochrome with reduced opacity - Remove onReorder prop and handleReorder from all carousel consumers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2c9795405
commit
f5ad492371
5 changed files with 72 additions and 214 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<!--
|
<!--
|
||||||
PageCarousel — Shared horizontal carousel with drag reorder and add button.
|
PageCarousel — Shared horizontal carousel with add button.
|
||||||
The scene+app bar is rendered in the layout's bottom-stack via bottomBarStore.
|
The scene+app bar is rendered in the layout's bottom-stack via bottomBarStore.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
pages: CarouselPage[];
|
pages: CarouselPage[];
|
||||||
defaultWidth?: number;
|
defaultWidth?: number;
|
||||||
showPicker: boolean;
|
showPicker: boolean;
|
||||||
onReorder: (fromId: string, toId: string) => void;
|
|
||||||
onRestore?: (id: string) => void;
|
onRestore?: (id: string) => void;
|
||||||
onMaximize?: (id: string) => void;
|
onMaximize?: (id: string) => void;
|
||||||
onRemove?: (id: string) => void;
|
onRemove?: (id: string) => void;
|
||||||
|
|
@ -26,7 +25,6 @@
|
||||||
pages,
|
pages,
|
||||||
defaultWidth = 480,
|
defaultWidth = 480,
|
||||||
showPicker,
|
showPicker,
|
||||||
onReorder,
|
|
||||||
onRestore: _onRestore,
|
onRestore: _onRestore,
|
||||||
onMaximize: _onMaximize,
|
onMaximize: _onMaximize,
|
||||||
onRemove: _onRemove,
|
onRemove: _onRemove,
|
||||||
|
|
@ -37,35 +35,6 @@
|
||||||
picker,
|
picker,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let dragId = $state<string | null>(null);
|
|
||||||
|
|
||||||
function handleDragStart(e: DragEvent, id: string) {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (!target.closest('.drag-handle')) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dragId = id;
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
e.dataTransfer.setData('text/plain', id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleDragOver(e: DragEvent) {
|
|
||||||
if (!dragId) return;
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
||||||
}
|
|
||||||
function handleDrop(e: DragEvent, targetId: string) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!dragId || dragId === targetId) return;
|
|
||||||
onReorder(dragId, targetId);
|
|
||||||
dragId = null;
|
|
||||||
}
|
|
||||||
function handleDragEnd() {
|
|
||||||
dragId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pickerEl = $state<HTMLDivElement | null>(null);
|
let pickerEl = $state<HTMLDivElement | null>(null);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (showPicker && pickerEl)
|
if (showPicker && pickerEl)
|
||||||
|
|
@ -76,16 +45,7 @@
|
||||||
<div class="carousel-root">
|
<div class="carousel-root">
|
||||||
<div class="fokus-track" style="--sheet-width: {defaultWidth}px">
|
<div class="fokus-track" style="--sheet-width: {defaultWidth}px">
|
||||||
{#each pages as p, idx (p.id)}
|
{#each pages as p, idx (p.id)}
|
||||||
<div
|
<div class="page-wrapper" role="listitem" data-page-id={p.id}>
|
||||||
class="page-drag-wrapper"
|
|
||||||
role="listitem"
|
|
||||||
class:dragging={dragId === p.id}
|
|
||||||
data-page-id={p.id}
|
|
||||||
ondragstart={(e) => handleDragStart(e, p.id)}
|
|
||||||
ondragover={handleDragOver}
|
|
||||||
ondrop={(e) => handleDrop(e, p.id)}
|
|
||||||
ondragend={handleDragEnd}
|
|
||||||
>
|
|
||||||
{@render pageSnippet(p, idx)}
|
{@render pageSnippet(p, idx)}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -131,12 +91,8 @@
|
||||||
.fokus-track::-webkit-scrollbar {
|
.fokus-track::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.page-drag-wrapper {
|
.page-wrapper {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.page-drag-wrapper.dragging {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
}
|
||||||
.add-card {
|
.add-card {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
<!--
|
<!--
|
||||||
PageShell — Shared card wrapper for pages in a carousel.
|
PageShell — Shared card wrapper for pages in a carousel.
|
||||||
Provides: drag handle, header, resize handle, maximized mode.
|
Provides: header, resize handle, maximized mode.
|
||||||
Used by workbench (AppPage) and todo (TodoPage).
|
Used by workbench (AppPage) and todo (TodoPage).
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import {
|
import { X, CornersOut, CornersIn, CaretLeft, CaretRight } from '@mana/shared-icons';
|
||||||
X,
|
|
||||||
DotsSixVertical,
|
|
||||||
CornersOut,
|
|
||||||
CornersIn,
|
|
||||||
CaretLeft,
|
|
||||||
CaretRight,
|
|
||||||
} from '@mana/shared-icons';
|
|
||||||
import type { Snippet, Component } from 'svelte';
|
import type { Snippet, Component } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -127,72 +120,15 @@
|
||||||
? `height: ${heightPx}px; min-height: 0;`
|
? `height: ${heightPx}px; min-height: 0;`
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
<!-- Header with window actions -->
|
||||||
<div class="drag-handle-bar" draggable="true" oncontextmenu={onContextMenu} role="toolbar">
|
<div class="page-header" oncontextmenu={onContextMenu} role="banner">
|
||||||
{#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>
|
|
||||||
<div class="window-actions">
|
|
||||||
{#if onMaximize}
|
|
||||||
<button
|
|
||||||
class="window-btn"
|
|
||||||
onclick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onMaximize();
|
|
||||||
}}
|
|
||||||
draggable="false"
|
|
||||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
|
||||||
>
|
|
||||||
{#if maximized}<CornersIn size={12} />{:else}<CornersOut size={12} />{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
class="window-btn window-btn-close"
|
|
||||||
onclick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
draggable="false"
|
|
||||||
title={$_('common.close')}
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#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 -->
|
|
||||||
<div class="page-header" ondragstart={(e) => e.preventDefault()} role="banner">
|
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
{#if header_left}
|
{#if header_left}
|
||||||
{@render header_left()}
|
{@render header_left()}
|
||||||
{:else}
|
{:else}
|
||||||
{#if IconComponent}
|
{#if IconComponent}
|
||||||
<span class="header-icon" style="color: {color}">
|
<span class="header-icon">
|
||||||
<IconComponent size={24} weight="fill" />
|
<IconComponent size={28} weight="bold" />
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="color-dot" style="background-color: {color}"></span>
|
<span class="color-dot" style="background-color: {color}"></span>
|
||||||
|
|
@ -204,8 +140,6 @@
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
ondragstart={(e) => e.preventDefault()}
|
|
||||||
draggable="false"
|
|
||||||
title={`${title} in neuem Tab öffnen`}
|
title={`${title} in neuem Tab öffnen`}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
|
@ -218,6 +152,57 @@
|
||||||
{@render badge()}
|
{@render badge()}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="window-actions">
|
||||||
|
{#if onMoveLeft}
|
||||||
|
<button
|
||||||
|
class="window-btn"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onMoveLeft();
|
||||||
|
}}
|
||||||
|
title="Nach links"
|
||||||
|
>
|
||||||
|
<CaretLeft size={24} weight="bold" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if onMoveRight}
|
||||||
|
<button
|
||||||
|
class="window-btn"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onMoveRight();
|
||||||
|
}}
|
||||||
|
title="Nach rechts"
|
||||||
|
>
|
||||||
|
<CaretRight size={24} weight="bold" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if onMaximize}
|
||||||
|
<button
|
||||||
|
class="window-btn"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onMaximize();
|
||||||
|
}}
|
||||||
|
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||||
|
>
|
||||||
|
{#if maximized}<CornersIn size={24} weight="bold" />{:else}<CornersOut
|
||||||
|
size={24}
|
||||||
|
weight="bold"
|
||||||
|
/>{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="window-btn window-btn-close"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
title={$_('common.close')}
|
||||||
|
>
|
||||||
|
<X size={24} weight="bold" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Optional toolbar (e.g. PageEditBar) -->
|
<!-- Optional toolbar (e.g. PageEditBar) -->
|
||||||
|
|
@ -302,79 +287,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle-bar {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.2rem 0;
|
|
||||||
cursor: grab;
|
|
||||||
background: transparent;
|
|
||||||
border-bottom: none;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
.drag-handle-bar:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.drag-handle-bar:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.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: hsl(var(--color-muted-foreground) / 0.5);
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
color 0.15s,
|
|
||||||
background 0.15s;
|
|
||||||
}
|
|
||||||
.move-btn:hover {
|
|
||||||
color: hsl(var(--color-foreground));
|
|
||||||
background: hsl(var(--color-surface-hover));
|
|
||||||
}
|
|
||||||
.move-left {
|
|
||||||
left: 0.5rem;
|
|
||||||
}
|
|
||||||
.move-right {
|
|
||||||
right: 0.5rem;
|
|
||||||
}
|
|
||||||
.drag-handle-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: hsl(var(--color-muted-foreground) / 0.5);
|
|
||||||
transform: rotate(90deg);
|
|
||||||
transition: color 0.15s;
|
|
||||||
}
|
|
||||||
.drag-handle-bar:hover .drag-handle-icon {
|
|
||||||
color: hsl(var(--color-muted-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.375rem 1rem;
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
.header-left {
|
.header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.3rem;
|
||||||
}
|
}
|
||||||
.header-icon {
|
.header-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.color-dot {
|
.color-dot {
|
||||||
width: 0.625rem;
|
width: 0.625rem;
|
||||||
|
|
@ -383,9 +313,11 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 1.125rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: hsl(var(--color-foreground));
|
color: hsl(var(--color-foreground));
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
a.page-title-link {
|
a.page-title-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -398,20 +330,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-actions {
|
.window-actions {
|
||||||
position: absolute;
|
|
||||||
right: 2rem;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.125rem;
|
gap: 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.window-btn {
|
.window-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 18px;
|
width: 28px;
|
||||||
height: 18px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
|
||||||
|
|
@ -129,10 +129,6 @@
|
||||||
function handleMoveRight(id: string) {
|
function handleMoveRight(id: string) {
|
||||||
workbenchScenesStore.moveAppRight(id);
|
workbenchScenesStore.moveAppRight(id);
|
||||||
}
|
}
|
||||||
function handleReorder(fromId: string, toId: string) {
|
|
||||||
workbenchScenesStore.reorderApps(fromId, toId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Card / tab context menus ────────────────────────────
|
// ── Card / tab context menus ────────────────────────────
|
||||||
const ctxMenu = createWorkbenchContextMenu();
|
const ctxMenu = createWorkbenchContextMenu();
|
||||||
|
|
||||||
|
|
@ -246,7 +242,6 @@
|
||||||
pages={carouselPages}
|
pages={carouselPages}
|
||||||
defaultWidth={DEFAULT_WIDTH}
|
defaultWidth={DEFAULT_WIDTH}
|
||||||
{showPicker}
|
{showPicker}
|
||||||
onReorder={handleReorder}
|
|
||||||
onTogglePicker={() => (showPicker = !showPicker)}
|
onTogglePicker={() => (showPicker = !showPicker)}
|
||||||
addLabel="App hinzufügen"
|
addLabel="App hinzufügen"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -120,16 +120,6 @@
|
||||||
openPages = openPages.map((p) => (p.id === id ? { ...p, widthPx } : p));
|
openPages = openPages.map((p) => (p.id === id ? { ...p, widthPx } : p));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReorder(fromId: string, toId: string) {
|
|
||||||
const fromIdx = openPages.findIndex((p) => p.id === fromId);
|
|
||||||
const toIdx = openPages.findIndex((p) => p.id === toId);
|
|
||||||
if (fromIdx === -1 || toIdx === -1) return;
|
|
||||||
const pages = [...openPages];
|
|
||||||
const [moved] = pages.splice(fromIdx, 1);
|
|
||||||
pages.splice(toIdx, 0, moved);
|
|
||||||
openPages = pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToContact(contact: Contact) {
|
function navigateToContact(contact: Contact) {
|
||||||
window.location.href = `/contacts/${contact.id}`;
|
window.location.href = `/contacts/${contact.id}`;
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +167,6 @@
|
||||||
pages={carouselPages}
|
pages={carouselPages}
|
||||||
defaultWidth={DEFAULT_WIDTH}
|
defaultWidth={DEFAULT_WIDTH}
|
||||||
{showPicker}
|
{showPicker}
|
||||||
onReorder={handleReorder}
|
|
||||||
onRestore={handleRestorePage}
|
onRestore={handleRestorePage}
|
||||||
onMaximize={handleMaximizePage}
|
onMaximize={handleMaximizePage}
|
||||||
onRemove={handleRemovePage}
|
onRemove={handleRemovePage}
|
||||||
|
|
|
||||||
|
|
@ -181,16 +181,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReorder(fromId: string, toId: string) {
|
|
||||||
const fromIdx = openPages.findIndex((p) => p.id === fromId);
|
|
||||||
const toIdx = openPages.findIndex((p) => p.id === toId);
|
|
||||||
if (fromIdx === -1 || toIdx === -1) return;
|
|
||||||
const pages = [...openPages];
|
|
||||||
const [moved] = pages.splice(fromIdx, 1);
|
|
||||||
pages.splice(toIdx, 0, moved);
|
|
||||||
openPages = pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Custom page CRUD ────────────────────────────────────
|
// ── Custom page CRUD ────────────────────────────────────
|
||||||
function handleCreateCustomPage() {
|
function handleCreateCustomPage() {
|
||||||
const id = `custom-${crypto.randomUUID().slice(0, 8)}`;
|
const id = `custom-${crypto.randomUUID().slice(0, 8)}`;
|
||||||
|
|
@ -257,7 +247,6 @@
|
||||||
pages={carouselPages}
|
pages={carouselPages}
|
||||||
defaultWidth={DEFAULT_WIDTH}
|
defaultWidth={DEFAULT_WIDTH}
|
||||||
showPicker={showPagePicker}
|
showPicker={showPagePicker}
|
||||||
onReorder={handleReorder}
|
|
||||||
onRestore={handleRestorePage}
|
onRestore={handleRestorePage}
|
||||||
onMaximize={handleMaximizePage}
|
onMaximize={handleMaximizePage}
|
||||||
onRemove={handleRemovePage}
|
onRemove={handleRemovePage}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue