mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +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>
112 lines
2.3 KiB
Svelte
112 lines
2.3 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* SplitPaneContainer Component
|
|
* Main container that handles split-screen layout.
|
|
*/
|
|
|
|
import type { Snippet } from 'svelte';
|
|
import { onMount } from 'svelte';
|
|
import { getSplitPanelContext } from '../stores/split-panel.svelte.js';
|
|
import AppPanel from './AppPanel.svelte';
|
|
import PanelControls from './PanelControls.svelte';
|
|
import ResizeHandle from './ResizeHandle.svelte';
|
|
|
|
interface Props {
|
|
children: Snippet;
|
|
class?: string;
|
|
}
|
|
|
|
let { children, class: className = '' }: Props = $props();
|
|
|
|
const splitPanel = getSplitPanelContext();
|
|
|
|
// Grid template based on divider position
|
|
let gridTemplate = $derived(
|
|
splitPanel.isActive && splitPanel.rightPanel ? `${splitPanel.dividerPosition}% 6px 1fr` : '1fr'
|
|
);
|
|
|
|
function handleResize(position: number) {
|
|
splitPanel.setDividerPosition(position);
|
|
}
|
|
|
|
function handleReset() {
|
|
splitPanel.resetDividerPosition();
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="split-pane-container {className}"
|
|
class:split-active={splitPanel.isActive && splitPanel.rightPanel}
|
|
style:--grid-template={gridTemplate}
|
|
>
|
|
<div class="main-panel">
|
|
{@render children()}
|
|
</div>
|
|
|
|
{#if splitPanel.isActive && splitPanel.rightPanel}
|
|
<ResizeHandle
|
|
position={splitPanel.dividerPosition}
|
|
onResize={handleResize}
|
|
onReset={handleReset}
|
|
/>
|
|
|
|
<div class="side-panel">
|
|
<AppPanel panel={splitPanel.rightPanel} />
|
|
<PanelControls
|
|
panelName={splitPanel.rightPanel.name || splitPanel.rightPanel.appId}
|
|
onSwap={() => splitPanel.swapPanels()}
|
|
onClose={() => splitPanel.closePanel()}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.split-pane-container {
|
|
display: grid;
|
|
grid-template-columns: var(--grid-template, 1fr);
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.main-panel {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.side-panel {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Ensure proper stacking */
|
|
.split-active .main-panel {
|
|
z-index: 1;
|
|
}
|
|
|
|
.split-active .side-panel {
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Hide side panel on mobile via media query as fallback */
|
|
@media (max-width: 1023px) {
|
|
.split-pane-container {
|
|
grid-template-columns: 1fr !important;
|
|
}
|
|
|
|
.side-panel,
|
|
.split-pane-container :global(.resize-handle) {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|