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, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js'; import { demo } from './eco-view-saasshare.demo.js'; // Ensure components are registered DeesAppuiSecondarymenu; DeesIcon; declare global { interface HTMLElementTagNameMap { 'eco-view-saasshare': EcoViewSaasshare; } } export type TSharePanel = | 'apps' | 'devices' | 'permissions' | 'requests' | 'activity' | 'security'; export type TPermissionType = | 'print' | 'scan' | 'storage' | 'camera' | 'audio' | 'display' | 'network'; export interface ISaasApp { id: string; name: string; domain: string; icon?: string; color?: string; verified: boolean; lastAccess?: Date; permissions: ISaasPermission[]; } export interface ISaasPermission { type: TPermissionType; deviceId?: string; deviceName?: string; granted: boolean; grantedAt?: Date; expiresAt?: Date; } export interface IAccessRequest { id: string; appId: string; appName: string; appDomain: string; permissionType: TPermissionType; deviceId?: string; deviceName?: string; requestedAt: Date; status: 'pending' | 'approved' | 'denied'; } export interface IAccessActivity { id: string; appId: string; appName: string; permissionType: TPermissionType; deviceName?: string; action: string; timestamp: Date; } @customElement('eco-view-saasshare') export class EcoViewSaasshare 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; } .share-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; } .panel-header { margin-bottom: 32px; display: flex; align-items: flex-start; justify-content: space-between; } .panel-header-left { flex: 1; } .panel-title { font-size: 28px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; margin-bottom: 8px; } .panel-description { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .header-action { display: flex; align-items: center; gap: 8px; padding: 10px 20px; background: hsl(217 91% 60%); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.15s ease; } .header-action:hover { background: hsl(217 91% 55%); } .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; margin-bottom: 24px; overflow: hidden; } .section-title { padding: 16px 20px 12px; font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; text-transform: uppercase; letter-spacing: 0.5px; display: flex; align-items: center; justify-content: space-between; } .section-count { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: 600; } .app-item { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; transition: background 0.15s ease; cursor: pointer; } .app-item:first-child { border-top: none; } .app-item:hover { background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')}; } .app-left { display: flex; align-items: center; gap: 14px; } .app-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: white; font-size: 20px; font-weight: 600; } .app-info { display: flex; flex-direction: column; gap: 4px; } .app-name { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; } .verified-badge { display: flex; align-items: center; gap: 4px; padding: 2px 8px; background: hsl(142 71% 45% / 0.15); color: hsl(142 71% 40%); border-radius: 4px; font-size: 11px; font-weight: 600; } .app-domain { font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .app-right { display: flex; align-items: center; gap: 16px; } .permission-badges { display: flex; gap: 6px; } .permission-badge { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 6px; background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')}; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .permission-badge.active { background: hsl(217 91% 60% / 0.15); color: hsl(217 91% 55%); } .last-access { font-size: 12px; color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')}; min-width: 100px; text-align: right; } .request-item { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; background: ${cssManager.bdTheme('hsl(45 100% 97%)', 'hsl(45 30% 12%)')}; } .request-item:first-child { border-top: none; } .request-info { display: flex; flex-direction: column; gap: 4px; } .request-title { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; } .request-detail { font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .request-actions { display: flex; gap: 8px; } .btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; } .btn-approve { background: hsl(142 71% 45%); color: white; } .btn-approve:hover { background: hsl(142 71% 40%); } .btn-deny { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; } .btn-deny:hover { background: hsl(0 72% 51%); color: white; } .device-group { margin-bottom: 24px; } .device-header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; } .device-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; } .device-info h4 { margin: 0; font-size: 15px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; } .device-info span { font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .device-apps { padding: 0; } .device-app { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px 12px 68px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; } .device-app:first-child { border-top: none; } .device-app-name { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; } .toggle-switch { position: relative; width: 44px; height: 24px; background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')}; border-radius: 12px; cursor: pointer; transition: background 0.2s ease; } .toggle-switch.active { background: hsl(217 91% 60%); } .toggle-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.2s ease; box-shadow: ${cssManager.bdTheme('0 1px 3px rgba(0,0,0,0.2)', 'none')}; } .toggle-switch.active::after { transform: translateX(20px); } .activity-item { display: flex; align-items: flex-start; gap: 14px; padding: 14px 20px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; } .activity-item:first-child { border-top: none; } .activity-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')}; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; flex-shrink: 0; } .activity-content { flex: 1; } .activity-title { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; margin-bottom: 2px; } .activity-title strong { font-weight: 600; } .activity-time { font-size: 12px; color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')}; } .settings-item { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; } .settings-item:first-child { border-top: none; } .item-left { display: flex; align-items: center; gap: 14px; } .item-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; } .item-icon.blue { background: hsl(217 91% 60%); } .item-icon.green { background: hsl(142 71% 45%); } .item-icon.orange { background: hsl(25 95% 53%); } .item-icon.red { background: hsl(0 72% 51%); } .item-icon.purple { background: hsl(262 83% 58%); } .item-icon.gray { background: hsl(220 9% 46%); } .item-info { display: flex; flex-direction: column; gap: 2px; } .item-label { font-size: 15px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; } .item-sublabel { font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .empty-state { text-align: center; padding: 48px 20px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .empty-state dees-icon { margin-bottom: 16px; opacity: 0.5; } .empty-state h3 { margin: 0 0 8px; font-size: 16px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')}; } .empty-state p { margin: 0; font-size: 14px; } .permission-type-row { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; } .permission-type-row:first-child { border-top: none; } .permission-type-info { display: flex; align-items: center; gap: 14px; } .permission-type-details h4 { margin: 0; font-size: 15px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; } .permission-type-details span { font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .permission-apps-count { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')}; border-radius: 6px; font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')}; cursor: pointer; transition: background 0.15s ease; } .permission-apps-count:hover { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 22%)')}; } `, ]; @property({ type: String }) accessor activePanel: TSharePanel = 'apps'; @state() accessor saasApps: ISaasApp[] = [ { id: 'google-docs', name: 'Google Docs', domain: 'docs.google.com', color: '#4285F4', verified: true, lastAccess: new Date(Date.now() - 1000 * 60 * 5), permissions: [ { type: 'print', deviceName: 'HP LaserJet Pro', granted: true }, { type: 'storage', deviceName: 'Synology NAS', granted: true }, ], }, { id: 'figma', name: 'Figma', domain: 'figma.com', color: '#F24E1E', verified: true, lastAccess: new Date(Date.now() - 1000 * 60 * 30), permissions: [ { type: 'display', granted: true }, ], }, { id: 'zoom', name: 'Zoom', domain: 'zoom.us', color: '#2D8CFF', verified: true, lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 2), permissions: [ { type: 'camera', deviceName: 'Logitech C920', granted: true }, { type: 'audio', deviceName: 'Built-in Microphone', granted: true }, { type: 'display', granted: true }, ], }, { id: 'notion', name: 'Notion', domain: 'notion.so', color: '#000000', verified: true, lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 24), permissions: [ { type: 'print', deviceName: 'HP LaserJet Pro', granted: true }, ], }, { id: 'dropbox', name: 'Dropbox', domain: 'dropbox.com', color: '#0061FF', verified: true, lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 48), permissions: [ { type: 'storage', deviceName: 'Synology NAS', granted: true }, { type: 'scan', deviceName: 'Epson Scanner', granted: true }, ], }, ]; @state() accessor accessRequests: IAccessRequest[] = [ { id: 'req-1', appId: 'slack', appName: 'Slack', appDomain: 'slack.com', permissionType: 'camera', deviceName: 'Logitech C920', requestedAt: new Date(Date.now() - 1000 * 60 * 10), status: 'pending', }, { id: 'req-2', appId: 'canva', appName: 'Canva', appDomain: 'canva.com', permissionType: 'print', deviceName: 'HP LaserJet Pro', requestedAt: new Date(Date.now() - 1000 * 60 * 25), status: 'pending', }, ]; @state() accessor activities: IAccessActivity[] = [ { id: 'act-1', appId: 'google-docs', appName: 'Google Docs', permissionType: 'print', deviceName: 'HP LaserJet Pro', action: 'printed document "Q4 Report.pdf"', timestamp: new Date(Date.now() - 1000 * 60 * 5), }, { id: 'act-2', appId: 'zoom', appName: 'Zoom', permissionType: 'camera', deviceName: 'Logitech C920', action: 'accessed camera for video call', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), }, { id: 'act-3', appId: 'dropbox', appName: 'Dropbox', permissionType: 'scan', deviceName: 'Epson Scanner', action: 'scanned 3 pages', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5), }, { id: 'act-4', appId: 'figma', appName: 'Figma', permissionType: 'display', action: 'shared screen to external display', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), }, ]; @state() accessor requireApproval = true; @state() accessor autoRevokeInactive = true; @state() accessor activityLogging = true; private getMenuGroups(): ISecondaryMenuGroup[] { const pendingCount = this.accessRequests.filter(r => r.status === 'pending').length; return [ { name: 'Overview', iconName: 'lucide:share2', items: [ { key: 'apps', iconName: 'lucide:layoutGrid', action: () => this.activePanel = 'apps', badge: this.saasApps.length.toString(), }, { key: 'requests', iconName: 'lucide:inbox', action: () => this.activePanel = 'requests', badge: pendingCount > 0 ? pendingCount.toString() : undefined, }, ], }, { name: 'Browse', iconName: 'lucide:folder', items: [ { key: 'devices', iconName: 'lucide:hardDrive', action: () => this.activePanel = 'devices', }, { key: 'permissions', iconName: 'lucide:key', action: () => this.activePanel = 'permissions', }, ], }, { name: 'Monitor', iconName: 'lucide:activity', items: [ { key: 'activity', iconName: 'lucide:clock', action: () => this.activePanel = 'activity', }, { key: 'security', iconName: 'lucide:shield', action: () => this.activePanel = 'security', }, ], }, ]; } private getSelectedItem(): ISecondaryMenuItem | null { for (const group of this.getMenuGroups()) { for (const item of group.items) { if ('key' in item && item.key === this.activePanel) { return item; } } } return null; } public render(): TemplateResult { return html`
${this.renderActivePanel()}
`; } private renderActivePanel(): TemplateResult { switch (this.activePanel) { case 'apps': return this.renderAppsPanel(); case 'devices': return this.renderDevicesPanel(); case 'permissions': return this.renderPermissionsPanel(); case 'requests': return this.renderRequestsPanel(); case 'activity': return this.renderActivityPanel(); case 'security': return this.renderSecurityPanel(); default: return this.renderAppsPanel(); } } private renderAppsPanel(): TemplateResult { return html`
Connected Apps
Manage SaaS applications with access to your peripherals
${this.accessRequests.filter(r => r.status === 'pending').length > 0 ? html`
Pending Requests ${this.accessRequests.filter(r => r.status === 'pending').length}
${this.accessRequests.filter(r => r.status === 'pending').map(request => html`
${request.appName} wants access to ${this.getPermissionLabel(request.permissionType)}
${request.deviceName ? `Device: ${request.deviceName} • ` : ''} Requested ${this.formatTimeAgo(request.requestedAt)}
`)}
` : ''}
All Apps ${this.saasApps.length}
${this.saasApps.map(app => html`
${app.name.charAt(0)}
${app.name} ${app.verified ? html` Verified ` : ''}
${app.domain}
${this.renderPermissionBadges(app.permissions)}
${app.lastAccess ? this.formatTimeAgo(app.lastAccess) : 'Never'}
`)}
`; } private renderDevicesPanel(): TemplateResult { const devices = [ { name: 'HP LaserJet Pro', type: 'print', icon: 'lucide:printer', apps: ['Google Docs', 'Notion'], }, { name: 'Epson Scanner', type: 'scan', icon: 'lucide:scan', apps: ['Dropbox'], }, { name: 'Logitech C920', type: 'camera', icon: 'lucide:camera', apps: ['Zoom'], }, { name: 'Built-in Microphone', type: 'audio', icon: 'lucide:mic', apps: ['Zoom'], }, { name: 'Synology NAS', type: 'storage', icon: 'lucide:hardDrive', apps: ['Google Docs', 'Dropbox'], }, { name: 'External Display', type: 'display', icon: 'lucide:monitor', apps: ['Zoom', 'Figma'], }, ]; return html`
Devices
View which apps have access to each peripheral
${devices.map(device => html`

${device.name}

${device.apps.length} app${device.apps.length !== 1 ? 's' : ''} with access
${device.apps.map(appName => html`
${appName}
`)}
`)} `; } private renderPermissionsPanel(): TemplateResult { const permissionTypes: { type: TPermissionType; icon: string; label: string; color: string; count: number }[] = [ { type: 'print', icon: 'lucide:printer', label: 'Printing', color: 'blue', count: 2 }, { type: 'scan', icon: 'lucide:scan', label: 'Scanning', color: 'purple', count: 1 }, { type: 'camera', icon: 'lucide:camera', label: 'Camera', color: 'green', count: 1 }, { type: 'audio', icon: 'lucide:mic', label: 'Microphone', color: 'red', count: 1 }, { type: 'storage', icon: 'lucide:hardDrive', label: 'Network Storage', color: 'orange', count: 2 }, { type: 'display', icon: 'lucide:monitor', label: 'Screen Sharing', color: 'gray', count: 2 }, ]; return html`
Permissions
Manage access by permission type
${permissionTypes.map(perm => html`

${perm.label}

${perm.count} app${perm.count !== 1 ? 's' : ''} with access
Manage
`)}
`; } private renderRequestsPanel(): TemplateResult { const pendingRequests = this.accessRequests.filter(r => r.status === 'pending'); return html`
Access Requests
Review and manage permission requests from SaaS applications
${pendingRequests.length > 0 ? html`
Pending ${pendingRequests.length}
${pendingRequests.map(request => html`
${request.appName} wants access to ${this.getPermissionLabel(request.permissionType)}
${request.deviceName ? `Device: ${request.deviceName} • ` : ''} ${request.appDomain} • Requested ${this.formatTimeAgo(request.requestedAt)}
`)}
` : html`

No pending requests

When SaaS apps request access to your peripherals, they'll appear here

`} `; } private renderActivityPanel(): TemplateResult { return html`
Activity Log
Recent peripheral access by SaaS applications
Recent Activity
${this.activities.map(activity => html`
${activity.appName} ${activity.action}
${this.formatTimeAgo(activity.timestamp)}
`)}
`; } private renderSecurityPanel(): TemplateResult { return html`
Security Settings
Configure how SaaS apps can access your peripherals
Access Control
Require Approval
Ask before granting new app access
this.requireApproval = !this.requireApproval} >
Auto-revoke Inactive Apps
Remove access after 30 days of inactivity
this.autoRevokeInactive = !this.autoRevokeInactive} >
Logging
Activity Logging
Record all peripheral access events
this.activityLogging = !this.activityLogging} >
Danger Zone
Revoke All Access
Remove all SaaS app permissions
`; } private renderPermissionBadges(permissions: ISaasPermission[]): TemplateResult[] { const permissionIcons: Record = { print: 'lucide:printer', scan: 'lucide:scan', storage: 'lucide:hardDrive', camera: 'lucide:camera', audio: 'lucide:mic', display: 'lucide:monitor', network: 'lucide:wifi', }; return permissions.map(perm => html`
`); } private getPermissionLabel(type: TPermissionType): string { const labels: Record = { print: 'Printing', scan: 'Scanning', storage: 'Storage', camera: 'Camera', audio: 'Microphone', display: 'Display', network: 'Network', }; return labels[type]; } private getPermissionIcon(type: TPermissionType): string { const icons: Record = { print: 'lucide:printer', scan: 'lucide:scan', storage: 'lucide:hardDrive', camera: 'lucide:camera', audio: 'lucide:mic', display: 'lucide:monitor', network: 'lucide:wifi', }; return icons[type]; } private formatTimeAgo(date: Date): string { const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; if (days < 7) return `${days}d ago`; return date.toLocaleDateString(); } private approveRequest(requestId: string): void { this.accessRequests = this.accessRequests.map(r => r.id === requestId ? { ...r, status: 'approved' as const } : r ); this.dispatchEvent(new CustomEvent('request-approved', { detail: { requestId }, bubbles: true, composed: true, })); } private denyRequest(requestId: string): void { this.accessRequests = this.accessRequests.map(r => r.id === requestId ? { ...r, status: 'denied' as const } : r ); this.dispatchEvent(new CustomEvent('request-denied', { detail: { requestId }, bubbles: true, composed: true, })); } }