mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 15:09:39 +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
13
packages/shared-splitscreen/src/utils/index.ts
Normal file
13
packages/shared-splitscreen/src/utils/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Split-Screen Utilities
|
||||
* Re-export all utility functions.
|
||||
*/
|
||||
|
||||
export { parseUrlState, updateUrlState, clearUrlState, getCurrentUrlState } from './url-state.js';
|
||||
|
||||
export {
|
||||
savePanelState,
|
||||
loadPanelState,
|
||||
clearPanelState,
|
||||
createStorageConfig,
|
||||
} from './local-storage.js';
|
||||
97
packages/shared-splitscreen/src/utils/local-storage.ts
Normal file
97
packages/shared-splitscreen/src/utils/local-storage.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* LocalStorage Utilities
|
||||
* Handle persistent storage for split-screen preferences.
|
||||
*/
|
||||
|
||||
import type { SplitScreenState, StorageConfig } from '../types.js';
|
||||
import { DIVIDER_CONSTRAINTS } from '../types.js';
|
||||
|
||||
const STORAGE_VERSION = 1;
|
||||
|
||||
interface StoredState {
|
||||
version: number;
|
||||
state: Partial<SplitScreenState>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate storage key for an app.
|
||||
*/
|
||||
function getStorageKey(config: StorageConfig): string {
|
||||
return `${config.prefix}-splitscreen-${config.currentAppId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save split-screen state to localStorage.
|
||||
*/
|
||||
export function savePanelState(config: StorageConfig, state: Partial<SplitScreenState>): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const stored: StoredState = {
|
||||
version: STORAGE_VERSION,
|
||||
state: {
|
||||
dividerPosition: state.dividerPosition,
|
||||
rightPanel: state.rightPanel,
|
||||
},
|
||||
};
|
||||
localStorage.setItem(getStorageKey(config), JSON.stringify(stored));
|
||||
} catch (_error) {
|
||||
// localStorage not available or quota exceeded
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load split-screen state from localStorage.
|
||||
*/
|
||||
export function loadPanelState(config: StorageConfig): Partial<SplitScreenState> | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(getStorageKey(config));
|
||||
if (!raw) return null;
|
||||
|
||||
const stored: StoredState = JSON.parse(raw);
|
||||
|
||||
// Version check for future migrations
|
||||
if (stored.version !== STORAGE_VERSION) {
|
||||
clearPanelState(config);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate divider position
|
||||
if (stored.state.dividerPosition !== undefined) {
|
||||
stored.state.dividerPosition = Math.max(
|
||||
DIVIDER_CONSTRAINTS.MIN,
|
||||
Math.min(DIVIDER_CONSTRAINTS.MAX, stored.state.dividerPosition)
|
||||
);
|
||||
}
|
||||
|
||||
return stored.state;
|
||||
} catch (_error) {
|
||||
// localStorage not available or corrupted data
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear split-screen state from localStorage.
|
||||
*/
|
||||
export function clearPanelState(config: StorageConfig): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
localStorage.removeItem(getStorageKey(config));
|
||||
} catch (_error) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default storage config with manacore prefix.
|
||||
*/
|
||||
export function createStorageConfig(currentAppId: string): StorageConfig {
|
||||
return {
|
||||
prefix: 'manacore',
|
||||
currentAppId,
|
||||
};
|
||||
}
|
||||
65
packages/shared-splitscreen/src/utils/url-state.ts
Normal file
65
packages/shared-splitscreen/src/utils/url-state.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* URL State Utilities
|
||||
* Handle URL-based state persistence for split-screen.
|
||||
*/
|
||||
|
||||
import type { UrlState } from '../types.js';
|
||||
|
||||
/**
|
||||
* Parse split-screen state from URL search params.
|
||||
* Reads `?panel=todo&split=60` format.
|
||||
*/
|
||||
export function parseUrlState(searchParams: URLSearchParams): UrlState {
|
||||
const panel = searchParams.get('panel') || undefined;
|
||||
const splitStr = searchParams.get('split');
|
||||
const split = splitStr ? parseInt(splitStr, 10) : undefined;
|
||||
|
||||
return {
|
||||
panel,
|
||||
split: split && !isNaN(split) ? split : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update URL with split-screen state without page reload.
|
||||
* Uses replaceState to avoid adding to browser history.
|
||||
*/
|
||||
export function updateUrlState(state: UrlState): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
if (state.panel) {
|
||||
url.searchParams.set('panel', state.panel);
|
||||
} else {
|
||||
url.searchParams.delete('panel');
|
||||
}
|
||||
|
||||
if (state.split && state.split !== 50) {
|
||||
url.searchParams.set('split', state.split.toString());
|
||||
} else {
|
||||
url.searchParams.delete('split');
|
||||
}
|
||||
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear split-screen state from URL.
|
||||
*/
|
||||
export function clearUrlState(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('panel');
|
||||
url.searchParams.delete('split');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current URL state.
|
||||
*/
|
||||
export function getCurrentUrlState(): UrlState {
|
||||
if (typeof window === 'undefined') return {};
|
||||
return parseUrlState(new URLSearchParams(window.location.search));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue