mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
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>
This commit is contained in:
parent
f51708d75a
commit
f2ac3e245e
27 changed files with 2770 additions and 531 deletions
375
docs/central-services/SPLIT-SCREEN.md
Normal file
375
docs/central-services/SPLIT-SCREEN.md
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
# 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.
|
||||
|
||||
```svelte
|
||||
<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:**
|
||||
```typescript
|
||||
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:**
|
||||
```svelte
|
||||
<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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"@manacore/shared-splitscreen": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Layout anpassen
|
||||
|
||||
```svelte
|
||||
<!-- +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
|
||||
|
||||
```typescript
|
||||
// Neues Prop für PillNavigation
|
||||
onOpenInPanel?: (appId: string, url: string) => void;
|
||||
```
|
||||
|
||||
## Mobile Verhalten
|
||||
|
||||
Split-Screen ist auf mobilen Geräten deaktiviert:
|
||||
|
||||
```typescript
|
||||
const MOBILE_BREAKPOINT = 1024; // px
|
||||
|
||||
// Automatische Prüfung bei Resize
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth < MOBILE_BREAKPOINT && isActive) {
|
||||
closePanel();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**CSS Fallback:**
|
||||
```css
|
||||
@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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
localStorage.removeItem('manacore-splitscreen-calendar');
|
||||
localStorage.removeItem('manacore-splitscreen-todo');
|
||||
localStorage.removeItem('manacore-splitscreen-contacts');
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue