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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@bfmeta/sign-util": "^1.3.10",
"@biochain/bio-sdk": "workspace:*",
"@biochain/chain-effect": "workspace:*",
"@biochain/ecosystem-native": "workspace:*",
"@biochain/key-ui": "workspace:*",
"@biochain/key-utils": "workspace:*",
"@biochain/plugin-navigation-sync": "workspace:*",
Expand Down
58 changes: 58 additions & 0 deletions packages/ecosystem-native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@biochain/ecosystem-native",
"version": "0.1.0",
"description": "Native DOM components for Ecosystem desktop with Safari optimization",
"type": "module",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
},
"./react": {
"import": "./react/index.ts",
"types": "./react/index.ts"
},
"./config": {
"import": "./src/config.ts",
"types": "./src/config.ts"
}
},
"files": [
"src",
"react"
],
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck:run": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run --passWithNoTests",
"lint:run": "oxlint .",
"i18n:run": "echo 'No i18n'",
"theme:run": "echo 'No theme'"
},
"dependencies": {
"lit": "^3.2.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"oxlint": "^1.32.0",
"typescript": "^5.9.3",
"vitest": "^4.0.0"
},
"keywords": [
"biochain",
"ecosystem",
"web-components",
"lit",
"safari-optimization"
],
"license": "MIT"
}
119 changes: 119 additions & 0 deletions packages/ecosystem-native/react/HomeButtonWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* React wrapper for ecosystem-home-button Web Component
*/

import { useEffect, useRef, useCallback, type ReactNode } from 'react';
import { ecosystemEvents } from '../src/events';
// Import to register the custom element
import '../src/components/home-button';

export interface HomeButtonWrapperProps {
/** Whether there are running apps (enables swipe gesture) */
hasRunningApps: boolean;
/** Callback when swipe up is detected */
onSwipeUp?: () => void;
/** Callback when button is tapped */
onTap?: () => void;
/** Swipe threshold in pixels (default: 30) */
swipeThreshold?: number;
/** Velocity threshold in px/ms (default: 0.3) */
velocityThreshold?: number;
/** Children to render inside the button */
children: ReactNode;
/** Additional class name */
className?: string;
}

/**
* React wrapper for the native Home Button Web Component
*
* This component wraps the ecosystem-home-button custom element
* and provides React-friendly props and callbacks.
*
* @example
* ```tsx
* <HomeButtonWrapper
* hasRunningApps={hasRunningApps}
* onSwipeUp={() => openStackView()}
* >
* <TabButton icon={EcosystemIcon} />
* </HomeButtonWrapper>
* ```
*/
export function HomeButtonWrapper({
hasRunningApps,
onSwipeUp,
onTap,
swipeThreshold = 30,
velocityThreshold = 0.3,
children,
className,
}: HomeButtonWrapperProps) {
const ref = useRef<HTMLElement>(null);

// Sync React props to Web Component attributes
useEffect(() => {
const element = ref.current;
if (!element) return;

// Set properties directly on the custom element
// Using type assertion for Web Component properties
const homeButton = element as HTMLElement & {
hasRunningApps: boolean;
swipeThreshold: number;
velocityThreshold: number;
};
homeButton.hasRunningApps = hasRunningApps;
homeButton.swipeThreshold = swipeThreshold;
homeButton.velocityThreshold = velocityThreshold;
}, [hasRunningApps, swipeThreshold, velocityThreshold]);

// Handle swipe-up event from Web Component
const handleSwipeUp = useCallback(() => {
onSwipeUp?.();
}, [onSwipeUp]);

// Handle tap event from Web Component
const handleTap = useCallback(() => {
onTap?.();
}, [onTap]);

// Subscribe to events from the event bus
useEffect(() => {
const unsubscribeSwipe = ecosystemEvents.on('home:swipe-up', handleSwipeUp);
const unsubscribeTap = ecosystemEvents.on('home:tap', handleTap);

return () => {
unsubscribeSwipe();
unsubscribeTap();
};
}, [handleSwipeUp, handleTap]);

return (
<ecosystem-home-button
ref={ref}
className={className}
has-running-apps={hasRunningApps || undefined}
swipe-threshold={swipeThreshold}
velocity-threshold={velocityThreshold}
>
{children}
</ecosystem-home-button>
);
}

// Extend JSX types for the custom element
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'ecosystem-home-button': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'has-running-apps'?: boolean;
'swipe-threshold'?: number;
'velocity-threshold'?: number;
},
HTMLElement
>;
}
}
}
17 changes: 17 additions & 0 deletions packages/ecosystem-native/react/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* React wrappers for ecosystem-native Web Components
*/

// Wrappers
export { HomeButtonWrapper, type HomeButtonWrapperProps } from './HomeButtonWrapper';
// export { EcosystemDesktopWrapper } from './EcosystemDesktopWrapper';
// export { SplashScreenWrapper } from './SplashScreenWrapper';

// Re-export core utilities for convenience
export {
ecosystemEvents,
getConfig,
getAnimationLevel,
isAnimationEnabled,
initConfig,
} from '../src/index';
124 changes: 124 additions & 0 deletions packages/ecosystem-native/src/components/home-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Home Button Web Component
*
* A native DOM component that detects upward swipe gestures
* to open the stack view. Designed for Safari stability.
*/

import { LitElement, html, css } from 'lit';
import { ecosystemEvents } from '../events';
import { createUpSwipeDetector } from '../gestures/swipe-detector';

export class HomeButton extends LitElement {
static override styles = css`
:host {
display: contents;
}

.home-button-wrapper {
display: contents;
touch-action: pan-x;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
`;

static override properties = {
hasRunningApps: { type: Boolean, attribute: 'has-running-apps' },
swipeThreshold: { type: Number, attribute: 'swipe-threshold' },
velocityThreshold: { type: Number, attribute: 'velocity-threshold' },
};

/**
* Whether there are running apps (enables swipe gesture)
*/
hasRunningApps = false;

/**
* Swipe threshold in pixels
*/
swipeThreshold = 30;

/**
* Velocity threshold in px/ms
*/
velocityThreshold = 0.3;

private swipeDetector = createUpSwipeDetector();

override connectedCallback(): void {
super.connectedCallback();
// Update detector with current thresholds
this.swipeDetector = createUpSwipeDetector({
threshold: this.swipeThreshold,
velocityThreshold: this.velocityThreshold,
});
}

override updated(changedProperties: Map<string, unknown>): void {
if (changedProperties.has('swipeThreshold') || changedProperties.has('velocityThreshold')) {
this.swipeDetector = createUpSwipeDetector({
threshold: this.swipeThreshold,
velocityThreshold: this.velocityThreshold,
});
}
}

private handleTouchStart = (e: TouchEvent): void => {
this.swipeDetector.handleTouchStart(e);
};

private handleTouchEnd = (e: TouchEvent): void => {
if (!this.hasRunningApps) return;

const result = this.swipeDetector.handleTouchEnd(e);

if (result.detected && result.direction === 'up') {
e.preventDefault();
ecosystemEvents.emit('home:swipe-up', undefined);

// Dispatch custom event for React integration
this.dispatchEvent(
new CustomEvent('swipe-up', {
bubbles: true,
composed: true,
detail: result,
})
);
}
};

private handleClick = (): void => {
ecosystemEvents.emit('home:tap', undefined);

this.dispatchEvent(
new CustomEvent('home-tap', {
bubbles: true,
composed: true,
})
);
};

override render() {
return html`
<div
class="home-button-wrapper"
@touchstart=${this.handleTouchStart}
@touchend=${this.handleTouchEnd}
@click=${this.handleClick}
>
<slot></slot>
</div>
`;
}
}

// Register the custom element
customElements.define('ecosystem-home-button', HomeButton);

declare global {
interface HTMLElementTagNameMap {
'ecosystem-home-button': HomeButton;
}
}
5 changes: 5 additions & 0 deletions packages/ecosystem-native/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Web Components for ecosystem
*/

export { HomeButton } from './home-button';
Loading