import { customElement, DeesElement, type TemplateResult, html, property, css, cssManager, state, } from '@design.estate/dees-element'; import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog'; import type { ISecondaryMenuGroup } from '../../elements/interfaces/secondarymenu.js'; import { demo } from './eco-view-scan.demo.js'; // Ensure components are registered DeesAppuiSecondarymenu; DeesIcon; declare global { interface HTMLElementTagNameMap { 'eco-view-scan': EcoViewScan; } } // Types export type TScanFormat = 'pdf' | 'jpeg' | 'png' | 'tiff'; export type TScanColorMode = 'color' | 'grayscale' | 'blackwhite'; export type TScanSource = 'flatbed' | 'adf' | 'adf-duplex'; export type TScanPanel = 'scan' | 'history' | 'settings'; export interface IScanSettings { format: TScanFormat; resolution: number; colorMode: TScanColorMode; source: TScanSource; } export interface IScannedDocument { id: string; timestamp: Date; format: TScanFormat; data: string; // base64 thumbnail?: string; size: number; name?: string; } export interface IScannerInfo { id: string; name: string; address: string; status: 'online' | 'offline' | 'busy' | 'error'; capabilities?: { resolutions: number[]; formats: TScanFormat[]; colorModes: TScanColorMode[]; sources: TScanSource[]; }; } export interface IDataProviderInfo { id: string; name: string; icon?: string; } @customElement('eco-view-scan') export class EcoViewScan 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('#f5f5f7', 'hsl(240 6% 10%)')}; 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; } .scan-container { display: flex; height: 100%; } dees-appui-secondarymenu { flex-shrink: 0; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')}; border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')}; } .content { flex: 1; overflow-y: auto; padding: 32px 48px; display: flex; flex-direction: column; } .panel-header { margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; } .panel-header-left { display: flex; flex-direction: column; gap: 4px; } .panel-title { font-size: 28px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; } .panel-description { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .scanner-selector { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; } .scanner-selector label { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; } .scanner-selector select { flex: 1; max-width: 300px; padding: 10px 14px; font-size: 14px; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')}; border-radius: 8px; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')}; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')}; cursor: pointer; } .scanner-selector select:focus { outline: none; border-color: hsl(217 91% 60%); } .preview-area { flex: 1; min-height: 300px; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')}; border: 2px dashed ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')}; border-radius: 12px; display: flex; align-items: center; justify-content: center; overflow: hidden; margin-bottom: 24px; } .preview-area.has-image { border-style: solid; } .preview-placeholder { display: flex; flex-direction: column; align-items: center; gap: 12px; color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')}; } .preview-placeholder dees-icon { opacity: 0.5; } .preview-image { max-width: 100%; max-height: 100%; object-fit: contain; } .action-bar { display: flex; gap: 12px; margin-bottom: 32px; } .action-button { display: flex; align-items: center; gap: 8px; padding: 12px 24px; font-size: 14px; font-weight: 500; border: none; border-radius: 8px; cursor: pointer; transition: all 0.15s ease; } .action-button.primary { background: hsl(217 91% 60%); color: white; } .action-button.primary:hover:not(:disabled) { background: hsl(217 91% 55%); } .action-button.secondary { background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')}; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')}; } .action-button.secondary:hover:not(:disabled) { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')}; } .action-button:disabled { opacity: 0.5; cursor: not-allowed; } .action-button.scanning dees-icon { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .dropdown-container { position: relative; } .dropdown-menu { position: absolute; bottom: 100%; left: 0; margin-bottom: 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: 200px; 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 dees-icon { opacity: 0.7; } .settings-section { background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; border-radius: 12px; padding: 20px; margin-bottom: 20px; } .settings-section-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .setting-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')}; } .setting-row:last-child { border-bottom: none; } .setting-label { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; } .setting-control select { padding: 8px 12px; font-size: 14px; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')}; border-radius: 6px; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')}; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')}; cursor: pointer; } .history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; } .history-item { background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.15s ease; } .history-item:hover { border-color: hsl(217 91% 60%); transform: translateY(-2px); box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; } .history-thumbnail { width: 100%; aspect-ratio: 1; background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')}; display: flex; align-items: center; justify-content: center; } .history-thumbnail img { width: 100%; height: 100%; object-fit: cover; } .history-info { padding: 10px; } .history-name { font-size: 13px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-date { font-size: 12px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .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; } `, ]; @property({ type: Array }) accessor scanners: IScannerInfo[] = []; @property({ type: Array }) accessor providers: IDataProviderInfo[] = []; @state() accessor activePanel: TScanPanel = 'scan'; @state() accessor selectedScannerId: string | null = null; @state() accessor isScanning = false; @state() accessor currentPreview: string | null = null; @state() accessor currentDocument: IScannedDocument | null = null; @state() accessor scanHistory: IScannedDocument[] = []; @state() accessor settings: IScanSettings = { format: 'pdf', resolution: 300, colorMode: 'color', source: 'flatbed', }; @state() accessor showSendToMenu = false; private get selectedScanner(): IScannerInfo | null { return this.scanners.find(s => s.id === this.selectedScannerId) || null; } private get availableResolutions(): number[] { return this.selectedScanner?.capabilities?.resolutions || [150, 300, 600]; } private get availableFormats(): TScanFormat[] { return this.selectedScanner?.capabilities?.formats || ['pdf', 'jpeg', 'png']; } private get availableColorModes(): TScanColorMode[] { return this.selectedScanner?.capabilities?.colorModes || ['color', 'grayscale']; } private get availableSources(): TScanSource[] { return this.selectedScanner?.capabilities?.sources || ['flatbed']; } public render(): TemplateResult { return html`
${this.renderContent()}
`; } private getMenuGroups(): ISecondaryMenuGroup[] { return [ { name: 'SCAN', iconName: 'lucide:scan', items: [ { key: 'scan', label: 'New Scan', iconName: 'lucide:plus', action: () => this.activePanel = 'scan', }, ], }, { name: 'HISTORY', iconName: 'lucide:history', items: [ { key: 'history', label: 'Recent Scans', iconName: 'lucide:clock', action: () => this.activePanel = 'history', badge: this.scanHistory.length > 0 ? this.scanHistory.length : undefined, }, ], }, { name: 'OPTIONS', iconName: 'lucide:settings', items: [ { key: 'settings', label: 'Scan Settings', iconName: 'lucide:sliders', action: () => this.activePanel = 'settings', }, ], }, ]; } private renderContent(): TemplateResult { switch (this.activePanel) { case 'scan': return this.renderScanPanel(); case 'history': return this.renderHistoryPanel(); case 'settings': return this.renderSettingsPanel(); default: return this.renderScanPanel(); } } private renderScanPanel(): TemplateResult { return html`

Scan Document

Select a scanner and start scanning

${this.currentPreview ? html`Scan preview` : html`
Scan preview will appear here
` }
`; } private renderSendToMenu(): TemplateResult { return html` `; } private renderHistoryPanel(): TemplateResult { return html`

Recent Scans

View and manage your scanned documents

${this.scanHistory.length > 0 ? html`
${this.scanHistory.map(doc => html`
this.handleHistoryItemClick(doc)}>
${doc.thumbnail ? html`${doc.name` : html`` }
${doc.name || `Scan ${doc.id.slice(0, 8)}`}
${this.formatDate(doc.timestamp)}
`)}
` : html`

No scans yet. Start scanning to see your documents here.

` } `; } private renderSettingsPanel(): TemplateResult { return html`

Scan Settings

Configure default scan options

Output Settings
Format
Resolution (DPI)
Color Mode
Source
`; } private handleScannerChange(e: Event): void { const select = e.target as HTMLSelectElement; this.selectedScannerId = select.value || null; this.dispatchEvent(new CustomEvent('scanner-select', { detail: { scannerId: this.selectedScannerId }, bubbles: true, composed: true, })); } private async handleScan(): Promise { if (!this.selectedScannerId || this.isScanning) return; this.isScanning = true; this.dispatchEvent(new CustomEvent('scan-request', { detail: { scannerId: this.selectedScannerId, settings: this.settings, }, bubbles: true, composed: true, })); } public setScanResult(result: { data: string; format: TScanFormat; thumbnail?: string }): void { const doc: IScannedDocument = { id: crypto.randomUUID(), timestamp: new Date(), format: result.format, data: result.data, thumbnail: result.thumbnail, size: result.data.length, }; this.currentDocument = doc; this.currentPreview = result.thumbnail || `data:image/${result.format};base64,${result.data}`; this.scanHistory = [doc, ...this.scanHistory.slice(0, 19)]; // Keep last 20 this.isScanning = false; } public setScanError(error: string): void { this.isScanning = false; console.error('Scan error:', error); // Could dispatch error event or show toast } private handleSaveLocal(): void { if (!this.currentDocument) return; this.dispatchEvent(new CustomEvent('save-local', { detail: { document: this.currentDocument }, bubbles: true, composed: true, })); } private handleSendToProvider(providerId: string): void { if (!this.currentDocument) return; this.showSendToMenu = false; this.dispatchEvent(new CustomEvent('send-to-provider', { detail: { providerId, document: this.currentDocument, }, bubbles: true, composed: true, })); } private handleHistoryItemClick(doc: IScannedDocument): void { this.currentDocument = doc; this.currentPreview = doc.thumbnail || `data:image/${doc.format};base64,${doc.data}`; this.activePanel = 'scan'; } private updateSetting(key: K, value: IScanSettings[K]): void { this.settings = { ...this.settings, [key]: value }; this.dispatchEvent(new CustomEvent('settings-change', { detail: { settings: this.settings }, bubbles: true, composed: true, })); } private getColorModeLabel(mode: TScanColorMode): string { const labels: Record = { color: 'Color', grayscale: 'Grayscale', blackwhite: 'Black & White', }; return labels[mode]; } private getSourceLabel(source: TScanSource): string { const labels: Record = { flatbed: 'Flatbed', adf: 'Document Feeder', 'adf-duplex': 'Document Feeder (Duplex)', }; return labels[source]; } private formatDate(date: Date): string { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }).format(date); } }