Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ class WindowStackManager {
stack?.setSlotHidden(appId, hidden);
}

/**
* Set slot interactivity.
*/
setSlotInteractive(desktop: MiniappTargetDesktop, appId: string, interactive: boolean): void {
const stack = this.stacks.get(desktop);
stack?.setSlotInteractive(appId, interactive);
}

/**
* Check if a desktop's WindowStack is registered.
*/
Expand Down
74 changes: 74 additions & 0 deletions packages/ecosystem-native/src/components/window-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ describe('WindowStack', () => {
expect(slot).toBeInstanceOf(HTMLDivElement);
expect(slot.dataset.appId).toBe('test-app-1');
expect(slot.dataset.desktop).toBe('mine');
expect(slot.dataset.interactive).toBe('false');
expect(slot.style.pointerEvents).toBe('none');
expect(stack.hasSlot('test-app-1')).toBe(true);
});

Expand Down Expand Up @@ -81,6 +83,66 @@ describe('WindowStack', () => {
expect(slot.style.display).toBe('');
});

it('should set slot interactive state', () => {
const slot = stack.getOrCreateSlot('test-app-1');

stack.setSlotInteractive('test-app-1', true);
expect(slot.dataset.interactive).toBe('true');
expect(slot.style.pointerEvents).toBe('auto');

stack.setSlotInteractive('test-app-1', false);
expect(slot.dataset.interactive).toBe('false');
expect(slot.style.pointerEvents).toBe('none');
});

it('should keep existing slots after component update', async () => {
const slot = stack.getOrCreateSlot('test-app-1');
expect(stack.hasSlot('test-app-1')).toBe(true);

// Trigger a Lit update cycle
stack.requestUpdate();
await stack.updateComplete;

// Slot should still exist in manager and DOM
expect(stack.hasSlot('test-app-1')).toBe(true);
expect(stack.getSlot('test-app-1')).toBe(slot);
expect(slot.isConnected).toBe(true);
});

it('should attach slots created before first render', async () => {
const localContainer = document.createElement('div');
document.body.appendChild(localContainer);

const earlyStack = document.createElement('ecosystem-window-stack') as WindowStack;
earlyStack.desktop = 'stack';
localContainer.appendChild(earlyStack);

const earlySlot = earlyStack.getOrCreateSlot('early-app');
expect(earlySlot.isConnected).toBe(false);

await earlyStack.updateComplete;

expect(earlySlot.isConnected).toBe(true);
expect(earlyStack.getSlot('early-app')).toBe(earlySlot);

localContainer.remove();
});

it('should reattach detached slot when requesting existing slot', () => {
const slot = stack.getOrCreateSlot('test-app-1');
expect(slot.isConnected).toBe(true);

slot.remove();
expect(slot.isConnected).toBe(false);

const recovered = stack.getOrCreateSlot('test-app-1');

expect(recovered).toBe(slot);
expect(slot.isConnected).toBe(true);
expect(slot.parentElement?.classList.contains('stack-container')).toBe(true);
});


it('should get all slot ids', () => {
stack.getOrCreateSlot('app-1');
stack.getOrCreateSlot('app-2');
Expand Down Expand Up @@ -186,4 +248,16 @@ describe('windowStackManager', () => {
expect(slot.dataset.hidden).toBe('false');
expect(slot.style.display).toBe('');
});

it('should set slot interactive via manager', () => {
const slot = windowStackManager.getOrCreateSlot('mine', 'test-app-1');

windowStackManager.setSlotInteractive('mine', 'test-app-1', true);
expect(slot.dataset.interactive).toBe('true');
expect(slot.style.pointerEvents).toBe('auto');

windowStackManager.setSlotInteractive('mine', 'test-app-1', false);
expect(slot.dataset.interactive).toBe('false');
expect(slot.style.pointerEvents).toBe('none');
});
});
61 changes: 52 additions & 9 deletions packages/ecosystem-native/src/components/window-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,30 @@ export class WindowStack extends LitElement {
*/
private containerEl: HTMLDivElement | null = null;

/**
* Resolve the latest stack container element.
*/
private resolveContainer(): HTMLDivElement | null {
if (this.containerEl && this.contains(this.containerEl)) {
return this.containerEl;
}

this.containerEl = this.querySelector('.stack-container');
return this.containerEl;
}

/**
* Ensure a slot element is attached to the current container.
*/
private ensureSlotAttached(slotInfo: SlotInfo): void {
const container = this.resolveContainer();
if (!container) return;

if (slotInfo.element.parentElement !== container) {
container.appendChild(slotInfo.element);
}
}

/**
* Callback when a slot is created
*/
Expand Down Expand Up @@ -121,15 +145,17 @@ export class WindowStack extends LitElement {
getOrCreateSlot(appId: string): HTMLDivElement {
const existing = this.slots.get(appId);
if (existing) {
this.ensureSlotAttached(existing);
return existing.element;
}

// Create slot element synchronously
const slot = document.createElement('div');
slot.style.cssText = 'grid-area: 1 / 1; pointer-events: auto;';
slot.style.cssText = 'grid-area: 1 / 1; pointer-events: none;';
slot.dataset.miniappSlot = '';
slot.dataset.appId = appId;
slot.dataset.desktop = this.desktop;
slot.dataset.interactive = 'false';

const slotInfo: SlotInfo = {
appId,
Expand All @@ -138,12 +164,7 @@ export class WindowStack extends LitElement {
};

this.slots.set(appId, slotInfo);

// Append to container
const container = this.containerEl ?? this.querySelector('.stack-container');
if (container) {
container.appendChild(slot);
}
this.ensureSlotAttached(slotInfo);

// Notify listeners
this.onSlotCreated?.(slotInfo);
Expand All @@ -168,7 +189,11 @@ export class WindowStack extends LitElement {
* Get an existing slot (returns null if not found).
*/
getSlot(appId: string): HTMLDivElement | null {
return this.slots.get(appId)?.element ?? null;
const slotInfo = this.slots.get(appId);
if (!slotInfo) return null;

this.ensureSlotAttached(slotInfo);
return slotInfo.element;
}

/**
Expand All @@ -189,6 +214,19 @@ export class WindowStack extends LitElement {
}
}

/**
* Set slot interactivity.
* interactive=true => pointer-events:auto (foreground)
* interactive=false => pointer-events:none (background)
*/
setSlotInteractive(appId: string, interactive: boolean): void {
const slot = this.slots.get(appId)?.element;
if (slot) {
slot.dataset.interactive = String(interactive);
slot.style.pointerEvents = interactive ? 'auto' : 'none';
}
}

/**
* Get all slot appIds.
*/
Expand Down Expand Up @@ -218,7 +256,12 @@ export class WindowStack extends LitElement {

override firstUpdated() {
// Cache the container reference
this.containerEl = this.querySelector('.stack-container');
this.containerEl = this.resolveContainer();

// Recover any slot created before first render completed
for (const slotInfo of this.slots.values()) {
this.ensureSlotAttached(slotInfo);
}
}

override render() {
Expand Down
2 changes: 0 additions & 2 deletions src/StackflowApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ export function StackflowApp() {
<MiniappWindow />
{/* 层叠视图 - 多应用管理 */}
<MiniappStackView visible={isStackViewOpen} onClose={closeStackView} />
{/* Fallback 容器 - 当 slot lost 时保持 MiniappWindow 挂载 */}
<div id="miniapp-fallback-portal" style={{ display: 'contents' }} />
{/* 开发版水印 */}
<DevWatermark />
</>
Expand Down
24 changes: 11 additions & 13 deletions src/components/ecosystem/miniapp-motion-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export type MiniappFlow = (typeof MINIAPP_FLOW)[number]

export type WindowContainerVariant = 'open' | 'closed'
export type VisibilityVariant = 'show' | 'hide'
/** 层级变体:top=上层可见,bottom=下层隐藏,gone=display:none */
export type LayerVariant = 'top' | 'bottom' | 'gone'
/** 层级变体:top=上层可见,bottom=下层隐藏 */
export type LayerVariant = 'top' | 'bottom'

export const flowToWindowContainer: Record<MiniappFlow, WindowContainerVariant> = {
closed: 'closed',
Expand All @@ -33,15 +33,14 @@ export const flowToWindowContainer: Record<MiniappFlow, WindowContainerVariant>
* splash-bg 层级(和 iframe 互斥,作为内容层)
* - top: 在 iframe 上方,可见(启动中)
* - bottom: 在 iframe 下方,隐藏(已打开)
* - gone: display:none(后台、已关闭)
*/
export const flowToSplashBgLayer: Record<MiniappFlow, LayerVariant> = {
closed: 'gone',
closed: 'bottom',
opening: 'top',
splash: 'top',
opened: 'bottom',
backgrounding: 'bottom',
backgrounded: 'gone',
backgrounded: 'bottom',
foregrounding: 'bottom',
closing: 'bottom', // 关闭时 splash-bg 不显示,只有 splash-icon 做动画
}
Expand All @@ -50,15 +49,14 @@ export const flowToSplashBgLayer: Record<MiniappFlow, LayerVariant> = {
* iframe 层级(和 splash-bg 互斥,作为内容层)
* - top: 在 splash-bg 上方,可见(已打开)
* - bottom: 在 splash-bg 下方,隐藏(启动中)
* - gone: display:none(后台、已关闭)
*/
export const flowToIframeLayer: Record<MiniappFlow, LayerVariant> = {
closed: 'gone',
closed: 'bottom',
opening: 'bottom',
splash: 'bottom',
opened: 'top',
backgrounding: 'top',
backgrounded: 'gone',
backgrounded: 'bottom',
foregrounding: 'top',
closing: 'top', // 关闭时 iframe 保持在上层,通过 opacity 淡出
}
Expand All @@ -67,13 +65,13 @@ export const flowToIframeLayer: Record<MiniappFlow, LayerVariant> = {
* splash-icon 层级(独立于内容层,只在 opening/closing 时出现做动画)
*/
export const flowToSplashIconLayer: Record<MiniappFlow, LayerVariant> = {
closed: 'gone',
closed: 'bottom',
opening: 'top',
splash: 'top',
opened: 'gone',
backgrounding: 'gone',
backgrounded: 'gone',
foregrounding: 'gone',
opened: 'bottom',
backgrounding: 'bottom',
backgrounded: 'bottom',
foregrounding: 'bottom',
closing: 'top',
}

Expand Down
3 changes: 1 addition & 2 deletions src/components/ecosystem/miniapp-splash-screen.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
justify-content: center;
overflow: hidden;
z-index: 10;
pointer-events: auto;
pointer-events: none;

/* 动画状态 */
opacity: 1;
Expand All @@ -23,7 +23,6 @@

.splashScreen[data-visible="false"] {
opacity: 0;
pointer-events: none;
}

/* 光晕背景层 - 使用 CSS 变量动态设置颜色 */
Expand Down
25 changes: 4 additions & 21 deletions src/components/ecosystem/miniapp-stack-view.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,30 +63,13 @@
font-weight: 500;
}

/* Swiper 区域 */
.swiperWrapper {
/* Item 列表区域 */
.listWrapper {
position: relative;
z-index: 1;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}

.swiper {
width: 100%;
max-width: 320px;
height: 100%;
max-height: 480px;
}

.slide {
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
overflow: hidden;
min-height: 0;
padding-top: 8px;
}

/* 操作提示 */
Expand Down
Loading