mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 03:19:40 +02:00
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>
155 lines
2.8 KiB
Svelte
155 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* AppPanel Component
|
|
* iFrame container for displaying an app in split-screen.
|
|
*/
|
|
|
|
import type { PanelConfig } from '../types.js';
|
|
|
|
interface Props {
|
|
panel: PanelConfig;
|
|
class?: string;
|
|
}
|
|
|
|
let { panel, class: className = '' }: Props = $props();
|
|
|
|
let isLoading = $state(true);
|
|
let hasError = $state(false);
|
|
|
|
function handleLoad() {
|
|
isLoading = false;
|
|
hasError = false;
|
|
}
|
|
|
|
function handleError() {
|
|
isLoading = false;
|
|
hasError = true;
|
|
}
|
|
|
|
// iFrame sandbox permissions
|
|
const sandboxPermissions = [
|
|
'allow-same-origin',
|
|
'allow-scripts',
|
|
'allow-forms',
|
|
'allow-popups',
|
|
'allow-popups-to-escape-sandbox',
|
|
'allow-storage-access-by-user-activation',
|
|
].join(' ');
|
|
</script>
|
|
|
|
<div class="app-panel {className}">
|
|
{#if isLoading}
|
|
<div class="loading-state">
|
|
<div class="spinner"></div>
|
|
<span>Loading {panel.name || panel.appId}...</span>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if hasError}
|
|
<div class="error-state">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="48"
|
|
height="48"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="12" y1="8" x2="12" y2="12" />
|
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
</svg>
|
|
<span>Failed to load {panel.name || panel.appId}</span>
|
|
<button
|
|
onclick={() => {
|
|
isLoading = true;
|
|
hasError = false;
|
|
}}
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<iframe
|
|
src={panel.url}
|
|
title={panel.name || panel.appId}
|
|
sandbox={sandboxPermissions}
|
|
class:hidden={hasError}
|
|
onload={handleLoad}
|
|
onerror={handleError}
|
|
></iframe>
|
|
</div>
|
|
|
|
<style>
|
|
.app-panel {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--color-bg-secondary, #1a1a1a);
|
|
overflow: hidden;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
background: var(--color-bg-primary, #0a0a0a);
|
|
}
|
|
|
|
iframe.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.loading-state,
|
|
.error-state {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
color: var(--color-text-secondary, #888);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid var(--color-border, rgba(255, 255, 255, 0.1));
|
|
border-top-color: var(--color-primary, #3b82f6);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.error-state {
|
|
color: var(--color-error, #ef4444);
|
|
}
|
|
|
|
.error-state button {
|
|
margin-top: 8px;
|
|
padding: 8px 16px;
|
|
background: var(--color-primary, #3b82f6);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.error-state button:hover {
|
|
opacity: 0.9;
|
|
}
|
|
</style>
|