mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
refactor(manacore/web): unify page carousel system, remove edit mode
Extract shared PageShell and PageCarousel components from duplicated workbench/todo code. Remove edit mode (FAB, width pills, move buttons) from both routes. Add per-page resize handle (drag bottom-right corner). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9e078492a5
commit
079015ade7
9 changed files with 928 additions and 1494 deletions
|
|
@ -0,0 +1,349 @@
|
|||
<!--
|
||||
PageCarousel — Shared horizontal carousel with drag reorder, minimized tabs, and add button.
|
||||
Used by workbench (home) and todo routes.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, X, ArrowsOut } from '@manacore/shared-icons';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export interface CarouselPage {
|
||||
id: string;
|
||||
minimized: boolean;
|
||||
maximized?: boolean;
|
||||
widthPx: number;
|
||||
title: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pages: CarouselPage[];
|
||||
defaultWidth?: number;
|
||||
showPicker: boolean;
|
||||
onReorder: (fromId: string, toId: string) => void;
|
||||
onRestore: (id: string) => void;
|
||||
onMaximize: (id: string) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onTogglePicker: () => void;
|
||||
addLabel?: string;
|
||||
page: Snippet<[CarouselPage, number]>;
|
||||
picker?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
pages,
|
||||
defaultWidth = 480,
|
||||
showPicker,
|
||||
onReorder,
|
||||
onRestore,
|
||||
onMaximize,
|
||||
onRemove,
|
||||
onTogglePicker,
|
||||
addLabel = 'Hinzufügen',
|
||||
page: pageSnippet,
|
||||
picker,
|
||||
}: Props = $props();
|
||||
|
||||
let expandedPages = $derived(pages.filter((p) => !p.minimized));
|
||||
let minimizedPages = $derived(pages.filter((p) => p.minimized));
|
||||
|
||||
// ── Drag reorder ────────────────────────────────────────
|
||||
let dragId = $state<string | null>(null);
|
||||
|
||||
function handleDragStart(e: DragEvent, id: string) {
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Picker scroll ───────────────────────────────────────
|
||||
let pickerEl = $state<HTMLDivElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (showPicker && pickerEl) {
|
||||
pickerEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="carousel-root">
|
||||
<!-- Carousel track -->
|
||||
<div class="fokus-track" style="--sheet-width: {defaultWidth}px">
|
||||
{#each expandedPages as p, idx (p.id)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="page-drag-wrapper"
|
||||
class:dragging={dragId === p.id}
|
||||
draggable={true}
|
||||
ondragstart={(e) => handleDragStart(e, p.id)}
|
||||
ondragover={handleDragOver}
|
||||
ondrop={(e) => handleDrop(e, p.id)}
|
||||
ondragend={handleDragEnd}
|
||||
>
|
||||
{@render pageSnippet(p, idx)}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Picker / add button -->
|
||||
{#if expandedPages.length === 0}
|
||||
<div class="empty-wrapper">
|
||||
{#if showPicker && picker}
|
||||
{@render picker()}
|
||||
{:else}
|
||||
<button class="add-card alone" onclick={onTogglePicker}>
|
||||
<Plus size={24} />
|
||||
<span class="add-label">{addLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if showPicker && picker}
|
||||
<div bind:this={pickerEl}>
|
||||
{@render picker()}
|
||||
</div>
|
||||
{:else}
|
||||
<button class="add-card" onclick={onTogglePicker} title={addLabel}>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Minimized tabs -->
|
||||
{#if minimizedPages.length > 0}
|
||||
<div class="minimized-tabs">
|
||||
{#each minimizedPages as p (p.id)}
|
||||
<div class="minimized-tab">
|
||||
<span class="tab-dot" style="background-color: {p.color}"></span>
|
||||
<button class="tab-title" onclick={() => onRestore(p.id)}>
|
||||
{p.title}
|
||||
</button>
|
||||
<button class="tab-maximize" onclick={() => onMaximize(p.id)} title="Maximieren">
|
||||
<ArrowsOut size={12} />
|
||||
</button>
|
||||
<button class="tab-close" onclick={() => onRemove(p.id)} title={$_('common.close')}>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="tab-add" onclick={onTogglePicker} title={addLabel}>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.carousel-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Carousel track */
|
||||
.fokus-track {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 1rem calc(50% - var(--sheet-width) / 2);
|
||||
scrollbar-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
.fokus-track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-drag-wrapper {
|
||||
flex: 0 0 auto;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.page-drag-wrapper.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
.add-card {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
border: 2px dashed rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.empty-wrapper {
|
||||
flex: 0 0 auto;
|
||||
width: var(--sheet-width, 480px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
.add-card.alone {
|
||||
width: 100%;
|
||||
min-height: 60vh;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.add-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 4%, transparent);
|
||||
}
|
||||
:global(.dark) .add-card {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .add-card.alone {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .add-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent);
|
||||
}
|
||||
.add-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Minimized tabs */
|
||||
.minimized-tabs {
|
||||
position: fixed;
|
||||
bottom: 4.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 45;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
background: #fffef5;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
animation: slideUp 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .minimized-tabs {
|
||||
background: #252220;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
.minimized-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.minimized-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .minimized-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.tab-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab-title {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #374151;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
}
|
||||
.tab-title:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
:global(.dark) .tab-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
:global(.dark) .tab-title:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
.tab-maximize,
|
||||
.tab-close {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0.125rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.minimized-tab:hover .tab-maximize,
|
||||
.minimized-tab:hover .tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
.tab-maximize:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
.tab-close:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.tab-add {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.15s;
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
.tab-add:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
<!--
|
||||
PageShell — Shared card wrapper for pages in a carousel.
|
||||
Provides: drag handle, header, resize handle, maximized mode.
|
||||
Used by workbench (AppPage) and todo (TodoPage).
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { X, Minus, DotsSixVertical, CornersOut, CornersIn } from '@manacore/shared-icons';
|
||||
import type { Snippet, Component } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
widthPx: number;
|
||||
maximized?: boolean;
|
||||
onClose: () => void;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onResize?: (widthPx: number) => void;
|
||||
// Default header
|
||||
title?: string;
|
||||
color?: string;
|
||||
icon?: Component;
|
||||
// Snippet overrides
|
||||
header_left?: Snippet;
|
||||
badge?: Snippet;
|
||||
toolbar?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
widthPx,
|
||||
maximized = false,
|
||||
onClose,
|
||||
onMinimize,
|
||||
onMaximize,
|
||||
onResize,
|
||||
title = '',
|
||||
color = '#6B7280',
|
||||
icon: IconComponent,
|
||||
header_left,
|
||||
badge,
|
||||
toolbar,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
const MIN_WIDTH = 280;
|
||||
const MAX_WIDTH = 1200;
|
||||
|
||||
let resizing = $state(false);
|
||||
|
||||
function handleResizeStart(startX: number) {
|
||||
if (!onResize) return;
|
||||
const startWidth = widthPx;
|
||||
resizing = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
|
||||
function onMove(clientX: number) {
|
||||
const delta = clientX - startX;
|
||||
const newWidth = Math.round(Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta)));
|
||||
onResize!(newWidth);
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
resizing = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onEnd);
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', onEnd);
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
onMove(e.clientX);
|
||||
}
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
onMove(e.touches[0].clientX);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onEnd);
|
||||
window.addEventListener('touchmove', onTouchMove);
|
||||
window.addEventListener('touchend', onEnd);
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
handleResizeStart(e.clientX);
|
||||
}
|
||||
|
||||
function onTouchStartHandle(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
handleResizeStart(e.touches[0].clientX);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="page-shell"
|
||||
class:maximized
|
||||
class:resizing
|
||||
style="width: {maximized ? '100%' : `${widthPx}px`}"
|
||||
>
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle"><DotsSixVertical size={14} /></span>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-header" ondragstart={(e) => e.preventDefault()}>
|
||||
<div class="header-left">
|
||||
{#if header_left}
|
||||
{@render header_left()}
|
||||
{:else}
|
||||
{#if IconComponent}
|
||||
<span class="header-icon" style="color: {color}">
|
||||
<IconComponent size={16} weight="fill" />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="color-dot" style="background-color: {color}"></span>
|
||||
{/if}
|
||||
<span class="page-title">{title}</span>
|
||||
{/if}
|
||||
{#if badge}
|
||||
{@render badge()}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{#if onMinimize}
|
||||
<button class="header-btn" onclick={onMinimize} title="Minimieren">
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMaximize}
|
||||
<button
|
||||
class="header-btn"
|
||||
onclick={onMaximize}
|
||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||
>
|
||||
{#if maximized}<CornersIn size={14} />{:else}<CornersOut size={14} />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="header-btn" onclick={onClose} title={$_('common.close')}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Optional toolbar (e.g. PageEditBar) -->
|
||||
{#if toolbar}
|
||||
{@render toolbar()}
|
||||
{/if}
|
||||
|
||||
<!-- Body -->
|
||||
<div class="page-body">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<!-- Resize handle -->
|
||||
{#if onResize && !maximized}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="resize-handle" onmousedown={onMouseDown} ontouchstart={onTouchStartHandle}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="9" y1="5" x2="5" y2="9" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-shell {
|
||||
flex: 0 0 auto;
|
||||
min-height: 60vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeIn 0.25s ease-out;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
:global(.dark) .page-shell {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.page-shell.resizing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.12),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
:global(.dark) .page-shell.resizing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.2),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
.page-shell.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
width: 100% !important;
|
||||
min-height: 100vh;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
}
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 14px;
|
||||
color: #d1d5db;
|
||||
cursor: grab;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:global(.dark) .drag-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .drag-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.header-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.color-dot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .page-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
.header-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.header-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
.page-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ew-resize;
|
||||
color: #d1d5db;
|
||||
transition: color 0.15s;
|
||||
border-radius: 0.25rem 0 0.375rem 0;
|
||||
touch-action: none;
|
||||
}
|
||||
.resize-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .resize-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .resize-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { default as PageShell } from './PageShell.svelte';
|
||||
export { default as PageCarousel } from './PageCarousel.svelte';
|
||||
export type { CarouselPage } from './PageCarousel.svelte';
|
||||
|
|
@ -1,17 +1,9 @@
|
|||
<!--
|
||||
AppPage — Paper-sheet wrapper for any app in the workbench carousel.
|
||||
Lazy-loads the app's AppView component and renders it inside the page shell.
|
||||
AppPage — Workbench app card. Lazy-loads the app's component inside a PageShell.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import {
|
||||
X,
|
||||
Minus,
|
||||
DotsSixVertical,
|
||||
CornersOut,
|
||||
CornersIn,
|
||||
SpinnerGap,
|
||||
} from '@manacore/shared-icons';
|
||||
import { SpinnerGap } from '@manacore/shared-icons';
|
||||
import { PageShell } from '$lib/components/page-carousel';
|
||||
import { getAppEntry } from './app-registry';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
|
|
@ -35,9 +27,6 @@
|
|||
onResize,
|
||||
}: Props = $props();
|
||||
|
||||
const MIN_WIDTH = 280;
|
||||
const MAX_WIDTH = 1200;
|
||||
|
||||
let appEntry = $derived(getAppEntry(appId));
|
||||
let appName = $derived(appEntry?.name ?? appId);
|
||||
let appColor = $derived(appEntry?.color ?? '#6B7280');
|
||||
|
|
@ -45,7 +34,6 @@
|
|||
// Lazy-load app component
|
||||
let AppComponent = $state<Component | null>(null);
|
||||
let loadError = $state(false);
|
||||
let resizing = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
AppComponent = null;
|
||||
|
|
@ -57,274 +45,32 @@
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
function handleResizeStart(startX: number) {
|
||||
if (!onResize) return;
|
||||
const startWidth = widthPx;
|
||||
resizing = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
|
||||
function onMove(clientX: number) {
|
||||
const delta = clientX - startX;
|
||||
const newWidth = Math.round(Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta)));
|
||||
onResize!(newWidth);
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
resizing = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onEnd);
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', onEnd);
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
onMove(e.clientX);
|
||||
}
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
onMove(e.touches[0].clientX);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onEnd);
|
||||
window.addEventListener('touchmove', onTouchMove);
|
||||
window.addEventListener('touchend', onEnd);
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
handleResizeStart(e.clientX);
|
||||
}
|
||||
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
handleResizeStart(e.touches[0].clientX);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="app-page"
|
||||
class:maximized
|
||||
class:resizing
|
||||
style="width: {maximized ? '100%' : `${widthPx}px`}"
|
||||
<PageShell
|
||||
{widthPx}
|
||||
{maximized}
|
||||
title={appName}
|
||||
color={appColor}
|
||||
{onClose}
|
||||
{onMinimize}
|
||||
{onMaximize}
|
||||
{onResize}
|
||||
>
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle"><DotsSixVertical size={14} /></span>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<span class="app-dot" style="background-color: {appColor}"></span>
|
||||
<span class="app-name">{appName}</span>
|
||||
{#if loadError}
|
||||
<div class="load-state">
|
||||
<p>App konnte nicht geladen werden</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{#if onMinimize}
|
||||
<button class="header-btn" onclick={onMinimize} title="Minimieren">
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMaximize}
|
||||
<button
|
||||
class="header-btn"
|
||||
onclick={onMaximize}
|
||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||
>
|
||||
{#if maximized}<CornersIn size={14} />{:else}<CornersOut size={14} />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="header-btn" onclick={onClose} title={$_('common.close')}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App content -->
|
||||
<div class="page-body">
|
||||
{#if loadError}
|
||||
<div class="load-state">
|
||||
<p>App konnte nicht geladen werden</p>
|
||||
</div>
|
||||
{:else if AppComponent}
|
||||
<AppComponent />
|
||||
{:else}
|
||||
<div class="load-state">
|
||||
<SpinnerGap size={24} class="spinner" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Resize handle -->
|
||||
{#if onResize && !maximized}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
onmousedown={onMouseDown}
|
||||
ontouchstart={onTouchStart}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="9" y1="5" x2="5" y2="9" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{:else if AppComponent}
|
||||
<AppComponent />
|
||||
{:else}
|
||||
<div class="load-state">
|
||||
<SpinnerGap size={24} class="spinner" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PageShell>
|
||||
|
||||
<style>
|
||||
.app-page {
|
||||
flex: 0 0 auto;
|
||||
min-height: 60vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeIn 0.25s ease-out;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
:global(.dark) .app-page {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.app-page.resizing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.12),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
:global(.dark) .app-page.resizing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.2),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
.app-page.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
width: 100% !important;
|
||||
min-height: 100vh;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
}
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 14px;
|
||||
color: #d1d5db;
|
||||
cursor: grab;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:global(.dark) .drag-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .drag-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.app-dot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.app-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .app-name {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
.header-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.header-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
.page-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.load-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -347,30 +93,4 @@
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ew-resize;
|
||||
color: #d1d5db;
|
||||
transition: color 0.15s;
|
||||
border-radius: 0.25rem 0 0.375rem 0;
|
||||
touch-action: none;
|
||||
}
|
||||
.resize-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .resize-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .resize-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@
|
|||
Fire,
|
||||
Leaf,
|
||||
Heart,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Trash,
|
||||
} from '@manacore/shared-icons';
|
||||
import type { PageIcon, PageConfig } from '../../stores/settings.svelte';
|
||||
|
|
@ -19,14 +17,10 @@
|
|||
interface Props {
|
||||
config: PageConfig;
|
||||
onUpdate: (data: Partial<PageConfig>) => void;
|
||||
onMoveLeft?: () => void;
|
||||
onMoveRight?: () => void;
|
||||
onDelete: () => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
let { config, onUpdate, onMoveLeft, onMoveRight, onDelete, isFirst, isLast }: Props = $props();
|
||||
let { config, onUpdate, onDelete }: Props = $props();
|
||||
|
||||
const ICONS: { id: PageIcon; component: typeof Warning }[] = [
|
||||
{ id: 'warning', component: Warning },
|
||||
|
|
@ -148,18 +142,6 @@
|
|||
{/if}
|
||||
|
||||
<div class="edit-row actions-row">
|
||||
<div class="move-btns">
|
||||
{#if !isFirst && onMoveLeft}
|
||||
<button class="action-btn" onclick={onMoveLeft} title="Nach links">
|
||||
<ArrowLeft size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isLast && onMoveRight}
|
||||
<button class="action-btn" onclick={onMoveRight} title="Nach rechts">
|
||||
<ArrowRight size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="action-btn delete-btn" onclick={onDelete} title="Seite löschen">
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
|
|
@ -331,11 +313,7 @@
|
|||
color: #9ca3af;
|
||||
}
|
||||
.actions-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.move-btns {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.action-btn {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { isToday, isTomorrow, isPast, startOfDay, addDays, subHours, format } from 'date-fns';
|
||||
import { isToday, isTomorrow, isPast, startOfDay, addDays, subHours } from 'date-fns';
|
||||
import type { Task } from '../../types';
|
||||
import { tasksStore } from '../../stores/tasks.svelte';
|
||||
import { todoSettings } from '../../stores/settings.svelte';
|
||||
import type { PageConfig, PageIcon, PageWidth } from '../../stores/settings.svelte';
|
||||
import type { PageConfig, PageIcon } from '../../stores/settings.svelte';
|
||||
import PageEditBar from './PageEditBar.svelte';
|
||||
import TaskItem from '../TaskItem.svelte';
|
||||
import {
|
||||
X,
|
||||
Circle,
|
||||
Minus,
|
||||
DotsSixVertical,
|
||||
CornersOut,
|
||||
CornersIn,
|
||||
Warning,
|
||||
Calendar,
|
||||
CalendarDots,
|
||||
|
|
@ -24,25 +18,23 @@
|
|||
Leaf,
|
||||
Heart,
|
||||
} from '@manacore/shared-icons';
|
||||
import { PageShell } from '$lib/components/page-carousel';
|
||||
|
||||
interface Props {
|
||||
pageId: string;
|
||||
allTasks: Task[];
|
||||
widthPx: number;
|
||||
title?: string;
|
||||
maximized?: boolean;
|
||||
editMode?: boolean;
|
||||
filterConfig?: PageConfig['filter'];
|
||||
pageIcon?: PageIcon;
|
||||
customPageConfig?: PageConfig;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
onClose: () => void;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onResize?: (widthPx: number) => void;
|
||||
onRename?: (name: string) => void;
|
||||
onUpdateConfig?: (data: Partial<PageConfig>) => void;
|
||||
onMoveLeft?: () => void;
|
||||
onMoveRight?: () => void;
|
||||
onDelete?: () => void;
|
||||
onOpenTask?: (task: Task) => void;
|
||||
}
|
||||
|
|
@ -50,21 +42,18 @@
|
|||
let {
|
||||
pageId,
|
||||
allTasks,
|
||||
widthPx,
|
||||
title: customTitle,
|
||||
maximized = false,
|
||||
editMode = false,
|
||||
filterConfig,
|
||||
pageIcon,
|
||||
customPageConfig,
|
||||
isFirst = false,
|
||||
isLast = false,
|
||||
onClose,
|
||||
onMinimize,
|
||||
onMaximize,
|
||||
onResize,
|
||||
onRename,
|
||||
onUpdateConfig,
|
||||
onMoveLeft,
|
||||
onMoveRight,
|
||||
onDelete,
|
||||
onOpenTask,
|
||||
}: Props = $props();
|
||||
|
|
@ -205,15 +194,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
const PAGE_WIDTH_MAP: Record<string, string> = {
|
||||
narrow: 'min(360px, 85vw)',
|
||||
medium: 'min(480px, 85vw)',
|
||||
wide: 'min(640px, 90vw)',
|
||||
full: 'min(840px, 95vw)',
|
||||
};
|
||||
|
||||
let sheetWidth = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium);
|
||||
|
||||
let showCompleted = $derived(filterConfig?.completed ?? false);
|
||||
let openTasks = $derived(
|
||||
pageId === 'todo' ? filteredTasks.filter((t) => !t.isCompleted) : filteredTasks
|
||||
|
|
@ -243,80 +223,48 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="todo-page"
|
||||
class:maximized
|
||||
class:editing={editMode}
|
||||
style="width: {maximized ? '100%' : sheetWidth}"
|
||||
<PageShell
|
||||
{widthPx}
|
||||
{maximized}
|
||||
color={displayColor}
|
||||
icon={IconComponent}
|
||||
{onClose}
|
||||
{onMinimize}
|
||||
{onMaximize}
|
||||
{onResize}
|
||||
>
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle"><DotsSixVertical size={14} /></span>
|
||||
</div>
|
||||
{#snippet header_left()}
|
||||
{#if IconComponent}
|
||||
<span class="header-icon" style="color: {displayColor}">
|
||||
<IconComponent size={16} weight="fill" />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="color-dot" style="background-color: {displayColor}"></span>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span
|
||||
bind:this={titleEl}
|
||||
class="page-title"
|
||||
contenteditable={!!onRename}
|
||||
oninput={handleTitleInput}
|
||||
onkeydown={handleTitleKeydown}
|
||||
onfocus={() => (isTitleFocused = true)}
|
||||
onblur={() => (isTitleFocused = false)}
|
||||
></span>
|
||||
{/snippet}
|
||||
|
||||
{#if editMode && isCustom && customPageConfig && onUpdateConfig && onDelete}
|
||||
<PageEditBar
|
||||
config={customPageConfig}
|
||||
onUpdate={onUpdateConfig}
|
||||
{onMoveLeft}
|
||||
{onMoveRight}
|
||||
{onDelete}
|
||||
{isFirst}
|
||||
{isLast}
|
||||
/>
|
||||
{/if}
|
||||
{#snippet badge()}
|
||||
<span class="task-count">{filteredTasks.length}</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet toolbar()}
|
||||
{#if isCustom && customPageConfig && onUpdateConfig && onDelete}
|
||||
<PageEditBar config={customPageConfig} onUpdate={onUpdateConfig} {onDelete} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-header" ondragstart={(e) => e.preventDefault()}>
|
||||
<div class="header-left">
|
||||
{#if IconComponent}
|
||||
<span class="header-icon" style="color: {displayColor}">
|
||||
<IconComponent size={16} weight="fill" />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="color-dot" style="background-color: {displayColor}"></span>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span
|
||||
bind:this={titleEl}
|
||||
class="page-title"
|
||||
contenteditable={!!onRename}
|
||||
oninput={handleTitleInput}
|
||||
onkeydown={handleTitleKeydown}
|
||||
onfocus={() => (isTitleFocused = true)}
|
||||
onblur={() => (isTitleFocused = false)}
|
||||
></span>
|
||||
<span class="task-count">{filteredTasks.length}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{#if editMode && !isCustom && onDelete}
|
||||
<button class="header-btn delete-preset" onclick={onDelete} title="Seite entfernen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if !editMode}
|
||||
{#if onMinimize}
|
||||
<button class="header-btn" onclick={onMinimize} title="Minimieren">
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMaximize}
|
||||
<button
|
||||
class="header-btn"
|
||||
onclick={onMaximize}
|
||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||
>
|
||||
{#if maximized}<CornersIn size={14} />{:else}<CornersOut size={14} />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="header-btn" onclick={onClose} title="Seite schließen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-body" ondragstart={(e) => e.preventDefault()}>
|
||||
<div class="page-content" ondragstart={(e) => e.preventDefault()}>
|
||||
{#each openTasks as task (task.id)}
|
||||
<div class="task-card-wrapper" class:completed-task={task.isCompleted}>
|
||||
<TaskItem
|
||||
|
|
@ -344,7 +292,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !editMode && !showCompleted && pageId !== 'completed'}
|
||||
{#if !showCompleted && pageId !== 'completed'}
|
||||
<div class="inline-create">
|
||||
<span class="inline-create-icon"><Circle size={18} /></span>
|
||||
<input
|
||||
|
|
@ -359,116 +307,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</PageShell>
|
||||
|
||||
<style>
|
||||
.todo-page {
|
||||
flex: 0 0 auto;
|
||||
min-height: 60vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .todo-page {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.todo-page.editing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.12),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
:global(.dark) .todo-page.editing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.2),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
.todo-page.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
width: 100% !important;
|
||||
min-height: 100vh;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
align-items: center;
|
||||
}
|
||||
.todo-page.maximized .page-header,
|
||||
.todo-page.maximized .page-body {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
}
|
||||
.todo-page.maximized .page-header,
|
||||
.todo-page.maximized .page-body {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.drag-handle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 14px;
|
||||
color: #d1d5db;
|
||||
cursor: grab;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:global(.dark) .drag-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .drag-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.header-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
|
@ -505,44 +346,8 @@
|
|||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
.header-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.header-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.delete-preset:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
:global(.dark) .delete-preset:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
.page-body {
|
||||
flex: 1;
|
||||
.page-content {
|
||||
padding: 0.75rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.task-card-wrapper {
|
||||
margin-bottom: 0.5rem;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import type { TaskPriority } from '../types';
|
|||
export type TodoView = 'inbox' | 'today' | 'upcoming' | 'kanban' | 'completed';
|
||||
export type KanbanCardSize = 'compact' | 'normal' | 'large';
|
||||
export type LayoutMode = 'fokus' | 'uebersicht' | 'matrix';
|
||||
export type PageWidth = 'narrow' | 'medium' | 'wide' | 'full';
|
||||
|
||||
export type PageIcon =
|
||||
| 'warning'
|
||||
|
|
@ -78,9 +77,6 @@ export interface TodoAppSettings extends Record<string, unknown> {
|
|||
// View layout
|
||||
activeLayoutMode: LayoutMode;
|
||||
|
||||
// Page width
|
||||
pageWidth: PageWidth;
|
||||
|
||||
// Custom pages
|
||||
customPages: PageConfig[];
|
||||
}
|
||||
|
|
@ -116,7 +112,6 @@ const DEFAULT_SETTINGS: TodoAppSettings = {
|
|||
immersiveModeEnabled: false,
|
||||
filterStripCollapsed: false,
|
||||
activeLayoutMode: 'fokus' as LayoutMode,
|
||||
pageWidth: 'medium' as PageWidth,
|
||||
customPages: [] as PageConfig[],
|
||||
};
|
||||
|
||||
|
|
@ -166,9 +161,6 @@ export const todoSettings = {
|
|||
get focusMode() {
|
||||
return baseStore.settings.focusMode;
|
||||
},
|
||||
get pageWidth() {
|
||||
return baseStore.settings.pageWidth;
|
||||
},
|
||||
get activeLayoutMode() {
|
||||
return baseStore.settings.activeLayoutMode;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, X, ArrowsOut } from '@manacore/shared-icons';
|
||||
import AppPage from '$lib/components/workbench/AppPage.svelte';
|
||||
import AppPagePicker from '$lib/components/workbench/AppPagePicker.svelte';
|
||||
import { getAppEntry, APP_REGISTRY } from '$lib/components/workbench/app-registry';
|
||||
import { PageCarousel, type CarouselPage } from '$lib/components/page-carousel';
|
||||
import { getAppEntry } from '$lib/components/workbench/app-registry';
|
||||
import { createAppSettingsStore } from '@manacore/shared-stores';
|
||||
|
||||
// ── Persisted workbench state ───────────────────────────
|
||||
|
|
@ -21,7 +20,6 @@
|
|||
],
|
||||
});
|
||||
|
||||
// Local reactive state synced from persisted store
|
||||
let openApps = $state<
|
||||
{ appId: string; minimized: boolean; maximized?: boolean; widthPx?: number }[]
|
||||
>([
|
||||
|
|
@ -30,13 +28,11 @@
|
|||
{ appId: 'contacts', minimized: false },
|
||||
]);
|
||||
|
||||
// Initialize from persisted settings
|
||||
$effect(() => {
|
||||
const s = workbenchStore.settings;
|
||||
if (s.openApps?.length) openApps = [...s.openApps];
|
||||
});
|
||||
|
||||
// Persist changes (debounced via store)
|
||||
function persistState() {
|
||||
workbenchStore.update({
|
||||
openApps: openApps.map((a) => ({
|
||||
|
|
@ -48,7 +44,20 @@
|
|||
});
|
||||
}
|
||||
|
||||
let expandedApps = $derived(openApps.filter((a) => !a.minimized));
|
||||
// ── Map to CarouselPage[] ───────────────────────────────
|
||||
let carouselPages = $derived<CarouselPage[]>(
|
||||
openApps.map((a) => {
|
||||
const entry = getAppEntry(a.appId);
|
||||
return {
|
||||
id: a.appId,
|
||||
minimized: a.minimized,
|
||||
maximized: a.maximized,
|
||||
widthPx: a.widthPx ?? DEFAULT_WIDTH,
|
||||
title: entry?.name ?? a.appId,
|
||||
color: entry?.color ?? '#6B7280',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
let showPicker = $state(false);
|
||||
|
||||
|
|
@ -63,89 +72,43 @@
|
|||
persistState();
|
||||
}
|
||||
|
||||
function handleRemoveApp(appId: string) {
|
||||
openApps = openApps.filter((a) => a.appId !== appId);
|
||||
function handleRemoveApp(id: string) {
|
||||
openApps = openApps.filter((a) => a.appId !== id);
|
||||
persistState();
|
||||
}
|
||||
|
||||
function handleMinimizeApp(appId: string) {
|
||||
openApps = openApps.map((a) => (a.appId === appId ? { ...a, minimized: true } : a));
|
||||
function handleMinimizeApp(id: string) {
|
||||
openApps = openApps.map((a) => (a.appId === id ? { ...a, minimized: true } : a));
|
||||
persistState();
|
||||
}
|
||||
|
||||
function handleRestoreApp(appId: string) {
|
||||
openApps = openApps.map((a) => (a.appId === appId ? { ...a, minimized: false } : a));
|
||||
function handleRestoreApp(id: string) {
|
||||
openApps = openApps.map((a) => (a.appId === id ? { ...a, minimized: false } : a));
|
||||
persistState();
|
||||
}
|
||||
|
||||
function handleMaximizeApp(appId: string) {
|
||||
function handleMaximizeApp(id: string) {
|
||||
openApps = openApps.map((a) =>
|
||||
a.appId === appId ? { ...a, maximized: !a.maximized, minimized: false } : a
|
||||
a.appId === id ? { ...a, maximized: !a.maximized, minimized: false } : a
|
||||
);
|
||||
persistState();
|
||||
}
|
||||
|
||||
function handleResize(appId: string, widthPx: number) {
|
||||
openApps = openApps.map((a) => (a.appId === appId ? { ...a, widthPx } : a));
|
||||
function handleResize(id: string, widthPx: number) {
|
||||
openApps = openApps.map((a) => (a.appId === id ? { ...a, widthPx } : a));
|
||||
persistState();
|
||||
}
|
||||
|
||||
// ── Drag reorder ────────────────────────────────────────
|
||||
let dragAppId = $state<string | null>(null);
|
||||
|
||||
function handleDragStart(e: DragEvent, appId: string) {
|
||||
dragAppId = appId;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', appId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
if (!dragAppId) return;
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent, targetAppId: string) {
|
||||
e.preventDefault();
|
||||
if (!dragAppId || dragAppId === targetAppId) return;
|
||||
const fromIdx = openApps.findIndex((a) => a.appId === dragAppId);
|
||||
const toIdx = openApps.findIndex((a) => a.appId === targetAppId);
|
||||
function handleReorder(fromId: string, toId: string) {
|
||||
const fromIdx = openApps.findIndex((a) => a.appId === fromId);
|
||||
const toIdx = openApps.findIndex((a) => a.appId === toId);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
const apps = [...openApps];
|
||||
const [moved] = apps.splice(fromIdx, 1);
|
||||
apps.splice(toIdx, 0, moved);
|
||||
openApps = apps;
|
||||
dragAppId = null;
|
||||
persistState();
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
dragAppId = null;
|
||||
}
|
||||
|
||||
// ── Minimized tabs ──────────────────────────────────────
|
||||
let minimizedApps = $derived(
|
||||
openApps
|
||||
.filter((a) => a.minimized)
|
||||
.map((a) => {
|
||||
const entry = getAppEntry(a.appId);
|
||||
return {
|
||||
appId: a.appId,
|
||||
name: entry?.name ?? a.appId,
|
||||
color: entry?.color ?? '#6B7280',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
let pickerEl = $state<HTMLDivElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (showPicker && pickerEl) {
|
||||
pickerEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -153,92 +116,36 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="workbench">
|
||||
<!-- App carousel -->
|
||||
<div class="fokus-track" style="--sheet-width: {DEFAULT_WIDTH}px">
|
||||
{#each expandedApps as app (app.appId)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="page-drag-wrapper"
|
||||
class:dragging={dragAppId === app.appId}
|
||||
draggable={true}
|
||||
ondragstart={(e) => handleDragStart(e, app.appId)}
|
||||
ondragover={handleDragOver}
|
||||
ondrop={(e) => handleDrop(e, app.appId)}
|
||||
ondragend={handleDragEnd}
|
||||
>
|
||||
<AppPage
|
||||
appId={app.appId}
|
||||
widthPx={app.widthPx ?? DEFAULT_WIDTH}
|
||||
maximized={app.maximized}
|
||||
onClose={() => handleRemoveApp(app.appId)}
|
||||
onMinimize={() => handleMinimizeApp(app.appId)}
|
||||
onMaximize={() => handleMaximizeApp(app.appId)}
|
||||
onResize={(w) => handleResize(app.appId, w)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Picker / add button -->
|
||||
{#if expandedApps.length === 0}
|
||||
<div class="empty-wrapper">
|
||||
{#if showPicker}
|
||||
<AppPagePicker
|
||||
onSelect={handleAddApp}
|
||||
onClose={() => (showPicker = false)}
|
||||
activeAppIds={openApps.map((a) => a.appId)}
|
||||
/>
|
||||
{:else}
|
||||
<button class="add-card alone" onclick={() => (showPicker = true)}>
|
||||
<Plus size={24} />
|
||||
<span class="add-label">App hinzufügen</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if showPicker}
|
||||
<div bind:this={pickerEl}>
|
||||
<AppPagePicker
|
||||
onSelect={handleAddApp}
|
||||
onClose={() => (showPicker = false)}
|
||||
activeAppIds={openApps.map((a) => a.appId)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="add-card" onclick={() => (showPicker = true)} title="App hinzufügen">
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Minimized tabs -->
|
||||
{#if minimizedApps.length > 0}
|
||||
<div class="minimized-tabs">
|
||||
{#each minimizedApps as app (app.appId)}
|
||||
<div class="minimized-tab">
|
||||
<span class="tab-dot" style="background-color: {app.color}"></span>
|
||||
<button class="tab-title" onclick={() => handleRestoreApp(app.appId)}>
|
||||
{app.name}
|
||||
</button>
|
||||
<button
|
||||
class="tab-maximize"
|
||||
onclick={() => handleMaximizeApp(app.appId)}
|
||||
title="Maximieren"
|
||||
>
|
||||
<ArrowsOut size={12} />
|
||||
</button>
|
||||
<button
|
||||
class="tab-close"
|
||||
onclick={() => handleRemoveApp(app.appId)}
|
||||
title={$_('common.close')}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="tab-add" onclick={() => (showPicker = true)} title="App hinzufügen">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<PageCarousel
|
||||
pages={carouselPages}
|
||||
defaultWidth={DEFAULT_WIDTH}
|
||||
{showPicker}
|
||||
onReorder={handleReorder}
|
||||
onRestore={handleRestoreApp}
|
||||
onMaximize={handleMaximizeApp}
|
||||
onRemove={handleRemoveApp}
|
||||
onTogglePicker={() => (showPicker = !showPicker)}
|
||||
addLabel="App hinzufügen"
|
||||
>
|
||||
{#snippet page(p)}
|
||||
<AppPage
|
||||
appId={p.id}
|
||||
widthPx={p.widthPx}
|
||||
maximized={p.maximized}
|
||||
onClose={() => handleRemoveApp(p.id)}
|
||||
onMinimize={() => handleMinimizeApp(p.id)}
|
||||
onMaximize={() => handleMaximizeApp(p.id)}
|
||||
onResize={(w) => handleResize(p.id, w)}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet picker()}
|
||||
<AppPagePicker
|
||||
onSelect={handleAddApp}
|
||||
onClose={() => (showPicker = false)}
|
||||
activeAppIds={openApps.map((a) => a.appId)}
|
||||
/>
|
||||
{/snippet}
|
||||
</PageCarousel>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -248,195 +155,4 @@
|
|||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Carousel */
|
||||
.fokus-track {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 1rem calc(50% - var(--sheet-width) / 2);
|
||||
scrollbar-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
.fokus-track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-drag-wrapper {
|
||||
flex: 0 0 auto;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.page-drag-wrapper.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
.add-card {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
border: 2px dashed rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.empty-wrapper {
|
||||
flex: 0 0 auto;
|
||||
width: var(--sheet-width, min(480px, 85vw));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
.add-card.alone {
|
||||
width: 100%;
|
||||
min-height: 60vh;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.add-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 4%, transparent);
|
||||
}
|
||||
:global(.dark) .add-card {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .add-card.alone {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .add-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent);
|
||||
}
|
||||
.add-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Minimized tabs */
|
||||
.minimized-tabs {
|
||||
position: fixed;
|
||||
bottom: 4.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 45;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
background: #fffef5;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
animation: slideUp 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .minimized-tabs {
|
||||
background: #252220;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
.minimized-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.minimized-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .minimized-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.tab-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab-title {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #374151;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
}
|
||||
.tab-title:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
:global(.dark) .tab-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
:global(.dark) .tab-title:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
.tab-maximize,
|
||||
.tab-close {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0.125rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.minimized-tab:hover .tab-maximize,
|
||||
.minimized-tab:hover .tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
.tab-maximize:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
.tab-close:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.tab-add {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.15s;
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
.tab-add:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,18 +2,9 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import type { Observable } from 'dexie';
|
||||
import { dropTarget } from '@manacore/shared-ui/dnd';
|
||||
import type { DragPayload, TagDragData } from '@manacore/shared-ui/dnd';
|
||||
import { useAllTags } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
type Task,
|
||||
type LocalLabel,
|
||||
type LocalBoardView,
|
||||
type LocalTodoProject,
|
||||
tasksStore,
|
||||
taskTable,
|
||||
} from '$lib/modules/todo';
|
||||
import { Plus, PencilSimple, X, Gear, ArrowsOut } from '@manacore/shared-icons';
|
||||
import type { DragPayload } from '@manacore/shared-ui/dnd';
|
||||
import { type Task, type LocalLabel, tasksStore, taskTable } from '$lib/modules/todo';
|
||||
import { Gear } from '@manacore/shared-icons';
|
||||
import { ShareModal } from '@manacore/shared-uload';
|
||||
|
||||
// Components
|
||||
|
|
@ -25,8 +16,9 @@
|
|||
import TodoPage from '$lib/modules/todo/components/pages/TodoPage.svelte';
|
||||
import PagePicker from '$lib/modules/todo/components/pages/PagePicker.svelte';
|
||||
import { todoSettings } from '$lib/modules/todo/stores/settings.svelte';
|
||||
import type { PageConfig, PageWidth } from '$lib/modules/todo/stores/settings.svelte';
|
||||
import type { PageConfig } from '$lib/modules/todo/stores/settings.svelte';
|
||||
import { getTaskStats } from '$lib/modules/todo';
|
||||
import { PageCarousel, type CarouselPage } from '$lib/components/page-carousel';
|
||||
|
||||
// Get data from layout context
|
||||
const allTasks$: Observable<Task[]> = getContext('tasks');
|
||||
|
|
@ -91,19 +83,21 @@
|
|||
return () => tagDropCtx?.clear();
|
||||
});
|
||||
|
||||
// ── Edit mode ──────────────────────────────────────────
|
||||
let editMode = $state(false);
|
||||
|
||||
// ── Pages ───────────────────────────────────────────────
|
||||
const DEFAULT_WIDTH = 480;
|
||||
let showPagePicker = $state(false);
|
||||
let openPages = $state<
|
||||
{ id: string; minimized: boolean; maximized?: boolean; customTitle?: string }[]
|
||||
{
|
||||
id: string;
|
||||
minimized: boolean;
|
||||
maximized?: boolean;
|
||||
widthPx?: number;
|
||||
customTitle?: string;
|
||||
}[]
|
||||
>([{ id: 'todo', minimized: false }]);
|
||||
|
||||
let expandedPages = $derived(openPages.filter((p) => !p.minimized));
|
||||
let customPages = $derived(todoSettings.customPages);
|
||||
|
||||
// Minimized pages for tab bar
|
||||
const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
todo: { title: 'To Do', color: '#6B7280' },
|
||||
completed: { title: 'Erledigt', color: '#22C55E' },
|
||||
|
|
@ -128,18 +122,24 @@
|
|||
heart: '#EC4899',
|
||||
};
|
||||
|
||||
let minimizedPages = $derived(
|
||||
openPages
|
||||
.filter((p) => p.minimized)
|
||||
.map((p) => {
|
||||
const config = getCustomPageConfig(p.id);
|
||||
const preset = PAGE_META[p.id];
|
||||
const title = p.customTitle ?? config?.label ?? preset?.title ?? p.id;
|
||||
const color = config?.icon
|
||||
? (ICON_COLORS[config.icon] ?? '#8B5CF6')
|
||||
: (preset?.color ?? '#6B7280');
|
||||
return { id: p.id, title, color };
|
||||
})
|
||||
// Map to CarouselPage[]
|
||||
let carouselPages = $derived<CarouselPage[]>(
|
||||
openPages.map((p) => {
|
||||
const config = getCustomPageConfig(p.id);
|
||||
const preset = PAGE_META[p.id];
|
||||
const title = p.customTitle ?? config?.label ?? preset?.title ?? p.id;
|
||||
const color = config?.icon
|
||||
? (ICON_COLORS[config.icon] ?? '#8B5CF6')
|
||||
: (preset?.color ?? '#6B7280');
|
||||
return {
|
||||
id: p.id,
|
||||
minimized: p.minimized,
|
||||
maximized: p.maximized,
|
||||
widthPx: p.widthPx ?? DEFAULT_WIDTH,
|
||||
title,
|
||||
color,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
function handleAddPage(pageId: string) {
|
||||
|
|
@ -151,24 +151,28 @@
|
|||
showPagePicker = false;
|
||||
}
|
||||
|
||||
function handleRemovePage(pageId: string) {
|
||||
openPages = openPages.filter((p) => p.id !== pageId);
|
||||
function handleRemovePage(id: string) {
|
||||
openPages = openPages.filter((p) => p.id !== id);
|
||||
}
|
||||
|
||||
function handleMinimizePage(pageId: string) {
|
||||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: true } : p));
|
||||
function handleMinimizePage(id: string) {
|
||||
openPages = openPages.map((p) => (p.id === id ? { ...p, minimized: true } : p));
|
||||
}
|
||||
|
||||
function handleRestorePage(pageId: string) {
|
||||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p));
|
||||
function handleRestorePage(id: string) {
|
||||
openPages = openPages.map((p) => (p.id === id ? { ...p, minimized: false } : p));
|
||||
}
|
||||
|
||||
function handleMaximizePage(pageId: string) {
|
||||
function handleMaximizePage(id: string) {
|
||||
openPages = openPages.map((p) =>
|
||||
p.id === pageId ? { ...p, maximized: !p.maximized, minimized: false } : p
|
||||
p.id === id ? { ...p, maximized: !p.maximized, minimized: false } : p
|
||||
);
|
||||
}
|
||||
|
||||
function handleResize(id: string, widthPx: number) {
|
||||
openPages = openPages.map((p) => (p.id === id ? { ...p, widthPx } : p));
|
||||
}
|
||||
|
||||
function handleRenamePage(pageId: string, name: string) {
|
||||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, customTitle: name } : p));
|
||||
if (pageId.startsWith('custom-')) {
|
||||
|
|
@ -177,6 +181,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
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 ────────────────────────────────────
|
||||
function handleCreateCustomPage() {
|
||||
const id = `custom-${crypto.randomUUID().slice(0, 8)}`;
|
||||
|
|
@ -184,7 +198,6 @@
|
|||
todoSettings.update({ customPages: [...customPages, newPage] });
|
||||
openPages = [...openPages, { id, minimized: false }];
|
||||
showPagePicker = false;
|
||||
editMode = true;
|
||||
}
|
||||
|
||||
function handleUpdateCustomPage(pageId: string, data: Partial<PageConfig>) {
|
||||
|
|
@ -205,95 +218,6 @@
|
|||
function getCustomPageConfig(pageId: string): PageConfig | undefined {
|
||||
return customPages.find((cp) => cp.id === pageId);
|
||||
}
|
||||
|
||||
// ── Page reorder ────────────────────────────────────────
|
||||
function handleMovePageLeft(pageId: string) {
|
||||
const idx = openPages.findIndex((p) => p.id === pageId);
|
||||
if (idx <= 0) return;
|
||||
const pages = [...openPages];
|
||||
[pages[idx - 1], pages[idx]] = [pages[idx], pages[idx - 1]];
|
||||
openPages = pages;
|
||||
}
|
||||
|
||||
function handleMovePageRight(pageId: string) {
|
||||
const idx = openPages.findIndex((p) => p.id === pageId);
|
||||
if (idx === -1 || idx >= openPages.length - 1) return;
|
||||
const pages = [...openPages];
|
||||
[pages[idx], pages[idx + 1]] = [pages[idx + 1], pages[idx]];
|
||||
openPages = pages;
|
||||
}
|
||||
|
||||
// ── Page drag reorder ───────────────────────────────────
|
||||
let dragPageId = $state<string | null>(null);
|
||||
|
||||
function handlePageDragStart(e: DragEvent, pageId: string) {
|
||||
dragPageId = pageId;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', pageId);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageDragOver(e: DragEvent) {
|
||||
if (!dragPageId) return;
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
|
||||
function handlePageDrop(e: DragEvent, targetPageId: string) {
|
||||
e.preventDefault();
|
||||
if (!dragPageId || dragPageId === targetPageId) return;
|
||||
const fromIdx = openPages.findIndex((p) => p.id === dragPageId);
|
||||
const toIdx = openPages.findIndex((p) => p.id === targetPageId);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
const pages = [...openPages];
|
||||
const [moved] = pages.splice(fromIdx, 1);
|
||||
pages.splice(toIdx, 0, moved);
|
||||
openPages = pages;
|
||||
dragPageId = null;
|
||||
}
|
||||
|
||||
function handlePageDragEnd() {
|
||||
dragPageId = null;
|
||||
}
|
||||
|
||||
function togglePagePicker() {
|
||||
showPagePicker = !showPagePicker;
|
||||
}
|
||||
|
||||
function toggleEditMode() {
|
||||
editMode = !editMode;
|
||||
if (!editMode) showPagePicker = false;
|
||||
}
|
||||
|
||||
// ── Width pills ─────────────────────────────────────────
|
||||
const WIDTH_OPTIONS: { id: PageWidth; label: string }[] = [
|
||||
{ id: 'narrow', label: 'S' },
|
||||
{ id: 'medium', label: 'M' },
|
||||
{ id: 'wide', label: 'L' },
|
||||
{ id: 'full', label: 'XL' },
|
||||
];
|
||||
|
||||
function setPageWidth(width: PageWidth) {
|
||||
todoSettings.update({ pageWidth: width });
|
||||
}
|
||||
|
||||
const PAGE_WIDTH_MAP: Record<string, string> = {
|
||||
narrow: 'min(360px, 85vw)',
|
||||
medium: 'min(480px, 85vw)',
|
||||
wide: 'min(640px, 90vw)',
|
||||
full: 'min(840px, 95vw)',
|
||||
};
|
||||
|
||||
let sheetWidthVar = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium);
|
||||
|
||||
let pagePickerEl = $state<HTMLDivElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (showPagePicker && pagePickerEl) {
|
||||
pagePickerEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -328,143 +252,49 @@
|
|||
<QuickAddTask labels={allLabels} onShowSyntaxHelp={() => (showSyntaxHelp = true)} />
|
||||
</div>
|
||||
|
||||
<!-- Width pills (visible in edit mode) -->
|
||||
{#if editMode}
|
||||
<div class="edit-toolbar">
|
||||
<div class="width-pills">
|
||||
{#each WIDTH_OPTIONS as opt (opt.id)}
|
||||
<button
|
||||
class="width-pill"
|
||||
class:active={todoSettings.pageWidth === opt.id}
|
||||
onclick={() => setPageWidth(opt.id)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Pages carousel -->
|
||||
<div class="fokus-track" style="--sheet-width: {sheetWidthVar}">
|
||||
{#each expandedPages as page, pageIdx (page.id)}
|
||||
{@const config = getCustomPageConfig(page.id)}
|
||||
{@const isCustom = page.id.startsWith('custom-')}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="page-drag-wrapper"
|
||||
class:dragging={dragPageId === page.id}
|
||||
draggable={!editMode}
|
||||
ondragstart={(e) => handlePageDragStart(e, page.id)}
|
||||
ondragover={handlePageDragOver}
|
||||
ondrop={(e) => handlePageDrop(e, page.id)}
|
||||
ondragend={handlePageDragEnd}
|
||||
>
|
||||
<TodoPage
|
||||
pageId={page.id}
|
||||
{allTasks}
|
||||
title={page.customTitle ?? config?.label}
|
||||
maximized={page.maximized}
|
||||
{editMode}
|
||||
filterConfig={isCustom ? config?.filter : undefined}
|
||||
pageIcon={isCustom ? config?.icon : undefined}
|
||||
customPageConfig={isCustom ? config : undefined}
|
||||
isFirst={pageIdx === 0}
|
||||
isLast={pageIdx === expandedPages.length - 1}
|
||||
onClose={() => handleRemovePage(page.id)}
|
||||
onMinimize={() => handleMinimizePage(page.id)}
|
||||
onMaximize={() => handleMaximizePage(page.id)}
|
||||
onRename={(name) => handleRenamePage(page.id, name)}
|
||||
onUpdateConfig={isCustom ? (data) => handleUpdateCustomPage(page.id, data) : undefined}
|
||||
onMoveLeft={editMode ? () => handleMovePageLeft(page.id) : undefined}
|
||||
onMoveRight={editMode ? () => handleMovePageRight(page.id) : undefined}
|
||||
onDelete={editMode ? () => handleDeletePage(page.id) : undefined}
|
||||
onOpenTask={(task) => (editTask = task)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Page picker / add button -->
|
||||
{#if expandedPages.length === 0}
|
||||
<div class="empty-pages-wrapper">
|
||||
{#if showPagePicker}
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
onCreateCustom={handleCreateCustomPage}
|
||||
activePageIds={openPages.map((p) => p.id)}
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
class="neue-seite-card alone"
|
||||
onclick={togglePagePicker}
|
||||
title="Neue Seite hinzufügen"
|
||||
>
|
||||
<Plus size={24} />
|
||||
<span class="neue-seite-label">Seite hinzufügen</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if showPagePicker}
|
||||
<div bind:this={pagePickerEl}>
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
onCreateCustom={handleCreateCustomPage}
|
||||
activePageIds={openPages.map((p) => p.id)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="neue-seite-card" onclick={togglePagePicker} title="Neue Seite hinzufügen">
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Minimized tabs bar -->
|
||||
{#if minimizedPages.length > 0}
|
||||
<div class="minimized-tabs">
|
||||
{#each minimizedPages as page (page.id)}
|
||||
<div class="minimized-tab group">
|
||||
<span class="tab-dot" style="background-color: {page.color}"></span>
|
||||
<button class="tab-title" onclick={() => handleRestorePage(page.id)}>
|
||||
{page.title}
|
||||
</button>
|
||||
<button
|
||||
class="tab-maximize"
|
||||
onclick={() => handleMaximizePage(page.id)}
|
||||
title="Maximieren"
|
||||
>
|
||||
<ArrowsOut size={12} />
|
||||
</button>
|
||||
<button
|
||||
class="tab-close"
|
||||
onclick={() => handleRemovePage(page.id)}
|
||||
title={$_('common.close')}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="tab-add" onclick={togglePagePicker} title="Seite hinzufügen">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Edit FAB -->
|
||||
<button
|
||||
class="edit-fab"
|
||||
class:active={editMode}
|
||||
onclick={toggleEditMode}
|
||||
title={editMode ? 'Bearbeitung beenden' : 'Seiten bearbeiten'}
|
||||
<PageCarousel
|
||||
pages={carouselPages}
|
||||
defaultWidth={DEFAULT_WIDTH}
|
||||
showPicker={showPagePicker}
|
||||
onReorder={handleReorder}
|
||||
onRestore={handleRestorePage}
|
||||
onMaximize={handleMaximizePage}
|
||||
onRemove={handleRemovePage}
|
||||
onTogglePicker={() => (showPagePicker = !showPagePicker)}
|
||||
addLabel="Seite hinzufügen"
|
||||
>
|
||||
{#if editMode}
|
||||
<X size={20} />
|
||||
{:else}
|
||||
<PencilSimple size={20} />
|
||||
{/if}
|
||||
</button>
|
||||
{#snippet page(p)}
|
||||
{@const config = getCustomPageConfig(p.id)}
|
||||
{@const isCustom = p.id.startsWith('custom-')}
|
||||
<TodoPage
|
||||
pageId={p.id}
|
||||
{allTasks}
|
||||
widthPx={p.widthPx}
|
||||
title={openPages.find((op) => op.id === p.id)?.customTitle ?? config?.label}
|
||||
maximized={p.maximized}
|
||||
filterConfig={isCustom ? config?.filter : undefined}
|
||||
pageIcon={isCustom ? config?.icon : undefined}
|
||||
customPageConfig={isCustom ? config : undefined}
|
||||
onClose={() => handleRemovePage(p.id)}
|
||||
onMinimize={() => handleMinimizePage(p.id)}
|
||||
onMaximize={() => handleMaximizePage(p.id)}
|
||||
onResize={(w) => handleResize(p.id, w)}
|
||||
onRename={(name) => handleRenamePage(p.id, name)}
|
||||
onUpdateConfig={isCustom ? (data) => handleUpdateCustomPage(p.id, data) : undefined}
|
||||
onDelete={() => handleDeletePage(p.id)}
|
||||
onOpenTask={(task) => (editTask = task)}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet picker()}
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
onCreateCustom={handleCreateCustomPage}
|
||||
activePageIds={openPages.map((p) => p.id)}
|
||||
/>
|
||||
{/snippet}
|
||||
</PageCarousel>
|
||||
</div>
|
||||
|
||||
<!-- Task Edit Modal -->
|
||||
|
|
@ -543,317 +373,6 @@
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Edit toolbar */
|
||||
.edit-toolbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
animation: fadeDown 0.2s ease-out;
|
||||
}
|
||||
@keyframes fadeDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.width-pills {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
:global(.dark) .width-pills {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.width-pill {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.width-pill:hover {
|
||||
color: #374151;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.width-pill.active {
|
||||
background: var(--color-primary, #8b5cf6);
|
||||
color: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
:global(.dark) .width-pill {
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .width-pill:hover {
|
||||
color: #e5e7eb;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
:global(.dark) .width-pill.active {
|
||||
background: var(--color-primary, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Pages carousel */
|
||||
.fokus-track {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 1rem calc(50% - var(--sheet-width) / 2);
|
||||
scrollbar-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
.fokus-track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-drag-wrapper {
|
||||
flex: 0 0 auto;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.page-drag-wrapper.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Add page button */
|
||||
.neue-seite-card {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
border: 2px dashed rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.empty-pages-wrapper {
|
||||
flex: 0 0 auto;
|
||||
width: var(--sheet-width, min(480px, 85vw));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
.neue-seite-card.alone {
|
||||
width: 100%;
|
||||
min-height: 60vh;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.neue-seite-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 4%, transparent);
|
||||
}
|
||||
:global(.dark) .neue-seite-card {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .neue-seite-card.alone {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .neue-seite-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent);
|
||||
}
|
||||
.neue-seite-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* Minimized tabs */
|
||||
.minimized-tabs {
|
||||
position: fixed;
|
||||
bottom: 4.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 45;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
background: #fffef5;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
animation: slideUp 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .minimized-tabs {
|
||||
background: #252220;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.minimized-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.minimized-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .minimized-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.tab-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #374151;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
}
|
||||
.tab-title:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
:global(.dark) .tab-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
:global(.dark) .tab-title:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
|
||||
.tab-maximize,
|
||||
.tab-close {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0.125rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
:global(.group:hover) .tab-maximize,
|
||||
:global(.group:hover) .tab-close,
|
||||
.minimized-tab:hover .tab-maximize,
|
||||
.minimized-tab:hover .tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
.tab-maximize:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
.tab-close:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
.tab-add {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.15s;
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
.tab-add:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
|
||||
/* Edit FAB */
|
||||
.edit-fab {
|
||||
position: fixed;
|
||||
bottom: 5.5rem;
|
||||
right: 1.25rem;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 9999px;
|
||||
border: none;
|
||||
background: #fffef5;
|
||||
color: #6b7280;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.12),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
z-index: 40;
|
||||
}
|
||||
.edit-fab:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.edit-fab.active {
|
||||
background: var(--color-primary, #8b5cf6);
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(139, 92, 246, 0.3),
|
||||
0 0 0 1px rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
.edit-fab.active:hover {
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 85%, black);
|
||||
color: white;
|
||||
}
|
||||
:global(.dark) .edit-fab {
|
||||
background: #252220;
|
||||
color: #9ca3af;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
:global(.dark) .edit-fab:hover {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
:global(.dark) .edit-fab.active {
|
||||
background: var(--color-primary, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:global(.mana-drop-target-hover) {
|
||||
outline: 2px solid var(--color-primary, #6366f1);
|
||||
outline-offset: -2px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue