managarten/docs/central-services/SPLIT-SCREEN.md
Till-JS f2ac3e245e feat(splitscreen): add split-screen feature for multi-app side-by-side view
Add new @manacore/shared-splitscreen package enabling iFrame-based
split-screen functionality across Calendar, Todo, and Contacts apps.

Features:
- SplitPaneContainer with CSS Grid layout
- AppPanel with iFrame sandbox permissions and loading/error states
- ResizeHandle with mouse, touch, and keyboard support (20-80% range)
- PanelControls for swap and close actions
- Svelte 5 runes-based store with Context API
- URL persistence (?panel=todo&split=60)
- localStorage persistence with versioning
- Mobile auto-disable (<1024px breakpoint)

Integration:
- PillNavigation: added onOpenInPanel prop and Ctrl/Cmd+click support
- PillDropdown: added split button per app item
- Calendar, Todo, Contacts layouts wrapped with SplitPaneContainer

Also fixes:
- WeekView.svelte: fixed {@const} placement error
- MultiDayView.svelte: fixed {@const} placement error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 13:00:26 +01:00

11 KiB

Split-Screen Feature

Das Split-Screen Feature ermöglicht es, zwei ManaCore-Apps nebeneinander in einem Browser-Tab anzuzeigen. Die rechte App wird dabei in einem iFrame eingebettet.

Übersicht

Aspekt Details
Package @manacore/shared-splitscreen
Integrierte Apps Calendar, Todo, Contacts
Aktivierung Split-Button in App-Dropdown oder Ctrl/Cmd+Klick
Persistenz URL-Parameter + localStorage
Mobile Automatisch deaktiviert (<1024px)

Architektur

┌─────────────────────────────────────────────────────────────────┐
│                     SplitPaneContainer                          │
├─────────────────────────┬───────┬───────────────────────────────┤
│                         │       │                               │
│      Main Panel         │ ║ ║ ║ │        Side Panel             │
│    (aktuelle App)       │ ║ ║ ║ │     (iFrame mit App)          │
│                         │ ║ ║ ║ │                               │
│   ┌─────────────────┐   │ Resize│   ┌─────────────────────────┐ │
│   │ PillNavigation  │   │ Handle│   │      AppPanel           │ │
│   └─────────────────┘   │       │   │  ┌─────────────────────┐│ │
│                         │       │   │  │  PanelControls      ││ │
│   ┌─────────────────┐   │       │   │  │  [Swap] [Close]     ││ │
│   │     Content     │   │       │   │  └─────────────────────┘│ │
│   │                 │   │       │   │                         │ │
│   │                 │   │       │   │  ┌─────────────────────┐│ │
│   │                 │   │       │   │  │                     ││ │
│   │                 │   │       │   │  │     <iframe>        ││ │
│   │                 │   │       │   │  │                     ││ │
│   │                 │   │       │   │  └─────────────────────┘│ │
│   └─────────────────┘   │       │   └─────────────────────────┘ │
│                         │       │                               │
└─────────────────────────┴───────┴───────────────────────────────┘

Package-Struktur

packages/shared-splitscreen/
├── src/
│   ├── index.ts                    # Barrel exports
│   ├── types.ts                    # TypeScript types
│   ├── stores/
│   │   └── split-panel.svelte.ts   # Svelte 5 runes store mit Context API
│   ├── components/
│   │   ├── SplitPaneContainer.svelte  # Haupt-Layout (CSS Grid)
│   │   ├── AppPanel.svelte            # iFrame-Container
│   │   ├── PanelControls.svelte       # Swap/Close Buttons
│   │   └── ResizeHandle.svelte        # Draggable Divider
│   └── utils/
│       ├── url-state.ts            # URL-Persistenz (?panel=todo&split=60)
│       ├── local-storage.ts        # localStorage-Persistenz
│       └── index.ts                # Utils barrel
├── package.json
└── tsconfig.json

Komponenten

SplitPaneContainer

Haupt-Container mit CSS Grid Layout.

<script>
  import { SplitPaneContainer, setSplitPanelContext, DEFAULT_APPS } from '@manacore/shared-splitscreen';

  // Context initialisieren
  const splitPanel = setSplitPanelContext('calendar', DEFAULT_APPS);
</script>

<SplitPaneContainer>
  <!-- Dein App-Content -->
  <slot />
</SplitPaneContainer>

CSS Grid:

  • Ohne Split: grid-template-columns: 1fr
  • Mit Split: grid-template-columns: {dividerPos}% 6px 1fr

AppPanel

iFrame-Container mit Loading/Error States.

iFrame Sandbox Permissions:

const sandboxPermissions = [
  'allow-same-origin',     // Für localStorage/Cookie-Zugriff
  'allow-scripts',         // JavaScript ausführen
  'allow-forms',           // Formulare absenden
  'allow-popups',          // Popups öffnen
  'allow-popups-to-escape-sandbox',
  'allow-storage-access-by-user-activation',
];

ResizeHandle

Draggable Divider für Panel-Größenanpassung.

Features:

  • Maus- und Touch-Support
  • Tastatur-Navigation (Pfeiltasten)
  • Doppelklick = Reset auf 50%
  • Constraints: 20% - 80%

Accessibility:

<div
  role="separator"
  aria-orientation="vertical"
  aria-valuenow={position}
  aria-valuemin={20}
  aria-valuemax={80}
  tabindex="0"
/>

PanelControls

Overlay mit Swap- und Close-Buttons.

  • Swap: Navigiert zur App im rechten Panel (window.location.href)
  • Close: Schließt das Split-Panel

Store API

Initialisierung

import { setSplitPanelContext, DEFAULT_APPS } from '@manacore/shared-splitscreen';

// Im Layout-Component (z.B. +layout.svelte)
const splitPanel = setSplitPanelContext('calendar', DEFAULT_APPS);

// In onMount initialisieren (lädt URL/localStorage State)
onMount(() => {
  splitPanel.initialize();
});

Store Interface

interface SplitPanelStore {
  // State (readonly)
  readonly isActive: boolean;           // Split-Modus aktiv?
  readonly rightPanel: PanelConfig | null;  // Rechtes Panel
  readonly dividerPosition: number;     // Position in % (20-80)
  readonly isMobile: boolean;           // Mobile Breakpoint erreicht?
  readonly availableApps: AppDefinition[];  // Verfügbare Apps (ohne aktuelle)

  // Actions
  openPanel: (appId: string, path?: string) => void;
  closePanel: () => void;
  swapPanels: () => void;
  setDividerPosition: (position: number) => void;
  resetDividerPosition: () => void;
  initialize: () => void;
}

Context-Zugriff

import { getSplitPanelContext } from '@manacore/shared-splitscreen';

// In Child-Components
const splitPanel = getSplitPanelContext();

Verfügbare Apps

Standard-Konfiguration in DEFAULT_APPS:

App ID Port Farbe
Calendar calendar 5179 #3b82f6
Todo todo 5188 #10b981
Contacts contacts 5184 #8b5cf6
Clock clock 5187 #f59e0b

Persistenz

URL-State

https://calendar.app/?panel=todo&split=60
  • panel: App-ID des rechten Panels
  • split: Divider-Position in % (nur wenn ≠ 50)

localStorage

// Key: manacore-splitscreen-{appId}
{
  "version": 1,
  "state": {
    "dividerPosition": 60,
    "rightPanel": {
      "appId": "todo",
      "url": "http://localhost:5188/",
      "name": "Todo"
    }
  }
}

Priorität: URL > localStorage

Integration in Apps

1. Dependency hinzufügen

// package.json
{
  "dependencies": {
    "@manacore/shared-splitscreen": "workspace:*"
  }
}

2. Layout anpassen

<!-- +layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { PillNavigation } from '@manacore/shared-ui';
  import {
    SplitPaneContainer,
    setSplitPanelContext,
    DEFAULT_APPS,
  } from '@manacore/shared-splitscreen';

  // Split-Panel Store initialisieren
  const splitPanel = setSplitPanelContext('calendar', DEFAULT_APPS);

  // Handler für Split-Panel Öffnung
  function handleOpenInPanel(appId: string, url: string) {
    splitPanel.openPanel(appId);
  }

  onMount(() => {
    splitPanel.initialize();
  });
</script>

<SplitPaneContainer>
  <div class="layout-container">
    <PillNavigation
      {...props}
      onOpenInPanel={handleOpenInPanel}
    />

    <main>
      {@render children()}
    </main>
  </div>
</SplitPaneContainer>

3. PillNavigation Props

Die PillNavigation-Komponente unterstützt automatisch:

  • Split-Button: Erscheint bei jedem App-Item im Dropdown
  • Modifier-Key Detection: Ctrl/Cmd+Klick öffnet im Split-Panel
// Neues Prop für PillNavigation
onOpenInPanel?: (appId: string, url: string) => void;

Mobile Verhalten

Split-Screen ist auf mobilen Geräten deaktiviert:

const MOBILE_BREAKPOINT = 1024; // px

// Automatische Prüfung bei Resize
window.addEventListener('resize', () => {
  if (window.innerWidth < MOBILE_BREAKPOINT && isActive) {
    closePanel();
  }
});

CSS Fallback:

@media (max-width: 1023px) {
  .split-pane-container {
    grid-template-columns: 1fr !important;
  }
  .side-panel,
  .resize-handle {
    display: none;
  }
}

Constraints

Constraint Wert
Min. Divider Position 20%
Max. Divider Position 80%
Default Position 50%
Mobile Breakpoint 1024px
Resize Handle Breite 6px

Bekannte Einschränkungen

  1. Keine Cross-App Kommunikation: Apps im iFrame können nicht direkt kommunizieren
  2. Separate Auth Sessions: Jede App hat ihre eigene Session (funktioniert wegen shared localStorage)
  3. Kein Drag & Drop zwischen Apps: Feature wurde bewusst nicht implementiert
  4. Performance: iFrame lädt komplette App, kann bei langsamen Verbindungen spürbar sein

Zukünftige Erweiterungen

  • postMessage API für Cross-App Events
  • Mehr als 2 Panels
  • Panel Templates / Presets
  • Keyboard Shortcuts (Cmd+\ zum Togglen)
  • Panel-Position speichern per App-Kombination

Debugging

DevTools

// State prüfen
const store = getSplitPanelContext();
console.log({
  isActive: store.isActive,
  rightPanel: store.rightPanel,
  dividerPosition: store.dividerPosition,
  isMobile: store.isMobile,
});

// Manuell öffnen
store.openPanel('todo');

// Manuell schließen
store.closePanel();

URL testen

# Todo im Split öffnen
http://localhost:5179/?panel=todo

# Mit angepasster Position
http://localhost:5179/?panel=contacts&split=70

localStorage löschen

localStorage.removeItem('manacore-splitscreen-calendar');
localStorage.removeItem('manacore-splitscreen-todo');
localStorage.removeItem('manacore-splitscreen-contacts');