import { DeesElement, type TemplateResult, property, customElement, html, css, cssManager, state, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import '../../dees-icon/dees-icon.js'; import '@design.estate/dees-wcctools/demotools'; import type { IActivityEntry, IActivityLogAPI } from '../../interfaces/appconfig.js'; @customElement('dees-appui-activitylog') export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI { // STATIC public static demo = () => { // Create the activity log element const activityLog = document.createElement('dees-appui-activitylog') as DeesAppuiActivitylog; // Add demo entries after the element is connected setTimeout(() => { activityLog.addMany([ { type: 'login', user: 'John Doe', message: 'logged in from Chrome on macOS' }, { type: 'create', user: 'John Doe', message: 'created a new project "Frontend App"' }, { type: 'update', user: 'Jane Smith', message: 'updated API documentation' }, { type: 'view', user: 'John Doe', message: 'viewed dashboard analytics' }, { type: 'delete', user: 'Admin', message: 'removed deprecated endpoint' }, { type: 'custom', user: 'System', message: 'scheduled backup completed', iconName: 'lucide:database' }, { type: 'logout', user: 'Alice Brown', message: 'logged out' }, { type: 'create', user: 'Jane Smith', message: 'created invoice #1234' }, ]); // Subscribe to updates activityLog.entries$.subscribe((entries) => { console.log('Activity log updated:', entries.length, 'entries'); }); }, 100); return html`
${activityLog}
`; }; // INSTANCE PROPERTIES @state() accessor entries: IActivityEntry[] = []; @state() accessor searchQuery: string = ''; @state() accessor filterCriteria: { user?: string; type?: IActivityEntry['type'] } = {}; // RxJS Subject for reactive updates public entries$ = new domtools.plugins.smartrx.rxjs.Subject(); // STYLES public static styles = [ cssManager.defaultStyles, css` :host { color: ${cssManager.bdTheme('#09090b', '#fafafa')}; position: relative; display: block; width: 100%; max-width: 320px; height: 100%; background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; font-family: 'Geist Mono', monospace; border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; cursor: default; box-shadow: ${cssManager.bdTheme( '-4px 0 12px rgba(0, 0, 0, 0.02)', '-4px 0 12px rgba(0, 0, 0, 0.2)' )}; } .maincontainer { position: absolute; top: 0px; left: 0px; height: 100%; width: 100%; } .topbar { position: absolute; top: 0px; height: 48px; width: 100%; padding: 0px 16px; background: ${cssManager.bdTheme('#ffffff', '#09090b')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; display: flex; align-items: center; box-sizing: border-box; } .topbar .heading { font-weight: 600; font-size: 14px; font-family: 'Geist Sans', sans-serif; color: ${cssManager.bdTheme('#09090b', '#fafafa')}; } .activityContainer { position: absolute; top: 48px; bottom: 48px; width: 100%; padding: 12px 0px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: ${cssManager.bdTheme('#e5e7eb', '#27272a')} transparent; } .activityContainer::-webkit-scrollbar { width: 6px; } .activityContainer::-webkit-scrollbar-track { background: transparent; } .activityContainer::-webkit-scrollbar-thumb { background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; border-radius: 3px; } .activityContainer::-webkit-scrollbar-thumb:hover { background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; } .empty-state { font-size: 13px; text-align: center; padding: 32px 16px; color: ${cssManager.bdTheme('#71717a', '#71717a')}; font-family: 'Geist Sans', sans-serif; } .streamingIndicator { font-size: 11px; text-align: center; padding: 16px; color: ${cssManager.bdTheme('#71717a', '#71717a')}; font-family: 'Geist Sans', sans-serif; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 8px; } .streamingIndicator::before { content: ''; width: 6px; height: 6px; background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; border-radius: 50%; animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 0.4; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.2); } } .date-separator { padding: 12px 16px 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#71717a', '#71717a')}; background: ${cssManager.bdTheme('#f9fafb', '#09090b')}; border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')}; position: sticky; top: 0; z-index: 1; } .activityentry { min-height: 36px; font-size: 13px; padding: 10px 16px; border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')}; transition: all 0.15s ease; display: flex; align-items: center; gap: 8px; line-height: 1.4; animation: fadeIn 0.3s ease-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } .activityentry:last-of-type { border-bottom: none; } .activityentry:hover { background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; } .timestamp { color: ${cssManager.bdTheme('#71717a', '#71717a')}; font-weight: 500; font-size: 12px; font-variant-numeric: tabular-nums; flex-shrink: 0; min-width: 45px; } .activity-icon { width: 28px; height: 28px; border-radius: 6px; background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 14px; } .activity-icon.login { background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.1)')}; color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; } .activity-icon.logout { background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')}; color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; } .activity-icon.view { background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')}; color: ${cssManager.bdTheme('#2563eb', '#3b82f6')}; } .activity-icon.create { background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.1)')}; color: ${cssManager.bdTheme('#9333ea', '#a855f7')}; } .activity-icon.update { background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.1)', 'rgba(251, 146, 60, 0.1)')}; color: ${cssManager.bdTheme('#ea580c', '#fb923c')}; } .activity-icon.delete { background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')}; color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; } .activity-icon.custom { background: ${cssManager.bdTheme('rgba(100, 116, 139, 0.1)', 'rgba(100, 116, 139, 0.1)')}; color: ${cssManager.bdTheme('#475569', '#94a3b8')}; } .activity-text { flex: 1; color: ${cssManager.bdTheme('#18181b', '#e4e4e7')}; } .activity-user { font-weight: 600; color: ${cssManager.bdTheme('#09090b', '#fafafa')}; } .searchbox { position: absolute; bottom: 0px; width: 100%; height: 48px; background: ${cssManager.bdTheme('#ffffff', '#09090b')}; border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; padding: 8px; } .search-wrapper { position: relative; width: 100%; height: 32px; } .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: ${cssManager.bdTheme('#71717a', '#71717a')}; font-size: 14px; pointer-events: none; transition: color 0.15s ease; } .searchbox input { color: ${cssManager.bdTheme('#09090b', '#fafafa')}; background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; width: 100%; height: 100%; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; border-radius: 6px; padding: 0 12px 0 36px; font-family: 'Geist Sans', sans-serif; font-size: 13px; transition: all 0.15s ease; } .searchbox input::placeholder { color: ${cssManager.bdTheme('#71717a', '#71717a')}; } .searchbox input:focus { outline: none; border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')}; } .searchbox input:focus ~ .search-icon, .search-wrapper:has(input:focus) .search-icon { color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; } .bottomShadow { position: absolute; width: 100%; height: 24px; bottom: 48px; background: ${cssManager.bdTheme( 'linear-gradient(180deg, transparent 0%, #fafafa 100%)', 'linear-gradient(180deg, transparent 0%, #0a0a0a 100%)' )}; pointer-events: none; opacity: 0.8; } .topShadow { position: absolute; width: 100%; height: 24px; top: 48px; background: ${cssManager.bdTheme( 'linear-gradient(0deg, transparent 0%, #fafafa 100%)', 'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)' )}; pointer-events: none; opacity: 0.8; } `, ]; // RENDER public render(): TemplateResult { const filteredEntries = this.getFilteredEntries(); const groupedEntries = this.groupEntriesByDate(filteredEntries); return html` ${domtools.elementBasic.styles}
Activity Log
${filteredEntries.length > 0 ? html`
Live Updates
` : ''} ${filteredEntries.length === 0 ? html`
No activity entries
` : groupedEntries.map( (group) => html`
${group.label}
${group.entries.map((entry) => this.renderActivityEntry(entry))} ` )}
`; } private renderActivityEntry(entry: IActivityEntry): TemplateResult { const timestamp = entry.timestamp || new Date(); const timeStr = this.formatTime(timestamp); const iconName = entry.iconName || this.getIconForType(entry.type); return html`
this.handleContextMenu(e, entry)} > ${timeStr}
${entry.user} ${entry.message}
`; } // API METHODS public add(entry: IActivityEntry): void { const newEntry: IActivityEntry = { ...entry, id: entry.id || this.generateId(), timestamp: entry.timestamp || new Date(), }; this.entries = [newEntry, ...this.entries]; this.entries$.next(this.entries); } public addMany(entries: IActivityEntry[]): void { const newEntries = entries.map((entry) => ({ ...entry, id: entry.id || this.generateId(), timestamp: entry.timestamp || new Date(), })); this.entries = [...newEntries.reverse(), ...this.entries]; this.entries$.next(this.entries); } public clear(): void { this.entries = []; this.entries$.next(this.entries); } public getEntries(): IActivityEntry[] { return [...this.entries]; } public filter(criteria: { user?: string; type?: IActivityEntry['type'] }): IActivityEntry[] { return this.entries.filter((entry) => { if (criteria.user && entry.user !== criteria.user) return false; if (criteria.type && entry.type !== criteria.type) return false; return true; }); } public search(query: string): IActivityEntry[] { const lowerQuery = query.toLowerCase(); return this.entries.filter( (entry) => entry.message.toLowerCase().includes(lowerQuery) || entry.user.toLowerCase().includes(lowerQuery) ); } // PRIVATE HELPERS private generateId(): string { return `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } private getFilteredEntries(): IActivityEntry[] { let result = this.entries; if (this.searchQuery) { const lowerQuery = this.searchQuery.toLowerCase(); result = result.filter( (entry) => entry.message.toLowerCase().includes(lowerQuery) || entry.user.toLowerCase().includes(lowerQuery) ); } if (this.filterCriteria.user || this.filterCriteria.type) { result = result.filter((entry) => { if (this.filterCriteria.user && entry.user !== this.filterCriteria.user) return false; if (this.filterCriteria.type && entry.type !== this.filterCriteria.type) return false; return true; }); } return result; } private groupEntriesByDate( entries: IActivityEntry[] ): Array<{ label: string; entries: IActivityEntry[] }> { const groups: Map = new Map(); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); for (const entry of entries) { const date = entry.timestamp || new Date(); let label: string; if (this.isSameDay(date, today)) { label = 'Today'; } else if (this.isSameDay(date, yesterday)) { label = 'Yesterday'; } else { label = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined, }); } if (!groups.has(label)) { groups.set(label, []); } groups.get(label)!.push(entry); } return Array.from(groups.entries()).map(([label, entries]) => ({ label, entries, })); } private isSameDay(date1: Date, date2: Date): boolean { return ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ); } private formatTime(date: Date): string { return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, }); } private getIconForType(type: IActivityEntry['type']): string { const icons: Record = { login: 'lucide:logIn', logout: 'lucide:logOut', view: 'lucide:eye', create: 'lucide:plus', update: 'lucide:edit', delete: 'lucide:trash2', custom: 'lucide:activity', }; return icons[type] || icons.custom; } private handleSearchInput(e: InputEvent): void { const target = e.target as HTMLInputElement; this.searchQuery = target.value; } private handleContextMenu(e: MouseEvent, entry: IActivityEntry): void { e.preventDefault(); DeesContextmenu.openContextMenuWithOptions(e, [ { name: 'Copy activity', iconName: 'lucide:copy', action: async () => { await navigator.clipboard.writeText(`${entry.user} ${entry.message}`); }, }, { name: 'Filter by user', iconName: 'lucide:user', action: async () => { this.filterCriteria = { user: entry.user }; }, }, { name: 'Filter by type', iconName: 'lucide:filter', action: async () => { this.filterCriteria = { type: entry.type }; }, }, { name: 'Clear filters', iconName: 'lucide:x', action: async () => { this.filterCriteria = {}; this.searchQuery = ''; }, }, ]); } }