import { customElement, DeesElement, type TemplateResult, html, property, css, cssManager, state, } from '@design.estate/dees-element'; import { DeesIcon } from '@design.estate/dees-catalog'; import { demo } from './eco-view-browser.demo.js'; // Ensure components are registered DeesIcon; declare global { interface HTMLElementTagNameMap { 'eco-view-browser': EcoViewBrowser; } } // Types export interface IBookmark { id: string; title: string; url: string; favicon?: string; createdAt: Date; } export interface IBrowserState { url: string; title: string; canGoBack: boolean; canGoForward: boolean; isLoading: boolean; } export type TBrowserPanel = 'browser' | 'bookmarks'; @customElement('eco-view-browser') export class EcoViewBrowser extends DeesElement { public static demo = demo; public static demoGroup = 'Views'; public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')}; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .browser-container { display: flex; flex-direction: column; height: 100%; } .toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')}; } .nav-buttons { display: flex; gap: 4px; } .nav-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; border-radius: 6px; background: transparent; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; cursor: pointer; transition: all 0.15s ease; } .nav-button:hover:not(:disabled) { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .nav-button:disabled { opacity: 0.4; cursor: not-allowed; } .nav-button.loading dees-icon { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .url-container { flex: 1; display: flex; align-items: center; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')}; border-radius: 8px; padding: 0 12px; height: 36px; transition: border-color 0.15s ease; } .url-container:focus-within { border-color: hsl(217 91% 60%); } .url-icon { margin-right: 8px; opacity: 0.5; } .url-input { flex: 1; border: none; background: transparent; font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')}; outline: none; } .url-input::placeholder { color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')}; } .bookmark-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; border-radius: 6px; background: transparent; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; cursor: pointer; transition: all 0.15s ease; } .bookmark-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; } .bookmark-button.bookmarked { color: hsl(45 93% 47%); } .bookmark-button.bookmarked:hover { color: hsl(45 93% 40%); } .menu-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; border-radius: 6px; background: transparent; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; cursor: pointer; transition: all 0.15s ease; } .menu-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; } .webview-container { flex: 1; position: relative; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 5%)')}; overflow: hidden; } .webview-wrapper { width: 100%; height: 100%; position: absolute; top: 0; left: 0; right: 0; bottom: 0; } webview { width: 100%; height: 100%; border: none; display: flex; } .placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; gap: 16px; } .placeholder dees-icon { opacity: 0.3; } .placeholder-text { font-size: 14px; } .placeholder-hint { font-size: 12px; opacity: 0.7; } .bookmarks-bar { display: flex; align-items: center; gap: 4px; padding: 4px 12px; background: ${cssManager.bdTheme('#fafafa', 'hsl(240 6% 9%)')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 13%)')}; overflow-x: auto; } .bookmarks-bar::-webkit-scrollbar { height: 4px; } .bookmarks-bar::-webkit-scrollbar-track { background: transparent; } .bookmarks-bar::-webkit-scrollbar-thumb { background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')}; border-radius: 2px; } .bookmark-chip { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; border-radius: 6px; font-size: 12px; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; cursor: pointer; white-space: nowrap; transition: all 0.15s ease; } .bookmark-chip:hover { background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')}; border-color: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')}; } .bookmark-chip .favicon { width: 14px; height: 14px; border-radius: 2px; } .dropdown-menu { position: absolute; top: 100%; right: 0; margin-top: 4px; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')}; border-radius: 8px; box-shadow: 0 4px 16px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; min-width: 180px; z-index: 100; overflow: hidden; } .dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; cursor: pointer; transition: background 0.1s ease; } .dropdown-item:hover { background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')}; } .dropdown-item.destructive { color: hsl(0 72% 50%); } .dropdown-item.destructive:hover { background: hsl(0 72% 95%); } .dropdown-divider { height: 1px; background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; margin: 4px 0; } .menu-container { position: relative; } /* Bookmarks panel styles */ .bookmarks-panel { padding: 32px 48px; overflow-y: auto; height: 100%; } .panel-header { margin-bottom: 24px; } .panel-title { font-size: 28px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; margin: 0 0 8px 0; } .panel-description { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; margin: 0; } .bookmarks-list { display: flex; flex-direction: column; gap: 8px; } .bookmark-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; border-radius: 8px; cursor: pointer; transition: all 0.15s ease; } .bookmark-item:hover { border-color: hsl(217 91% 60%); transform: translateX(2px); } .bookmark-item .favicon { width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; } .bookmark-info { flex: 1; min-width: 0; } .bookmark-title { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .bookmark-url { font-size: 12px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .bookmark-delete { opacity: 0; transition: opacity 0.15s ease; } .bookmark-item:hover .bookmark-delete { opacity: 1; } .bookmark-delete-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: 4px; background: transparent; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; cursor: pointer; } .bookmark-delete-btn:hover { background: hsl(0 72% 95%); color: hsl(0 72% 50%); } .empty-state { text-align: center; padding: 60px 20px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .empty-state dees-icon { margin-bottom: 16px; opacity: 0.4; } .empty-state p { font-size: 14px; margin: 0; } .loading-bar { position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, hsl(217 91% 60%) 0%, hsl(217 91% 70%) 50%, hsl(217 91% 60%) 100% ); background-size: 200% 100%; animation: loading 1.5s linear infinite; } @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } `, ]; @property({ type: Array }) accessor bookmarks: IBookmark[] = []; @property({ type: Boolean }) accessor showBookmarksBar = true; @property({ type: String }) accessor defaultUrl = ''; @state() accessor activePanel: TBrowserPanel = 'browser'; @state() accessor currentUrl = ''; @state() accessor inputUrl = ''; @state() accessor pageTitle = ''; @state() accessor canGoBack = false; @state() accessor canGoForward = false; @state() accessor isLoading = false; @state() accessor showMenu = false; private isElectron = false; private webviewEventsBound = false; async connectedCallback(): Promise { await super.connectedCallback(); // Check if we're in Electron - webview tag only works in Electron // Since nodeIntegration is disabled, we check for the existence of electronAPI // or if we're in a file:// protocol (Electron loads from file://) this.isElectron = typeof window !== 'undefined' && ( typeof (window as any).electronAPI !== 'undefined' || window.location.protocol === 'file:' || navigator.userAgent.includes('Electron') ); if (this.defaultUrl) { this.inputUrl = this.defaultUrl; } } protected updated(changedProperties: Map): void { super.updated(changedProperties); // Bind webview events after it's rendered this.bindWebviewEvents(); } private bindWebviewEvents(): void { if (!this.isElectron || this.webviewEventsBound) return; const webview = this.shadowRoot?.querySelector('webview') as any; if (!webview) return; this.webviewEventsBound = true; webview.addEventListener('did-start-loading', () => { this.isLoading = true; }); webview.addEventListener('did-stop-loading', () => { this.isLoading = false; this.updateNavigationState(); }); webview.addEventListener('did-navigate', (e: any) => { this.currentUrl = e.url || this.currentUrl; this.inputUrl = this.currentUrl; this.updateNavigationState(); }); webview.addEventListener('did-navigate-in-page', (e: any) => { if (e.isMainFrame) { this.currentUrl = e.url || this.currentUrl; this.inputUrl = this.currentUrl; } }); webview.addEventListener('page-title-updated', (e: any) => { this.pageTitle = e.title || ''; }); webview.addEventListener('page-favicon-updated', (e: any) => { // Could store favicon for bookmarks }); webview.addEventListener('dom-ready', () => { this.updateNavigationState(); }); console.log('Webview events bound'); } public render(): TemplateResult { return html`
${this.renderToolbar()} ${this.showBookmarksBar && this.bookmarks.length > 0 ? this.renderBookmarksBar() : ''} ${this.activePanel === 'browser' ? this.renderBrowserContent() : this.renderBookmarksPanel()}
`; } private renderToolbar(): TemplateResult { return html`
this.inputUrl = (e.target as HTMLInputElement).value} @keydown=${this.handleUrlKeydown} />
`; } private renderMenu(): TemplateResult { return html` `; } private renderBookmarksBar(): TemplateResult { return html`
${this.bookmarks.slice(0, 10).map(bookmark => html`
this.navigate(bookmark.url)} > ${bookmark.favicon ? html`` : html`` } ${bookmark.title || new URL(bookmark.url).hostname}
`)}
`; } private renderBrowserContent(): TemplateResult { return html`
${this.isLoading ? html`
` : ''}
${this.currentUrl ? this.renderWebview() : this.renderPlaceholder()}
`; } private renderWebview(): TemplateResult { // In Electron, we use webview tag. In browser, we show a notice. if (this.isElectron) { // Reset event binding flag when URL changes so events get rebound // Note: webview events are bound in updated() lifecycle return html` `; } else { // Fallback for non-Electron environment (demo/testing) return html`
Webview requires Electron environment URL: ${this.currentUrl}
`; } } private renderPlaceholder(): TemplateResult { return html`
Enter a URL to start browsing or select a bookmark
`; } private renderBookmarksPanel(): TemplateResult { return html`

Bookmarks

Your saved websites

${this.bookmarks.length > 0 ? html`
${this.bookmarks.map(bookmark => html`
this.handleBookmarkClick(bookmark)}> ${bookmark.favicon ? html`` : html`` }
${bookmark.title || bookmark.url}
${bookmark.url}
`)}
` : html`

No bookmarks yet. Star a page to save it here.

` }
`; } // Navigation methods private navigate(url: string): void { if (!url) return; // Add protocol if missing if (!/^https?:\/\//i.test(url)) { // Check if it looks like a domain if (/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}/.test(url)) { url = 'https://' + url; } else { // Treat as search query url = `https://www.google.com/search?q=${encodeURIComponent(url)}`; } } this.currentUrl = url; this.inputUrl = url; this.isLoading = true; this.dispatchEvent(new CustomEvent('navigate', { detail: { url }, bubbles: true, composed: true, })); } private handleUrlKeydown(e: KeyboardEvent): void { if (e.key === 'Enter') { this.navigate(this.inputUrl); } } private get webview(): any { return this.shadowRoot?.querySelector('webview'); } private handleBack(): void { const wv = this.webview; if (wv && this.isElectron) { wv.goBack(); } this.dispatchEvent(new CustomEvent('browser-back', { bubbles: true, composed: true })); } private handleForward(): void { const wv = this.webview; if (wv && this.isElectron) { wv.goForward(); } this.dispatchEvent(new CustomEvent('browser-forward', { bubbles: true, composed: true })); } private handleRefresh(): void { const wv = this.webview; if (wv && this.isElectron) { wv.reload(); } this.dispatchEvent(new CustomEvent('browser-refresh', { bubbles: true, composed: true })); } private handleStop(): void { const wv = this.webview; if (wv && this.isElectron) { wv.stop(); } this.isLoading = false; this.dispatchEvent(new CustomEvent('browser-stop', { bubbles: true, composed: true })); } private handleFaviconUpdate(e: any): void { // Could update current page favicon for bookmarks } private updateNavigationState(): void { if (this.webview && this.isElectron) { this.canGoBack = (this.webview as any).canGoBack?.() || false; this.canGoForward = (this.webview as any).canGoForward?.() || false; } } // Bookmark methods private isCurrentPageBookmarked(): boolean { return this.bookmarks.some(b => b.url === this.currentUrl); } private toggleBookmark(): void { if (!this.currentUrl) return; if (this.isCurrentPageBookmarked()) { const bookmark = this.bookmarks.find(b => b.url === this.currentUrl); if (bookmark) { this.removeBookmark(bookmark.id); } } else { this.addBookmark(); } } private addBookmark(): void { const bookmark: IBookmark = { id: crypto.randomUUID(), title: this.pageTitle || this.currentUrl, url: this.currentUrl, createdAt: new Date(), }; this.dispatchEvent(new CustomEvent('bookmark-add', { detail: { bookmark }, bubbles: true, composed: true, })); } private removeBookmark(id: string): void { this.dispatchEvent(new CustomEvent('bookmark-remove', { detail: { id }, bubbles: true, composed: true, })); } private handleBookmarkClick(bookmark: IBookmark): void { this.navigate(bookmark.url); this.activePanel = 'browser'; } // Menu actions private handleNewTab(): void { this.showMenu = false; this.dispatchEvent(new CustomEvent('new-tab', { bubbles: true, composed: true })); } private handleOpenDevTools(): void { this.showMenu = false; if (this.webview && this.isElectron) { (this.webview as any).openDevTools(); } this.dispatchEvent(new CustomEvent('open-devtools', { bubbles: true, composed: true })); } private handleCopyUrl(): void { this.showMenu = false; if (this.currentUrl) { navigator.clipboard.writeText(this.currentUrl); } } // Public API public goTo(url: string): void { this.navigate(url); } public setBookmarks(bookmarks: IBookmark[]): void { this.bookmarks = bookmarks; } public getState(): IBrowserState { return { url: this.currentUrl, title: this.pageTitle, canGoBack: this.canGoBack, canGoForward: this.canGoForward, isLoading: this.isLoading, }; } }