This commit is contained in:
2025-12-22 10:53:15 +00:00
commit 753a98c67b
63 changed files with 15976 additions and 0 deletions

View File

@@ -0,0 +1,622 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import { injectCssVariables } from '../../00variables.js';
import type { DeesMobileViewstack } from './dees-mobile-viewstack.js';
// Shared styles for demos
const sharedStyles = html`
<style>
.demo-container {
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 12px;
overflow: hidden;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
}
.view-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
}
.view-title {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin: 0;
}
.back-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: transparent;
border-radius: 6px;
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
cursor: pointer;
font-size: 20px;
}
.back-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
cursor: pointer;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
transition: background 150ms ease;
}
.list-item:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.list-item:active {
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.item-title {
font-weight: 500;
}
.item-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 4px;
}
.chevron {
color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
}
.item-detail {
padding: 24px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.item-detail h2 {
margin: 0 0 16px;
font-size: 24px;
}
.item-detail p {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
line-height: 1.6;
}
.status-bar {
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
border-top: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.control-panel {
display: flex;
gap: 8px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
flex-wrap: wrap;
}
.control-button {
padding: 8px 16px;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#3f3f46')};
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
}
.control-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
}
.control-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-button.primary {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
.control-button.primary:hover {
background: #2563eb;
}
</style>
`;
// Helper functions
const handleListClick = (viewstack: DeesMobileViewstack, listName: string) => {
const listView = viewstack.querySelector('[view-id="list"]');
if (listView) {
(listView as HTMLElement).dataset.listName = listName;
}
viewstack.pushView('list');
};
const handleItemClick = (viewstack: DeesMobileViewstack, itemName: string) => {
const itemView = viewstack.querySelector('[view-id="item"]');
if (itemView) {
(itemView as HTMLElement).dataset.itemName = itemName;
}
viewstack.pushView('item');
};
const handleBack = (viewstack: DeesMobileViewstack) => {
viewstack.popView();
};
/**
* Demo 1: Mobile Phone Layout
* Simulates a typical mobile app navigation pattern
*/
const mobileDemo = () => {
injectCssVariables();
return html`
${sharedStyles}
<style>
.mobile-frame {
width: 375px;
height: 667px;
border: 8px solid ${cssManager.bdTheme('#1f1f1f', '#404040')};
border-radius: 32px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
</style>
<h3 style="margin: 0 0 16px; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">Mobile Phone Layout (375x667)</h3>
<p style="margin: 0 0 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
Simulates iPhone SE dimensions. Tap items to navigate forward, use back button to return.
</p>
<div class="mobile-frame">
<div class="demo-container" style="height: 100%; border: none; border-radius: 0;">
<dees-mobile-viewstack
initial-view="lists"
style="height: calc(100% - 44px);"
@view-changed=${(e: CustomEvent) => {
const target = e.target as HTMLElement;
const status = target?.closest('.demo-container')?.querySelector('.status-bar');
if (status) {
status.textContent = `${e.detail.currentView} (depth: ${e.detail.stackDepth})`;
}
}}
>
<dees-mobile-view view-id="lists">
<div class="view-header">
<h1 class="view-title">My Lists</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
if (viewstack) handleListClick(viewstack, 'Shopping');
}}>
<div>
<div class="item-title">Shopping List</div>
<div class="item-subtitle">12 items</div>
</div>
<span class="chevron"></span>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
if (viewstack) handleListClick(viewstack, 'Todo');
}}>
<div>
<div class="item-title">Todo List</div>
<div class="item-subtitle">5 items</div>
</div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="list">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
if (viewstack) handleBack(viewstack);
}}></button>
<h1 class="view-title">Items</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
if (viewstack) handleItemClick(viewstack, 'Milk');
}}>
<div><div class="item-title">Milk</div></div>
<span class="chevron"></span>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
if (viewstack) handleItemClick(viewstack, 'Bread');
}}>
<div><div class="item-title">Bread</div></div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="item">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
if (viewstack) handleBack(viewstack);
}}></button>
<h1 class="view-title">Details</h1>
</div>
<div class="item-detail">
<h2>Item Details</h2>
<p>Full item information would appear here.</p>
</div>
</dees-mobile-view>
</dees-mobile-viewstack>
<div class="status-bar">lists (depth: 1)</div>
</div>
</div>
`;
};
/**
* Demo 2: Desktop/Tablet Layout
* Wider layout suitable for tablets and desktop embedded views
*/
const desktopDemo = () => {
injectCssVariables();
return html`
${sharedStyles}
<h3 style="margin: 0 0 16px; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">Desktop/Tablet Layout</h3>
<p style="margin: 0 0 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
Wider container suitable for tablet or embedded desktop use. Same navigation behavior.
</p>
<div class="demo-container" style="width: 100%; max-width: 600px; height: 500px;">
<dees-mobile-viewstack
initial-view="categories"
style="height: calc(100% - 44px);"
@view-changed=${(e: CustomEvent) => {
const target = e.target as HTMLElement;
const status = target?.closest('.demo-container')?.querySelector('.status-bar');
if (status) {
status.textContent = `View: ${e.detail.currentView} | Stack: ${e.detail.stackDepth} | Direction: ${e.detail.direction}`;
}
}}
>
<dees-mobile-view view-id="categories">
<div class="view-header">
<h1 class="view-title">Categories</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.pushView('products');
}}>
<div>
<div class="item-title">Electronics</div>
<div class="item-subtitle">248 products</div>
</div>
<span class="chevron"></span>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.pushView('products');
}}>
<div>
<div class="item-title">Clothing</div>
<div class="item-subtitle">512 products</div>
</div>
<span class="chevron"></span>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.pushView('products');
}}>
<div>
<div class="item-title">Home & Garden</div>
<div class="item-subtitle">189 products</div>
</div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="products">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.popView();
}}></button>
<h1 class="view-title">Products</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.pushView('product-detail');
}}>
<div>
<div class="item-title">Wireless Headphones</div>
<div class="item-subtitle">$149.99</div>
</div>
<span class="chevron"></span>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.pushView('product-detail');
}}>
<div>
<div class="item-title">Smart Watch</div>
<div class="item-subtitle">$299.99</div>
</div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="product-detail">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
const target = e.target as HTMLElement;
const viewstack = target?.closest('dees-mobile-viewstack') as DeesMobileViewstack;
viewstack?.popView();
}}></button>
<h1 class="view-title">Product Details</h1>
</div>
<div class="item-detail">
<h2>Wireless Headphones</h2>
<p>Premium noise-cancelling headphones with 30-hour battery life.</p>
<p style="margin-top: 16px; font-weight: 600; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">$149.99</p>
</div>
</dees-mobile-view>
</dees-mobile-viewstack>
<div class="status-bar">View: categories | Stack: 1 | Direction: none</div>
</div>
`;
};
/**
* Demo 3: Programmatic Control
* Demonstrates API methods for controlling navigation
* Uses dees-demowrapper for proper scoped element access in wcctools
*/
const programmaticDemo = () => {
injectCssVariables();
return html`
${sharedStyles}
<h3 style="margin: 0 0 16px; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">Programmatic Control</h3>
<p style="margin: 0 0 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
Use the control panel to navigate programmatically via the viewstack API.
</p>
<dees-demowrapper .runAfterRender=${async (wrapper: HTMLElement) => {
const viewstack = wrapper.querySelector('dees-mobile-viewstack') as DeesMobileViewstack;
const backBtn = wrapper.querySelector('.btn-back') as HTMLButtonElement;
const rootBtn = wrapper.querySelector('.btn-root') as HTMLButtonElement;
const statusBar = wrapper.querySelector('.status-bar') as HTMLElement;
const pushABtn = wrapper.querySelector('.btn-push-a') as HTMLButtonElement;
const pushBBtn = wrapper.querySelector('.btn-push-b') as HTMLButtonElement;
const pushCBtn = wrapper.querySelector('.btn-push-c') as HTMLButtonElement;
if (!viewstack) return;
const updateButtons = () => {
if (backBtn) backBtn.disabled = !viewstack.canGoBack;
if (rootBtn) rootBtn.disabled = viewstack.stackDepth <= 1;
};
const updateStatus = () => {
if (statusBar) {
statusBar.textContent = `Current: ${viewstack.currentView} | Stack: [${viewstack.viewStack.join(' → ')}] | canGoBack: ${viewstack.canGoBack}`;
}
};
// Set up button click handlers
pushABtn?.addEventListener('click', () => viewstack.pushView('view-a'));
pushBBtn?.addEventListener('click', () => viewstack.pushView('view-b'));
pushCBtn?.addEventListener('click', () => viewstack.pushView('view-c'));
backBtn?.addEventListener('click', () => viewstack.popView());
rootBtn?.addEventListener('click', () => viewstack.goToRoot(false));
// Listen for view changes to update UI
viewstack.addEventListener('view-changed', () => {
updateButtons();
updateStatus();
});
// Initial state
updateButtons();
updateStatus();
}}>
<div class="demo-container" style="width: 100%; max-width: 500px; height: 450px;">
<div class="control-panel">
<button class="control-button primary btn-push-a">Push View A</button>
<button class="control-button primary btn-push-b">Push View B</button>
<button class="control-button primary btn-push-c">Push View C</button>
<button class="control-button btn-back" disabled>Pop View</button>
<button class="control-button btn-root" disabled>Go to Root</button>
</div>
<dees-mobile-viewstack initial-view="home" style="height: calc(100% - 100px);">
<dees-mobile-view view-id="home">
<div class="item-detail" style="text-align: center; padding-top: 60px;">
<h2>Home View</h2>
<p>This is the root view. Use the buttons above to push views onto the stack.</p>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="view-a">
<div class="item-detail" style="text-align: center; padding-top: 60px; background: ${cssManager.bdTheme('#fef2f2', '#1c1917')};">
<h2 style="color: #ef4444;">View A</h2>
<p>You navigated to View A</p>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="view-b">
<div class="item-detail" style="text-align: center; padding-top: 60px; background: ${cssManager.bdTheme('#f0fdf4', '#14532d')};">
<h2 style="color: #22c55e;">View B</h2>
<p>You navigated to View B</p>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="view-c">
<div class="item-detail" style="text-align: center; padding-top: 60px; background: ${cssManager.bdTheme('#eff6ff', '#1e3a5f')};">
<h2 style="color: #3b82f6;">View C</h2>
<p>You navigated to View C</p>
</div>
</dees-mobile-view>
</dees-mobile-viewstack>
<div class="status-bar">Current: home | Stack: [home] | canGoBack: false</div>
</div>
</dees-demowrapper>
`;
};
/**
* Demo 4: Deep Navigation (4+ levels)
* Shows handling of deeply nested navigation
*/
const deepNavigationDemo = () => {
injectCssVariables();
return html`
${sharedStyles}
<h3 style="margin: 0 0 16px; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">Deep Navigation (5 Levels)</h3>
<p style="margin: 0 0 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
Navigate through 5 levels deep: Region → Country → City → District → Location
</p>
<div class="demo-container" style="width: 100%; max-width: 450px; height: 500px;">
<dees-mobile-viewstack
initial-view="regions"
style="height: calc(100% - 44px);"
@view-changed=${(e: CustomEvent) => {
const target = e.target as HTMLElement;
const status = target?.closest('.demo-container')?.querySelector('.status-bar');
if (status) {
const depth = e.detail.stackDepth;
const levels = ['Regions', 'Country', 'City', 'District', 'Location'];
status.textContent = `Level ${depth}/5: ${levels[depth - 1] || 'Unknown'}`;
}
}}
>
<dees-mobile-view view-id="regions">
<div class="view-header">
<h1 class="view-title">Regions</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
const target = e.target as HTMLElement;
(target?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.pushView('country');
}}>
<div><div class="item-title">Europe</div><div class="item-subtitle">44 countries</div></div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="country">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
(e.target as HTMLElement)?.closest('dees-mobile-viewstack')?.dispatchEvent(new CustomEvent('pop-request'));
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.popView();
}}></button>
<h1 class="view-title">Germany</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.pushView('city');
}}>
<div><div class="item-title">Berlin</div><div class="item-subtitle">12 districts</div></div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="city">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.popView();
}}></button>
<h1 class="view-title">Berlin</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.pushView('district');
}}>
<div><div class="item-title">Mitte</div><div class="item-subtitle">Central district</div></div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="district">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.popView();
}}></button>
<h1 class="view-title">Mitte</h1>
</div>
<div class="list-item" @click=${(e: Event) => {
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.pushView('location');
}}>
<div><div class="item-title">Brandenburg Gate</div><div class="item-subtitle">Historic landmark</div></div>
<span class="chevron"></span>
</div>
</dees-mobile-view>
<dees-mobile-view view-id="location">
<div class="view-header">
<button class="back-button" @click=${(e: Event) => {
((e.target as HTMLElement)?.closest('dees-mobile-viewstack') as DeesMobileViewstack)?.popView();
}}></button>
<h1 class="view-title">Brandenburg Gate</h1>
</div>
<div class="item-detail">
<h2>Brandenburg Gate</h2>
<p>An 18th-century neoclassical monument in Berlin. One of the best-known landmarks of Germany.</p>
<p style="margin-top: 16px;">
<strong>You've reached the deepest level!</strong><br>
Use the back button to navigate up through the hierarchy.
</p>
</div>
</dees-mobile-view>
</dees-mobile-viewstack>
<div class="status-bar">Level 1/5: Regions</div>
</div>
`;
};
// Export array of demo functions
export const demoFunc = [
mobileDemo,
desktopDemo,
programmaticDemo,
deepNavigationDemo,
];

View File

@@ -0,0 +1,409 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import '../dees-mobile-view/dees-mobile-view.js';
import type { DeesMobileView } from '../dees-mobile-view/dees-mobile-view.js';
import { demoFunc } from './dees-mobile-viewstack.demo.js';
export interface IViewChangeEvent {
currentView: string;
previousView: string | null;
direction: 'forward' | 'back' | 'none';
stackDepth: number;
}
export interface IRouterConfig {
[route: string]: string;
}
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-viewstack': DeesMobileViewstack;
}
}
/**
* A programmatic view stack component for managing nested navigation with sliding transitions.
*
* @example
* ```html
* <dees-mobile-viewstack initial-view="lists">
* <dees-mobile-view view-id="lists">
* <view-lists></view-lists>
* </dees-mobile-view>
* <dees-mobile-view view-id="list">
* <view-list></view-list>
* </dees-mobile-view>
* <dees-mobile-view view-id="item">
* <view-item-details></view-item-details>
* </dees-mobile-view>
* </dees-mobile-viewstack>
* ```
*
* @fires view-changed - Fired when navigation completes
* @fires transition-start - Fired when animation begins
* @fires transition-end - Fired when animation completes
*/
@customElement('dees-mobile-viewstack')
export class DeesMobileViewstack extends DeesElement {
public static demo = demoFunc;
@property({ type: String, attribute: 'initial-view' })
accessor initialView: string = '';
@state()
accessor viewStack: string[] = [];
@state()
accessor navigationDirection: 'forward' | 'back' | 'none' = 'none';
@state()
accessor isTransitioning: boolean = false;
@state()
accessor currentView: string | null = null;
@state()
accessor previousView: string | null = null;
private viewRegistry: Map<string, DeesMobileView> = new Map();
private animationDuration = 300;
private connectedRouter: any = null;
private routerConfig: IRouterConfig = {};
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
}
.viewstack-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
::slotted(dees-mobile-view) {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
`,
];
async connectedCallback(): Promise<void> {
await super.connectedCallback();
// Wait for slot content to be available
await this.updateComplete;
// Register child views
this.registerChildViews();
// Set initial view
if (this.initialView && !this.currentView) {
this.viewStack = [this.initialView];
this.currentView = this.initialView;
this.activateView(this.initialView);
}
}
private registerChildViews(): void {
const slot = this.shadowRoot?.querySelector('slot');
if (!slot) return;
const views = slot.assignedElements().filter(
(el): el is DeesMobileView => el.tagName.toLowerCase() === 'dees-mobile-view'
);
this.viewRegistry.clear();
for (const view of views) {
if (view.viewId) {
this.viewRegistry.set(view.viewId, view);
view.active = false;
}
}
}
private activateView(viewId: string): void {
const view = this.viewRegistry.get(viewId);
if (view) {
view.active = true;
}
}
private deactivateView(viewId: string): void {
const view = this.viewRegistry.get(viewId);
if (view) {
view.active = false;
view.endAnimation();
}
}
/**
* Get the current stack depth
*/
public get stackDepth(): number {
return this.viewStack.length;
}
/**
* Check if navigation back is possible
*/
public get canGoBack(): boolean {
return this.viewStack.length > 1;
}
/**
* Push a new view onto the stack (slide forward animation)
*/
public async pushView(viewId: string): Promise<void> {
if (this.isTransitioning) return;
if (!this.viewRegistry.has(viewId)) {
console.warn(`View "${viewId}" not found in viewstack`);
return;
}
if (this.currentView === viewId) return;
this.isTransitioning = true;
this.navigationDirection = 'forward';
this.previousView = this.currentView;
this.dispatchEvent(new CustomEvent('transition-start', {
bubbles: true,
composed: true,
detail: { direction: 'forward', from: this.currentView, to: viewId }
}));
// Get view elements
const currentViewEl = this.previousView ? this.viewRegistry.get(this.previousView) : null;
const newViewEl = this.viewRegistry.get(viewId);
// Start animations
if (currentViewEl) {
currentViewEl.startAnimation('leaving', 'forward');
}
if (newViewEl) {
newViewEl.active = true;
newViewEl.startAnimation('entering', 'forward');
}
// Update stack
this.viewStack = [...this.viewStack, viewId];
this.currentView = viewId;
// Wait for animation
await this.waitForAnimation();
// Cleanup
if (currentViewEl) {
currentViewEl.active = false;
currentViewEl.endAnimation();
}
if (newViewEl) {
newViewEl.endAnimation();
}
this.isTransitioning = false;
this.navigationDirection = 'none';
this.dispatchViewChangedEvent();
this.dispatchEvent(new CustomEvent('transition-end', {
bubbles: true,
composed: true
}));
}
/**
* Pop the current view and return to previous (slide back animation)
*/
public async popView(): Promise<void> {
if (this.isTransitioning) return;
if (!this.canGoBack) return;
this.isTransitioning = true;
this.navigationDirection = 'back';
this.previousView = this.currentView;
const previousViewId = this.viewStack[this.viewStack.length - 2];
this.dispatchEvent(new CustomEvent('transition-start', {
bubbles: true,
composed: true,
detail: { direction: 'back', from: this.currentView, to: previousViewId }
}));
// Get view elements
const currentViewEl = this.currentView ? this.viewRegistry.get(this.currentView) : null;
const previousViewEl = this.viewRegistry.get(previousViewId);
// Start animations
if (currentViewEl) {
currentViewEl.startAnimation('leaving', 'back');
}
if (previousViewEl) {
previousViewEl.active = true;
previousViewEl.startAnimation('entering', 'back');
}
// Update stack
this.viewStack = this.viewStack.slice(0, -1);
this.currentView = previousViewId;
// Wait for animation
await this.waitForAnimation();
// Cleanup
if (currentViewEl) {
currentViewEl.active = false;
currentViewEl.endAnimation();
}
if (previousViewEl) {
previousViewEl.endAnimation();
}
this.isTransitioning = false;
this.navigationDirection = 'none';
this.dispatchViewChangedEvent();
this.dispatchEvent(new CustomEvent('transition-end', {
bubbles: true,
composed: true
}));
// Emit navigate-back for header integration
this.dispatchEvent(new CustomEvent('navigate-back', {
bubbles: true,
composed: true,
detail: { canGoBack: this.canGoBack }
}));
}
/**
* Replace current view without animation
*/
public replaceView(viewId: string): void {
if (!this.viewRegistry.has(viewId)) {
console.warn(`View "${viewId}" not found in viewstack`);
return;
}
// Deactivate current view
if (this.currentView) {
this.deactivateView(this.currentView);
}
// Update stack (replace last item)
if (this.viewStack.length > 0) {
this.viewStack = [...this.viewStack.slice(0, -1), viewId];
} else {
this.viewStack = [viewId];
}
this.previousView = this.currentView;
this.currentView = viewId;
this.activateView(viewId);
this.dispatchViewChangedEvent();
}
/**
* Go to root view (first in stack)
*/
public async goToRoot(animate: boolean = true): Promise<void> {
if (this.viewStack.length <= 1) return;
const rootViewId = this.viewStack[0];
if (animate) {
// Animate back to root
while (this.viewStack.length > 1) {
await this.popView();
}
} else {
// Instant navigation to root
if (this.currentView) {
this.deactivateView(this.currentView);
}
this.previousView = this.currentView;
this.viewStack = [rootViewId];
this.currentView = rootViewId;
this.activateView(rootViewId);
this.dispatchViewChangedEvent();
}
}
/**
* Connect an optional router for URL-based navigation
*/
public connectRouter(router: any, config: IRouterConfig): void {
this.connectedRouter = router;
this.routerConfig = config;
// Listen for route changes
if (router && typeof router.on === 'function') {
router.on('routeChange', (route: string) => {
const viewId = this.routerConfig[route];
if (viewId && viewId !== this.currentView) {
this.pushView(viewId);
}
});
}
}
/**
* Disconnect the router
*/
public disconnectRouter(): void {
this.connectedRouter = null;
this.routerConfig = {};
}
private async waitForAnimation(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, this.animationDuration));
}
private dispatchViewChangedEvent(): void {
const event: IViewChangeEvent = {
currentView: this.currentView || '',
previousView: this.previousView,
direction: this.navigationDirection,
stackDepth: this.stackDepth
};
this.dispatchEvent(new CustomEvent('view-changed', {
bubbles: true,
composed: true,
detail: event
}));
}
public render(): TemplateResult {
return html`
<div class="viewstack-container">
<slot @slotchange=${this.registerChildViews}></slot>
</div>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-viewstack.js';