tacel-canva 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/canva.js ADDED
@@ -0,0 +1,2910 @@
1
+ /**
2
+ * Tacel Canva Module
3
+ * Universal Canva integration for Electron apps
4
+ *
5
+ * Features:
6
+ * - OAuth 2.0 authentication with PKCE
7
+ * - Design gallery with thumbnails
8
+ * - Export/download designs
9
+ * - Folder navigation
10
+ * - Search functionality
11
+ */
12
+
13
+ let DomUtils, PKCEUtils;
14
+ if (typeof require === 'function') {
15
+ DomUtils = require('./utils/dom');
16
+ PKCEUtils = require('./utils/pkce');
17
+ }
18
+
19
+ const DEFAULT_CONFIG = {
20
+ features: {
21
+ export: true,
22
+ createNew: true,
23
+ folders: true,
24
+ search: true,
25
+ preview: true,
26
+ sorting: true,
27
+ viewToggle: true,
28
+ contextMenu: true,
29
+ dragDrop: true,
30
+ favorites: true,
31
+ recentlyViewed: true,
32
+ bulkActions: true,
33
+ collections: true,
34
+ themePicker: true,
35
+ // New features
36
+ brandKit: true, // 1. Quick Brand Kit Access
37
+ templates: true, // 2. Design Templates Browser
38
+ scheduledExports: true, // 3. Scheduled Publishing Queue
39
+ annotations: true, // 4. Design Annotations & Notes
40
+ quickInsert: true, // 5. Quick Insert to Documents
41
+ compareView: true, // 6. Design Comparison View
42
+ assetLibrary: true, // 7. Asset Library Sync
43
+ analytics: true, // 8. Design Analytics Dashboard
44
+ batchExport: true, // 9. Batch Export with Presets
45
+ designRequests: true, // 10. Design Request System
46
+ },
47
+ defaultView: 'grid',
48
+ sortBy: 'updated_at',
49
+ sortOrder: 'desc',
50
+ theme: 'default',
51
+ channelPrefix: 'canva',
52
+ pageSize: 50,
53
+ };
54
+
55
+ class TacelCanvaInstance {
56
+ constructor(container, config) {
57
+ this._container = container;
58
+ this._config = { ...DEFAULT_CONFIG, ...config };
59
+ this._config.features = { ...DEFAULT_CONFIG.features, ...(config.features || {}) };
60
+
61
+ this._state = {
62
+ isAuthenticated: false,
63
+ user: null,
64
+ designs: [],
65
+ folders: [],
66
+ collections: [],
67
+ currentFolder: null,
68
+ currentCollection: null,
69
+ searchQuery: '',
70
+ isLoading: false,
71
+ authState: null,
72
+ viewMode: this._config.defaultView || 'grid',
73
+ sortBy: this._config.sortBy || 'updated_at',
74
+ sortOrder: this._config.sortOrder || 'desc',
75
+ selectedDesigns: new Set(),
76
+ favorites: new Set(),
77
+ recentlyViewed: [],
78
+ previewDesign: null,
79
+ contextMenuTarget: null,
80
+ filterType: 'all',
81
+ continuation: null,
82
+ hasMore: false,
83
+ currentTheme: this._config.theme || 'default',
84
+ showCreateCollection: false,
85
+ // New feature states
86
+ currentView: 'gallery', // gallery, brandKit, templates, scheduled, analytics, requests, compare, assets
87
+ brandKit: null,
88
+ templates: [],
89
+ templateCategories: [],
90
+ scheduledExports: [],
91
+ annotations: {}, // designId -> { notes, tags, linkedTaskId }
92
+ assetLibrary: [], // designIds marked as assets
93
+ analytics: { views: {}, exports: {}, lastUsed: {} },
94
+ exportPresets: [],
95
+ designRequests: [],
96
+ compareDesigns: [], // designs selected for comparison
97
+ };
98
+
99
+ this._validateConfig();
100
+ this._init();
101
+ }
102
+
103
+ _validateConfig() {
104
+ if (!this._config.currentUserId) {
105
+ throw new Error('TacelCanva: currentUserId is required');
106
+ }
107
+ if (!this._config.onGetToken || typeof this._config.onGetToken !== 'function') {
108
+ throw new Error('TacelCanva: onGetToken callback is required');
109
+ }
110
+ if (!this._config.onSaveToken || typeof this._config.onSaveToken !== 'function') {
111
+ throw new Error('TacelCanva: onSaveToken callback is required');
112
+ }
113
+ }
114
+
115
+ async _init() {
116
+ this._container.classList.add('tacel-canva');
117
+ this._initKeyboardShortcuts();
118
+ this._render();
119
+ await this._checkAuthStatus();
120
+ }
121
+
122
+ async _checkAuthStatus() {
123
+ try {
124
+ const tokens = await this._config.onGetToken(this._config.currentUserId);
125
+ if (tokens && tokens.access_token) {
126
+ this._state.isAuthenticated = true;
127
+ await this._loadDesigns();
128
+ }
129
+ } catch (err) {
130
+ console.error('[TacelCanva] Auth check failed:', err);
131
+ }
132
+ this._render();
133
+ }
134
+
135
+ _render() {
136
+ DomUtils.clear(this._container);
137
+
138
+ if (!this._state.isAuthenticated) {
139
+ this._renderAuthScreen();
140
+ } else {
141
+ this._renderMainScreen();
142
+ }
143
+ }
144
+
145
+ _renderAuthScreen() {
146
+ const authScreen = DomUtils.el('div', { className: 'tc-auth-screen' });
147
+
148
+ const logo = DomUtils.el('div', { className: 'tc-auth-logo' });
149
+ logo.innerHTML = `
150
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
151
+ <rect width="48" height="48" rx="8" fill="#00C4CC"/>
152
+ <path d="M24 12L12 24L24 36L36 24L24 12Z" fill="white"/>
153
+ </svg>
154
+ `;
155
+
156
+ const title = DomUtils.el('h2', {
157
+ className: 'tc-auth-title',
158
+ text: 'Connect to Canva'
159
+ });
160
+
161
+ const desc = DomUtils.el('p', {
162
+ className: 'tc-auth-desc',
163
+ text: 'Access your Canva designs, export files, and manage your creative work.'
164
+ });
165
+
166
+ const connectBtn = DomUtils.el('button', {
167
+ className: 'tc-btn tc-btn-primary',
168
+ text: 'Connect Canva Account'
169
+ });
170
+ connectBtn.addEventListener('click', () => this._startOAuthFlow());
171
+
172
+ authScreen.appendChild(logo);
173
+ authScreen.appendChild(title);
174
+ authScreen.appendChild(desc);
175
+ authScreen.appendChild(connectBtn);
176
+
177
+ this._container.appendChild(authScreen);
178
+ }
179
+
180
+ _renderMainScreen() {
181
+ const wrapper = DomUtils.el('div', { className: 'tc-main-wrapper' });
182
+
183
+ // Sidebar with folders
184
+ if (this._config.features.folders) {
185
+ const sidebar = this._renderSidebar();
186
+ wrapper.appendChild(sidebar);
187
+ }
188
+
189
+ const mainArea = DomUtils.el('div', { className: 'tc-main-area' });
190
+
191
+ const header = this._renderHeader();
192
+ const toolbar = this._renderToolbar();
193
+ const content = this._renderContent();
194
+
195
+ mainArea.appendChild(header);
196
+ mainArea.appendChild(toolbar);
197
+ mainArea.appendChild(content);
198
+
199
+ wrapper.appendChild(mainArea);
200
+ this._container.appendChild(wrapper);
201
+
202
+ // Preview modal
203
+ if (this._state.previewDesign) {
204
+ this._renderPreviewModal();
205
+ }
206
+
207
+ // Context menu
208
+ if (this._state.contextMenuTarget) {
209
+ this._renderContextMenu();
210
+ }
211
+ }
212
+
213
+ _renderSidebar() {
214
+ const sidebar = DomUtils.el('div', { className: 'tc-sidebar' });
215
+
216
+ const sidebarHeader = DomUtils.el('div', { className: 'tc-sidebar-header' });
217
+ sidebarHeader.innerHTML = `<span class="tc-sidebar-title">Canva Hub</span>`;
218
+ sidebar.appendChild(sidebarHeader);
219
+
220
+ const nav = DomUtils.el('nav', { className: 'tc-sidebar-nav' });
221
+
222
+ // Main navigation sections
223
+ const mainNav = [
224
+ { id: 'gallery', view: 'gallery', icon: '🎨', label: 'My Designs' },
225
+ { id: 'brandKit', view: 'brandKit', icon: '🏷️', label: 'Brand Kit', feature: 'brandKit' },
226
+ { id: 'templates', view: 'templates', icon: '📋', label: 'Templates', feature: 'templates' },
227
+ { id: 'assets', view: 'assets', icon: '📦', label: 'Asset Library', feature: 'assetLibrary' },
228
+ ];
229
+
230
+ mainNav.forEach(item => {
231
+ if (item.feature && !this._config.features[item.feature]) return;
232
+ const navItem = DomUtils.el('div', {
233
+ className: `tc-sidebar-item ${this._state.currentView === item.view ? 'active' : ''}`,
234
+ });
235
+ navItem.innerHTML = `
236
+ <span class="tc-sidebar-icon">${item.icon}</span>
237
+ <span class="tc-sidebar-label">${item.label}</span>
238
+ `;
239
+ navItem.addEventListener('click', () => {
240
+ this._state.currentView = item.view;
241
+ this._state.filterType = 'all';
242
+ this._render();
243
+ });
244
+ nav.appendChild(navItem);
245
+ });
246
+
247
+ // Divider
248
+ nav.appendChild(DomUtils.el('div', { className: 'tc-sidebar-divider' }));
249
+
250
+ // Quick filters (only show in gallery view)
251
+ const filtersHeader = DomUtils.el('div', { className: 'tc-sidebar-section-header', text: 'Quick Filters' });
252
+ nav.appendChild(filtersHeader);
253
+
254
+ const filters = [
255
+ { id: 'all', icon: '📁', label: 'All Designs', count: this._state.designs.length },
256
+ { id: 'recent', icon: '🕐', label: 'Recently Viewed', count: this._state.recentlyViewed.length },
257
+ { id: 'favorites', icon: '⭐', label: 'Favorites', count: this._state.favorites.size },
258
+ ];
259
+
260
+ filters.forEach(filter => {
261
+ const item = DomUtils.el('div', {
262
+ className: `tc-sidebar-item ${this._state.filterType === filter.id ? 'active' : ''}`,
263
+ });
264
+ item.innerHTML = `
265
+ <span class="tc-sidebar-icon">${filter.icon}</span>
266
+ <span class="tc-sidebar-label">${filter.label}</span>
267
+ <span class="tc-sidebar-count">${filter.count}</span>
268
+ `;
269
+ item.addEventListener('click', () => {
270
+ this._state.filterType = filter.id;
271
+ this._state.currentFolder = null;
272
+ this._state.currentCollection = null;
273
+ this._render();
274
+ });
275
+ nav.appendChild(item);
276
+ });
277
+
278
+ // Custom Collections section
279
+ if (this._config.features.collections) {
280
+ const collectionsHeader = DomUtils.el('div', { className: 'tc-sidebar-section-header' });
281
+ collectionsHeader.innerHTML = `
282
+ <span>Collections</span>
283
+ <button class="tc-sidebar-add-btn" title="Create Collection">+</button>
284
+ `;
285
+ collectionsHeader.querySelector('.tc-sidebar-add-btn').addEventListener('click', (e) => {
286
+ e.stopPropagation();
287
+ this._showCreateCollectionModal();
288
+ });
289
+ nav.appendChild(collectionsHeader);
290
+
291
+ // Render collections
292
+ this._state.collections.forEach(collection => {
293
+ const count = collection.designIds ? collection.designIds.length : 0;
294
+ const item = DomUtils.el('div', {
295
+ className: `tc-sidebar-item ${this._state.currentCollection === collection.id ? 'active' : ''}`,
296
+ });
297
+ item.innerHTML = `
298
+ <span class="tc-sidebar-icon" style="color: ${collection.color || '#6b7280'}">●</span>
299
+ <span class="tc-sidebar-label">${collection.name}</span>
300
+ <span class="tc-sidebar-count">${count}</span>
301
+ `;
302
+ item.addEventListener('click', () => {
303
+ this._state.currentCollection = collection.id;
304
+ this._state.filterType = 'collection';
305
+ this._state.currentFolder = null;
306
+ this._render();
307
+ });
308
+ item.addEventListener('contextmenu', (e) => {
309
+ e.preventDefault();
310
+ this._showCollectionContextMenu(e, collection);
311
+ });
312
+ nav.appendChild(item);
313
+ });
314
+
315
+ if (this._state.collections.length === 0) {
316
+ const emptyHint = DomUtils.el('div', { className: 'tc-sidebar-hint', text: 'No collections yet' });
317
+ nav.appendChild(emptyHint);
318
+ }
319
+ }
320
+
321
+ // Canva Folders section
322
+ if (this._state.folders.length > 0) {
323
+ const foldersHeader = DomUtils.el('div', { className: 'tc-sidebar-section-header', text: 'Canva Folders' });
324
+ nav.appendChild(foldersHeader);
325
+
326
+ this._state.folders.forEach(folder => {
327
+ const item = DomUtils.el('div', {
328
+ className: `tc-sidebar-item ${this._state.currentFolder === folder.id ? 'active' : ''}`,
329
+ });
330
+ item.innerHTML = `
331
+ <span class="tc-sidebar-icon">📂</span>
332
+ <span class="tc-sidebar-label">${folder.name}</span>
333
+ `;
334
+ item.addEventListener('click', () => {
335
+ this._state.currentFolder = folder.id;
336
+ this._state.filterType = 'folder';
337
+ this._state.currentCollection = null;
338
+ this._loadDesigns();
339
+ });
340
+ nav.appendChild(item);
341
+ });
342
+ }
343
+
344
+ // Tools section
345
+ nav.appendChild(DomUtils.el('div', { className: 'tc-sidebar-divider' }));
346
+ const toolsHeader = DomUtils.el('div', { className: 'tc-sidebar-section-header', text: 'Tools' });
347
+ nav.appendChild(toolsHeader);
348
+
349
+ const tools = [
350
+ { id: 'compare', view: 'compare', icon: '⚖️', label: 'Compare Designs', feature: 'compareView', badge: this._state.compareDesigns.length },
351
+ { id: 'scheduled', view: 'scheduled', icon: '📅', label: 'Scheduled Exports', feature: 'scheduledExports', badge: this._state.scheduledExports.length },
352
+ { id: 'analytics', view: 'analytics', icon: '📊', label: 'Analytics', feature: 'analytics' },
353
+ { id: 'requests', view: 'requests', icon: '📝', label: 'Design Requests', feature: 'designRequests', badge: this._state.designRequests.filter(r => r.status === 'pending').length },
354
+ ];
355
+
356
+ tools.forEach(tool => {
357
+ if (tool.feature && !this._config.features[tool.feature]) return;
358
+ const toolItem = DomUtils.el('div', {
359
+ className: `tc-sidebar-item ${this._state.currentView === tool.view ? 'active' : ''}`,
360
+ });
361
+ toolItem.innerHTML = `
362
+ <span class="tc-sidebar-icon">${tool.icon}</span>
363
+ <span class="tc-sidebar-label">${tool.label}</span>
364
+ ${tool.badge ? `<span class="tc-sidebar-badge">${tool.badge}</span>` : ''}
365
+ `;
366
+ toolItem.addEventListener('click', () => {
367
+ this._state.currentView = tool.view;
368
+ this._render();
369
+ });
370
+ nav.appendChild(toolItem);
371
+ });
372
+
373
+ sidebar.appendChild(nav);
374
+ return sidebar;
375
+ }
376
+
377
+ _renderHeader() {
378
+ const header = DomUtils.el('div', { className: 'tc-header' });
379
+
380
+ const title = DomUtils.el('h2', {
381
+ className: 'tc-header-title',
382
+ text: 'Canva Designs'
383
+ });
384
+
385
+ const rightSection = DomUtils.el('div', { className: 'tc-header-right' });
386
+
387
+ // Theme picker
388
+ if (this._config.features.themePicker) {
389
+ const themePicker = this._renderThemePicker();
390
+ rightSection.appendChild(themePicker);
391
+ }
392
+
393
+ const userInfo = DomUtils.el('div', { className: 'tc-user-info' });
394
+ if (this._state.user) {
395
+ const avatar = DomUtils.el('div', { className: 'tc-user-avatar' });
396
+ avatar.textContent = (this._state.user.email || 'U').charAt(0).toUpperCase();
397
+ userInfo.appendChild(avatar);
398
+
399
+ const email = DomUtils.el('span', { text: this._state.user.email || 'Connected' });
400
+ userInfo.appendChild(email);
401
+ }
402
+ rightSection.appendChild(userInfo);
403
+
404
+ const disconnectBtn = DomUtils.el('button', {
405
+ className: 'tc-btn tc-btn-secondary tc-btn-small',
406
+ text: 'Disconnect'
407
+ });
408
+ disconnectBtn.addEventListener('click', () => this._disconnect());
409
+ rightSection.appendChild(disconnectBtn);
410
+
411
+ header.appendChild(title);
412
+ header.appendChild(rightSection);
413
+
414
+ return header;
415
+ }
416
+
417
+ _renderToolbar() {
418
+ const toolbar = DomUtils.el('div', { className: 'tc-toolbar' });
419
+
420
+ const leftSection = DomUtils.el('div', { className: 'tc-toolbar-left' });
421
+ const rightSection = DomUtils.el('div', { className: 'tc-toolbar-right' });
422
+
423
+ // Search
424
+ if (this._config.features.search) {
425
+ const searchWrapper = DomUtils.el('div', { className: 'tc-search-wrapper' });
426
+ searchWrapper.innerHTML = `<span class="tc-search-icon">🔍</span>`;
427
+ const searchInput = DomUtils.el('input', {
428
+ className: 'tc-search-input',
429
+ attrs: { type: 'text', placeholder: 'Search designs...' }
430
+ });
431
+ searchInput.value = this._state.searchQuery;
432
+ searchInput.addEventListener('input', (e) => {
433
+ this._state.searchQuery = e.target.value;
434
+ this._filterDesigns();
435
+ });
436
+ searchWrapper.appendChild(searchInput);
437
+ leftSection.appendChild(searchWrapper);
438
+ }
439
+
440
+ // Bulk actions (when items selected)
441
+ if (this._config.features.bulkActions && this._state.selectedDesigns.size > 0) {
442
+ const bulkActions = DomUtils.el('div', { className: 'tc-bulk-actions' });
443
+ bulkActions.innerHTML = `
444
+ <span class="tc-bulk-count">${this._state.selectedDesigns.size} selected</span>
445
+ <button class="tc-btn tc-btn-small" data-action="export">Export All</button>
446
+ <button class="tc-btn tc-btn-small" data-action="favorite">Add to Favorites</button>
447
+ <button class="tc-btn tc-btn-small tc-btn-ghost" data-action="clear">Clear</button>
448
+ `;
449
+ bulkActions.addEventListener('click', (e) => {
450
+ const action = e.target.dataset.action;
451
+ if (action === 'export') this._bulkExport();
452
+ else if (action === 'favorite') this._bulkFavorite();
453
+ else if (action === 'clear') { this._state.selectedDesigns.clear(); this._render(); }
454
+ });
455
+ leftSection.appendChild(bulkActions);
456
+ }
457
+
458
+ // Sorting
459
+ if (this._config.features.sorting) {
460
+ const sortWrapper = DomUtils.el('div', { className: 'tc-sort-wrapper' });
461
+ const sortSelect = DomUtils.el('select', { className: 'tc-sort-select' });
462
+ const sortOptions = [
463
+ { value: 'updated_at', label: 'Last Modified' },
464
+ { value: 'created_at', label: 'Date Created' },
465
+ { value: 'title', label: 'Name' },
466
+ ];
467
+ sortOptions.forEach(opt => {
468
+ const option = DomUtils.el('option', { text: opt.label, attrs: { value: opt.value } });
469
+ if (this._state.sortBy === opt.value) option.selected = true;
470
+ sortSelect.appendChild(option);
471
+ });
472
+ sortSelect.addEventListener('change', (e) => {
473
+ this._state.sortBy = e.target.value;
474
+ this._render();
475
+ });
476
+
477
+ const sortOrderBtn = DomUtils.el('button', {
478
+ className: 'tc-btn tc-btn-icon',
479
+ text: this._state.sortOrder === 'desc' ? '↓' : '↑'
480
+ });
481
+ sortOrderBtn.addEventListener('click', () => {
482
+ this._state.sortOrder = this._state.sortOrder === 'desc' ? 'asc' : 'desc';
483
+ this._render();
484
+ });
485
+
486
+ sortWrapper.appendChild(sortSelect);
487
+ sortWrapper.appendChild(sortOrderBtn);
488
+ rightSection.appendChild(sortWrapper);
489
+ }
490
+
491
+ // View toggle
492
+ if (this._config.features.viewToggle) {
493
+ const viewToggle = DomUtils.el('div', { className: 'tc-view-toggle' });
494
+ const gridBtn = DomUtils.el('button', {
495
+ className: `tc-btn tc-btn-icon ${this._state.viewMode === 'grid' ? 'active' : ''}`,
496
+ attrs: { title: 'Grid view' }
497
+ });
498
+ gridBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>`;
499
+ gridBtn.addEventListener('click', () => { this._state.viewMode = 'grid'; this._render(); });
500
+
501
+ const listBtn = DomUtils.el('button', {
502
+ className: `tc-btn tc-btn-icon ${this._state.viewMode === 'list' ? 'active' : ''}`,
503
+ attrs: { title: 'List view' }
504
+ });
505
+ listBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="2" width="14" height="3" rx="1"/><rect x="1" y="7" width="14" height="3" rx="1"/><rect x="1" y="12" width="14" height="3" rx="1"/></svg>`;
506
+ listBtn.addEventListener('click', () => { this._state.viewMode = 'list'; this._render(); });
507
+
508
+ viewToggle.appendChild(gridBtn);
509
+ viewToggle.appendChild(listBtn);
510
+ rightSection.appendChild(viewToggle);
511
+ }
512
+
513
+ // Create new
514
+ if (this._config.features.createNew) {
515
+ const createBtn = DomUtils.el('button', { className: 'tc-btn tc-btn-primary' });
516
+ createBtn.innerHTML = `<span class="tc-btn-icon-left">+</span> Create New`;
517
+ createBtn.addEventListener('click', () => this._createNewDesign());
518
+ rightSection.appendChild(createBtn);
519
+ }
520
+
521
+ // Refresh
522
+ const refreshBtn = DomUtils.el('button', { className: 'tc-btn tc-btn-icon', attrs: { title: 'Refresh' } });
523
+ refreshBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M13.65 2.35A8 8 0 1 0 16 8h-2a6 6 0 1 1-1.76-4.24L10 6h6V0l-2.35 2.35z"/></svg>`;
524
+ refreshBtn.addEventListener('click', () => this._loadDesigns());
525
+ rightSection.appendChild(refreshBtn);
526
+
527
+ toolbar.appendChild(leftSection);
528
+ toolbar.appendChild(rightSection);
529
+
530
+ return toolbar;
531
+ }
532
+
533
+ _renderContent() {
534
+ const content = DomUtils.el('div', { className: 'tc-content' });
535
+
536
+ if (this._state.isLoading) {
537
+ const loader = DomUtils.el('div', { className: 'tc-loader' });
538
+ loader.innerHTML = `
539
+ <div class="tc-loader-spinner"></div>
540
+ <p>Loading your designs...</p>
541
+ `;
542
+ content.appendChild(loader);
543
+ return content;
544
+ }
545
+
546
+ // Route to different views based on currentView
547
+ switch (this._state.currentView) {
548
+ case 'brandKit':
549
+ content.appendChild(this._renderBrandKitView());
550
+ return content;
551
+ case 'templates':
552
+ content.appendChild(this._renderTemplatesView());
553
+ return content;
554
+ case 'assets':
555
+ content.appendChild(this._renderAssetLibraryView());
556
+ return content;
557
+ case 'compare':
558
+ content.appendChild(this._renderCompareView());
559
+ return content;
560
+ case 'scheduled':
561
+ content.appendChild(this._renderScheduledView());
562
+ return content;
563
+ case 'analytics':
564
+ content.appendChild(this._renderAnalyticsView());
565
+ return content;
566
+ case 'requests':
567
+ content.appendChild(this._renderRequestsView());
568
+ return content;
569
+ case 'gallery':
570
+ default:
571
+ break;
572
+ }
573
+
574
+ // Gallery view (default)
575
+ const designs = this._getSortedFilteredDesigns();
576
+
577
+ if (designs.length === 0) {
578
+ content.appendChild(this._renderEmptyState());
579
+ return content;
580
+ }
581
+
582
+ // Stats bar
583
+ const statsBar = DomUtils.el('div', { className: 'tc-stats-bar' });
584
+ statsBar.innerHTML = `
585
+ <span>${designs.length} design${designs.length !== 1 ? 's' : ''}</span>
586
+ ${this._state.selectedDesigns.size > 0 ? `<span class="tc-selected-count">${this._state.selectedDesigns.size} selected</span>` : ''}
587
+ `;
588
+ content.appendChild(statsBar);
589
+
590
+ // Design grid or list
591
+ if (this._state.viewMode === 'grid') {
592
+ content.appendChild(this._renderDesignGrid(designs));
593
+ } else {
594
+ content.appendChild(this._renderDesignList(designs));
595
+ }
596
+
597
+ // Load more button
598
+ if (this._state.hasMore) {
599
+ const loadMore = DomUtils.el('button', {
600
+ className: 'tc-btn tc-btn-secondary tc-load-more',
601
+ text: 'Load More Designs'
602
+ });
603
+ loadMore.addEventListener('click', () => this._loadMoreDesigns());
604
+ content.appendChild(loadMore);
605
+ }
606
+
607
+ return content;
608
+ }
609
+
610
+ _getSortedFilteredDesigns() {
611
+ let designs = [...this._state.designs];
612
+
613
+ // Apply filter
614
+ if (this._state.filterType === 'favorites') {
615
+ designs = designs.filter(d => this._state.favorites.has(d.id));
616
+ } else if (this._state.filterType === 'recent') {
617
+ const recentIds = new Set(this._state.recentlyViewed);
618
+ designs = designs.filter(d => recentIds.has(d.id));
619
+ } else if (this._state.filterType === 'collection' && this._state.currentCollection) {
620
+ const collection = this._state.collections.find(c => c.id === this._state.currentCollection);
621
+ if (collection) {
622
+ const collectionIds = new Set(collection.designIds);
623
+ designs = designs.filter(d => collectionIds.has(d.id));
624
+ }
625
+ }
626
+
627
+ // Apply search
628
+ if (this._state.searchQuery) {
629
+ const query = this._state.searchQuery.toLowerCase();
630
+ designs = designs.filter(d => (d.title || '').toLowerCase().includes(query));
631
+ }
632
+
633
+ // Apply sort
634
+ designs.sort((a, b) => {
635
+ let aVal, bVal;
636
+ if (this._state.sortBy === 'title') {
637
+ aVal = (a.title || '').toLowerCase();
638
+ bVal = (b.title || '').toLowerCase();
639
+ return this._state.sortOrder === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
640
+ } else {
641
+ aVal = a[this._state.sortBy] || 0;
642
+ bVal = b[this._state.sortBy] || 0;
643
+ return this._state.sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
644
+ }
645
+ });
646
+
647
+ return designs;
648
+ }
649
+
650
+ _renderDesignGrid(designs) {
651
+ const grid = DomUtils.el('div', { className: 'tc-design-grid' });
652
+ designs.forEach(design => {
653
+ grid.appendChild(this._renderDesignCard(design));
654
+ });
655
+ return grid;
656
+ }
657
+
658
+ _renderDesignList(designs) {
659
+ const list = DomUtils.el('div', { className: 'tc-design-list' });
660
+ designs.forEach(design => {
661
+ list.appendChild(this._renderDesignListItem(design));
662
+ });
663
+ return list;
664
+ }
665
+
666
+ _renderDesignListItem(design) {
667
+ const row = DomUtils.el('div', {
668
+ className: `tc-design-row ${this._state.selectedDesigns.has(design.id) ? 'selected' : ''}`
669
+ });
670
+
671
+ // Checkbox for selection
672
+ if (this._config.features.bulkActions) {
673
+ const checkbox = DomUtils.el('input', {
674
+ className: 'tc-design-checkbox',
675
+ attrs: { type: 'checkbox' }
676
+ });
677
+ checkbox.checked = this._state.selectedDesigns.has(design.id);
678
+ checkbox.addEventListener('change', (e) => {
679
+ e.stopPropagation();
680
+ this._toggleSelection(design.id);
681
+ });
682
+ row.appendChild(checkbox);
683
+ }
684
+
685
+ // Thumbnail
686
+ const thumb = DomUtils.el('div', { className: 'tc-row-thumbnail' });
687
+ if (design.thumbnail?.url) {
688
+ thumb.innerHTML = `<img src="${design.thumbnail.url}" alt="${design.title || ''}">`;
689
+ }
690
+ row.appendChild(thumb);
691
+
692
+ // Info
693
+ const info = DomUtils.el('div', { className: 'tc-row-info' });
694
+ info.innerHTML = `
695
+ <div class="tc-row-title">${design.title || 'Untitled'}</div>
696
+ <div class="tc-row-meta">${design.page_count || 1} page${(design.page_count || 1) > 1 ? 's' : ''} • ${DomUtils.formatDate(design.updated_at)}</div>
697
+ `;
698
+ row.appendChild(info);
699
+
700
+ // Favorite button
701
+ if (this._config.features.favorites) {
702
+ const favBtn = DomUtils.el('button', {
703
+ className: `tc-btn tc-btn-icon tc-fav-btn ${this._state.favorites.has(design.id) ? 'active' : ''}`
704
+ });
705
+ favBtn.innerHTML = this._state.favorites.has(design.id) ? '★' : '☆';
706
+ favBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleFavorite(design.id); });
707
+ row.appendChild(favBtn);
708
+ }
709
+
710
+ // Actions
711
+ const actions = DomUtils.el('div', { className: 'tc-row-actions' });
712
+ actions.innerHTML = `
713
+ <button class="tc-btn tc-btn-small" data-action="open">Open</button>
714
+ <button class="tc-btn tc-btn-small" data-action="export">Export</button>
715
+ <button class="tc-btn tc-btn-icon" data-action="menu">⋮</button>
716
+ `;
717
+ actions.addEventListener('click', (e) => {
718
+ e.stopPropagation();
719
+ const action = e.target.dataset.action;
720
+ if (action === 'open') this._openDesign(design);
721
+ else if (action === 'export') this._showExportModal(design);
722
+ else if (action === 'menu') this._showContextMenu(e, design);
723
+ });
724
+ row.appendChild(actions);
725
+
726
+ // Click to preview
727
+ row.addEventListener('click', () => this._showPreview(design));
728
+ row.addEventListener('contextmenu', (e) => { e.preventDefault(); this._showContextMenu(e, design); });
729
+
730
+ return row;
731
+ }
732
+
733
+ _renderDesignCard(design) {
734
+ const card = DomUtils.el('div', {
735
+ className: `tc-design-card ${this._state.selectedDesigns.has(design.id) ? 'selected' : ''}`
736
+ });
737
+
738
+ // Thumbnail with overlay
739
+ const thumbnail = DomUtils.el('div', { className: 'tc-design-thumbnail' });
740
+ if (design.thumbnail?.url) {
741
+ thumbnail.innerHTML = `<img src="${design.thumbnail.url}" alt="${design.title || ''}" loading="lazy">`;
742
+ }
743
+
744
+ // Overlay with quick actions
745
+ const overlay = DomUtils.el('div', { className: 'tc-card-overlay' });
746
+ overlay.innerHTML = `
747
+ <div class="tc-overlay-actions">
748
+ <button class="tc-overlay-btn" data-action="preview" title="Preview">👁</button>
749
+ <button class="tc-overlay-btn" data-action="edit" title="Edit Design">✏️</button>
750
+ <button class="tc-overlay-btn" data-action="export" title="Export">⬇️</button>
751
+ <button class="tc-overlay-btn" data-action="menu" title="More">⋮</button>
752
+ </div>
753
+ `;
754
+ overlay.addEventListener('click', (e) => {
755
+ e.stopPropagation();
756
+ const action = e.target.closest('[data-action]')?.dataset.action;
757
+ if (action === 'preview') this._showPreview(design);
758
+ else if (action === 'edit') this._showEditorModal(design);
759
+ else if (action === 'export') this._showExportModal(design);
760
+ else if (action === 'menu') this._showContextMenu(e, design);
761
+ });
762
+ thumbnail.appendChild(overlay);
763
+
764
+ // Selection checkbox
765
+ if (this._config.features.bulkActions) {
766
+ const checkbox = DomUtils.el('div', { className: 'tc-card-checkbox' });
767
+ checkbox.innerHTML = `<input type="checkbox" ${this._state.selectedDesigns.has(design.id) ? 'checked' : ''}>`;
768
+ checkbox.addEventListener('click', (e) => {
769
+ e.stopPropagation();
770
+ this._toggleSelection(design.id);
771
+ });
772
+ thumbnail.appendChild(checkbox);
773
+ }
774
+
775
+ // Favorite button - always visible
776
+ if (this._config.features.favorites) {
777
+ const favBtn = DomUtils.el('button', {
778
+ className: `tc-card-fav-btn ${this._state.favorites.has(design.id) ? 'active' : ''}`
779
+ });
780
+ favBtn.innerHTML = this._state.favorites.has(design.id) ? '★' : '☆';
781
+ favBtn.title = this._state.favorites.has(design.id) ? 'Remove from favorites' : 'Add to favorites';
782
+ favBtn.addEventListener('click', (e) => {
783
+ e.stopPropagation();
784
+ this._toggleFavorite(design.id);
785
+ });
786
+ thumbnail.appendChild(favBtn);
787
+ }
788
+
789
+ // Info section
790
+ const info = DomUtils.el('div', { className: 'tc-design-info' });
791
+
792
+ const titleRow = DomUtils.el('div', { className: 'tc-design-title-row' });
793
+ const title = DomUtils.el('div', { className: 'tc-design-title', text: design.title || 'Untitled' });
794
+ titleRow.appendChild(title);
795
+
796
+ const meta = DomUtils.el('div', { className: 'tc-design-meta' });
797
+ meta.innerHTML = `
798
+ <span>${design.page_count || 1} page${(design.page_count || 1) > 1 ? 's' : ''}</span>
799
+ <span class="tc-meta-dot">•</span>
800
+ <span>${DomUtils.formatDate(design.updated_at)}</span>
801
+ `;
802
+
803
+ info.appendChild(titleRow);
804
+ info.appendChild(meta);
805
+
806
+ card.appendChild(thumbnail);
807
+ card.appendChild(info);
808
+
809
+ // Events
810
+ card.addEventListener('click', () => this._showPreview(design));
811
+ card.addEventListener('contextmenu', (e) => { e.preventDefault(); this._showContextMenu(e, design); });
812
+
813
+ return card;
814
+ }
815
+
816
+ _renderPreviewModal() {
817
+ const design = this._state.previewDesign;
818
+ if (!design) return;
819
+
820
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
821
+ modal.innerHTML = `
822
+ <div class="tc-preview-modal">
823
+ <div class="tc-preview-header">
824
+ <h3>${design.title || 'Untitled'}</h3>
825
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
826
+ </div>
827
+ <div class="tc-preview-body">
828
+ <div class="tc-preview-image">
829
+ ${design.thumbnail?.url ? `<img src="${design.thumbnail.url}" alt="${design.title || ''}">` : '<div class="tc-preview-placeholder">No preview available</div>'}
830
+ </div>
831
+ <div class="tc-preview-details">
832
+ <div class="tc-detail-row"><span class="tc-detail-label">Pages</span><span>${design.page_count || 1}</span></div>
833
+ <div class="tc-detail-row"><span class="tc-detail-label">Created</span><span>${DomUtils.formatDate(design.created_at)}</span></div>
834
+ <div class="tc-detail-row"><span class="tc-detail-label">Modified</span><span>${DomUtils.formatDate(design.updated_at)}</span></div>
835
+ </div>
836
+ </div>
837
+ <div class="tc-preview-footer">
838
+ <button class="tc-btn tc-btn-secondary" data-action="favorite">${this._state.favorites.has(design.id) ? '★ Remove from Favorites' : '☆ Add to Favorites'}</button>
839
+ <div class="tc-preview-actions">
840
+ <button class="tc-btn tc-btn-secondary" data-action="export">Export</button>
841
+ <button class="tc-btn tc-btn-primary" data-action="open">Open in Canva</button>
842
+ </div>
843
+ </div>
844
+ </div>
845
+ `;
846
+
847
+ // Event handlers
848
+ modal.querySelector('.tc-modal-close').addEventListener('click', () => this._closePreview());
849
+ modal.addEventListener('click', (e) => { if (e.target === modal) this._closePreview(); });
850
+ modal.querySelector('[data-action="open"]').addEventListener('click', () => this._openDesign(design));
851
+ modal.querySelector('[data-action="export"]').addEventListener('click', () => this._showExportModal(design));
852
+ modal.querySelector('[data-action="favorite"]').addEventListener('click', () => {
853
+ this._toggleFavorite(design.id);
854
+ this._render();
855
+ });
856
+
857
+ this._container.appendChild(modal);
858
+
859
+ // Track as recently viewed
860
+ this._addToRecentlyViewed(design.id);
861
+ }
862
+
863
+ _renderContextMenu() {
864
+ const { event, design } = this._state.contextMenuTarget;
865
+
866
+ const menu = DomUtils.el('div', { className: 'tc-context-menu' });
867
+ menu.style.left = `${event.clientX}px`;
868
+ menu.style.top = `${event.clientY}px`;
869
+
870
+ const items = [
871
+ { icon: '👁', label: 'Preview', action: 'preview' },
872
+ { icon: '✏️', label: 'Edit Design', action: 'edit' },
873
+ { icon: '🔗', label: 'Open in Browser', action: 'open' },
874
+ { icon: '⬇️', label: 'Export', action: 'export' },
875
+ { divider: true },
876
+ { icon: this._state.favorites.has(design.id) ? '★' : '☆', label: this._state.favorites.has(design.id) ? 'Remove from Favorites' : 'Add to Favorites', action: 'favorite' },
877
+ { icon: '📁', label: 'Add to Collection', action: 'add-to-collection', hasSubmenu: true },
878
+ { icon: '📝', label: 'Notes & Tags', action: 'annotations' },
879
+ { icon: '📦', label: 'Add to Asset Library', action: 'add-asset' },
880
+ { icon: '⚖️', label: 'Add to Compare', action: 'add-compare' },
881
+ { icon: '📋', label: 'Quick Insert', action: 'quick-insert' },
882
+ { divider: true },
883
+ { icon: '🗑️', label: 'Delete', action: 'delete', danger: true },
884
+ ];
885
+
886
+ // If viewing a collection, add "Remove from this collection" option
887
+ if (this._state.filterType === 'collection' && this._state.currentCollection) {
888
+ items.splice(6, 0, { icon: '➖', label: 'Remove from Collection', action: 'remove-from-collection' });
889
+ }
890
+
891
+ items.forEach(item => {
892
+ if (item.divider) {
893
+ menu.appendChild(DomUtils.el('div', { className: 'tc-context-divider' }));
894
+ } else {
895
+ const menuItem = DomUtils.el('div', {
896
+ className: `tc-context-item ${item.danger ? 'danger' : ''} ${item.hasSubmenu ? 'has-submenu' : ''}`
897
+ });
898
+ menuItem.innerHTML = `<span class="tc-context-icon">${item.icon}</span><span>${item.label}</span>${item.hasSubmenu ? '<span class="tc-submenu-arrow">▶</span>' : ''}`;
899
+
900
+ if (item.action === 'add-to-collection') {
901
+ // Show submenu with collections
902
+ menuItem.addEventListener('mouseenter', () => this._showCollectionSubmenu(menuItem, design));
903
+ menuItem.addEventListener('mouseleave', (e) => {
904
+ const submenu = menuItem.querySelector('.tc-submenu');
905
+ if (submenu && !submenu.contains(e.relatedTarget)) {
906
+ submenu.remove();
907
+ }
908
+ });
909
+ } else {
910
+ menuItem.addEventListener('click', (e) => {
911
+ this._closeContextMenu();
912
+ if (item.action === 'preview') this._showPreview(design);
913
+ else if (item.action === 'edit') this._showEditorModal(design);
914
+ else if (item.action === 'open') this._openDesign(design);
915
+ else if (item.action === 'export') this._showExportModal(design);
916
+ else if (item.action === 'favorite') this._toggleFavorite(design.id);
917
+ else if (item.action === 'copy-link') this._copyDesignLink(design);
918
+ else if (item.action === 'delete') this._confirmDelete(design);
919
+ else if (item.action === 'remove-from-collection') this._removeFromCollection(design.id, this._state.currentCollection);
920
+ else if (item.action === 'annotations') this._showAnnotationsModal(design);
921
+ else if (item.action === 'add-asset') this._addToAssetLibrary(design.id);
922
+ else if (item.action === 'add-compare') this._addToCompare(design.id);
923
+ else if (item.action === 'quick-insert') this._showQuickInsertMenu(design, e);
924
+ });
925
+ }
926
+ menu.appendChild(menuItem);
927
+ }
928
+ });
929
+
930
+ // Close on click outside
931
+ const closeHandler = (e) => {
932
+ if (!menu.contains(e.target)) {
933
+ this._closeContextMenu();
934
+ document.removeEventListener('click', closeHandler);
935
+ }
936
+ };
937
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
938
+
939
+ this._container.appendChild(menu);
940
+ }
941
+
942
+ _showCollectionSubmenu(parentItem, design) {
943
+ // Remove any existing submenu
944
+ const existing = parentItem.querySelector('.tc-submenu');
945
+ if (existing) return;
946
+
947
+ const submenu = DomUtils.el('div', { className: 'tc-submenu' });
948
+
949
+ if (this._state.collections.length === 0) {
950
+ const emptyItem = DomUtils.el('div', { className: 'tc-context-item disabled', text: 'No collections' });
951
+ submenu.appendChild(emptyItem);
952
+ } else {
953
+ this._state.collections.forEach(collection => {
954
+ const isInCollection = collection.designIds.includes(design.id);
955
+ const item = DomUtils.el('div', {
956
+ className: `tc-context-item ${isInCollection ? 'checked' : ''}`
957
+ });
958
+ item.innerHTML = `
959
+ <span class="tc-sidebar-icon" style="color: ${collection.color}">●</span>
960
+ <span>${collection.name}</span>
961
+ ${isInCollection ? '<span class="tc-check">✓</span>' : ''}
962
+ `;
963
+ item.addEventListener('click', (e) => {
964
+ e.stopPropagation();
965
+ if (isInCollection) {
966
+ this._removeFromCollection(design.id, collection.id);
967
+ } else {
968
+ this._addToCollection(design.id, collection.id);
969
+ }
970
+ this._closeContextMenu();
971
+ });
972
+ submenu.appendChild(item);
973
+ });
974
+ }
975
+
976
+ // Add "Create new collection" option
977
+ submenu.appendChild(DomUtils.el('div', { className: 'tc-context-divider' }));
978
+ const createItem = DomUtils.el('div', { className: 'tc-context-item' });
979
+ createItem.innerHTML = `<span class="tc-context-icon">+</span><span>Create Collection</span>`;
980
+ createItem.addEventListener('click', (e) => {
981
+ e.stopPropagation();
982
+ this._closeContextMenu();
983
+ this._showCreateCollectionModal();
984
+ });
985
+ submenu.appendChild(createItem);
986
+
987
+ parentItem.appendChild(submenu);
988
+ }
989
+
990
+ _showExportModal(design) {
991
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
992
+ modal.innerHTML = `
993
+ <div class="tc-export-modal">
994
+ <div class="tc-modal-header">
995
+ <h3>Export "${design.title || 'Untitled'}"</h3>
996
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
997
+ </div>
998
+ <div class="tc-modal-body">
999
+ <div class="tc-export-options">
1000
+ <label class="tc-export-option">
1001
+ <input type="radio" name="format" value="png" checked>
1002
+ <div class="tc-export-option-content">
1003
+ <span class="tc-export-icon">🖼️</span>
1004
+ <div>
1005
+ <div class="tc-export-format">PNG</div>
1006
+ <div class="tc-export-desc">High quality image</div>
1007
+ </div>
1008
+ </div>
1009
+ </label>
1010
+ <label class="tc-export-option">
1011
+ <input type="radio" name="format" value="pdf">
1012
+ <div class="tc-export-option-content">
1013
+ <span class="tc-export-icon">📄</span>
1014
+ <div>
1015
+ <div class="tc-export-format">PDF</div>
1016
+ <div class="tc-export-desc">Document format</div>
1017
+ </div>
1018
+ </div>
1019
+ </label>
1020
+ <label class="tc-export-option">
1021
+ <input type="radio" name="format" value="jpg">
1022
+ <div class="tc-export-option-content">
1023
+ <span class="tc-export-icon">📷</span>
1024
+ <div>
1025
+ <div class="tc-export-format">JPG</div>
1026
+ <div class="tc-export-desc">Compressed image</div>
1027
+ </div>
1028
+ </div>
1029
+ </label>
1030
+ </div>
1031
+ </div>
1032
+ <div class="tc-modal-footer">
1033
+ <button class="tc-btn tc-btn-secondary tc-modal-cancel">Cancel</button>
1034
+ <button class="tc-btn tc-btn-primary tc-export-confirm">Export</button>
1035
+ </div>
1036
+ </div>
1037
+ `;
1038
+
1039
+ modal.querySelector('.tc-modal-close').addEventListener('click', () => modal.remove());
1040
+ modal.querySelector('.tc-modal-cancel').addEventListener('click', () => modal.remove());
1041
+ modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
1042
+ modal.querySelector('.tc-export-confirm').addEventListener('click', async () => {
1043
+ const format = modal.querySelector('input[name="format"]:checked').value;
1044
+ modal.remove();
1045
+ await this._exportDesign(design, format);
1046
+ });
1047
+
1048
+ this._container.appendChild(modal);
1049
+ }
1050
+
1051
+ _getFilteredDesigns() {
1052
+ if (!this._state.searchQuery) {
1053
+ return this._state.designs;
1054
+ }
1055
+
1056
+ const query = this._state.searchQuery.toLowerCase();
1057
+ return this._state.designs.filter(d =>
1058
+ (d.title || '').toLowerCase().includes(query)
1059
+ );
1060
+ }
1061
+
1062
+ _filterDesigns() {
1063
+ const content = this._container.querySelector('.tc-content');
1064
+ if (content) {
1065
+ const newContent = this._renderContent();
1066
+ content.replaceWith(newContent);
1067
+ }
1068
+ }
1069
+
1070
+ // Helper methods for new features
1071
+ _toggleSelection(designId) {
1072
+ if (this._state.selectedDesigns.has(designId)) {
1073
+ this._state.selectedDesigns.delete(designId);
1074
+ } else {
1075
+ this._state.selectedDesigns.add(designId);
1076
+ }
1077
+ this._render();
1078
+ }
1079
+
1080
+ _toggleFavorite(designId) {
1081
+ if (this._state.favorites.has(designId)) {
1082
+ this._state.favorites.delete(designId);
1083
+ } else {
1084
+ this._state.favorites.add(designId);
1085
+ }
1086
+ this._persistFavorites(); // Save to disk
1087
+ this._render();
1088
+ }
1089
+
1090
+ _addToRecentlyViewed(designId) {
1091
+ const recent = this._state.recentlyViewed.filter(id => id !== designId);
1092
+ recent.unshift(designId);
1093
+ this._state.recentlyViewed = recent.slice(0, 20); // Keep last 20
1094
+ }
1095
+
1096
+ _showPreview(design) {
1097
+ this._state.previewDesign = design;
1098
+ this._render();
1099
+ }
1100
+
1101
+ _closePreview() {
1102
+ this._state.previewDesign = null;
1103
+ this._render();
1104
+ }
1105
+
1106
+ _showContextMenu(event, design) {
1107
+ this._state.contextMenuTarget = { event, design };
1108
+ this._render();
1109
+ }
1110
+
1111
+ _closeContextMenu() {
1112
+ this._state.contextMenuTarget = null;
1113
+ const menu = this._container.querySelector('.tc-context-menu');
1114
+ if (menu) menu.remove();
1115
+ }
1116
+
1117
+ _copyDesignLink(design) {
1118
+ const link = design.urls?.view_url || design.urls?.edit_url || '';
1119
+ if (link && navigator.clipboard) {
1120
+ navigator.clipboard.writeText(link).then(() => {
1121
+ this._showToast('Link copied to clipboard');
1122
+ });
1123
+ }
1124
+ }
1125
+
1126
+ _confirmDelete(design) {
1127
+ if (confirm(`Are you sure you want to delete "${design.title || 'Untitled'}"? This cannot be undone.`)) {
1128
+ // Note: Canva API doesn't support delete, so we just show a message
1129
+ this._showToast('Delete is not supported via API. Please delete in Canva.');
1130
+ }
1131
+ }
1132
+
1133
+ _showToast(message) {
1134
+ const existing = this._container.querySelector('.tc-toast');
1135
+ if (existing) existing.remove();
1136
+
1137
+ const toast = DomUtils.el('div', { className: 'tc-toast', text: message });
1138
+ this._container.appendChild(toast);
1139
+
1140
+ setTimeout(() => toast.classList.add('show'), 10);
1141
+ setTimeout(() => {
1142
+ toast.classList.remove('show');
1143
+ setTimeout(() => toast.remove(), 300);
1144
+ }, 3000);
1145
+ }
1146
+
1147
+ async _bulkExport() {
1148
+ const designs = this._state.designs.filter(d => this._state.selectedDesigns.has(d.id));
1149
+ this._showToast(`Exporting ${designs.length} designs...`);
1150
+
1151
+ for (const design of designs) {
1152
+ await this._exportDesign(design, 'png');
1153
+ }
1154
+
1155
+ this._state.selectedDesigns.clear();
1156
+ this._render();
1157
+ }
1158
+
1159
+ _bulkFavorite() {
1160
+ for (const id of this._state.selectedDesigns) {
1161
+ this._state.favorites.add(id);
1162
+ }
1163
+ this._state.selectedDesigns.clear();
1164
+ this._persistFavorites(); // Save to disk
1165
+ this._showToast('Added to favorites');
1166
+ this._render();
1167
+ }
1168
+
1169
+ async _loadMoreDesigns() {
1170
+ if (!this._state.continuation) return;
1171
+
1172
+ try {
1173
+ const tokens = await this._config.onGetToken(this._config.currentUserId);
1174
+ const channel = `${this._config.channelPrefix}:list-designs`;
1175
+ const result = await window.electron.invoke(channel, {
1176
+ userId: this._config.currentUserId,
1177
+ accessToken: tokens.access_token,
1178
+ continuation: this._state.continuation
1179
+ });
1180
+
1181
+ this._state.designs = [...this._state.designs, ...(result.items || [])];
1182
+ this._state.continuation = result.continuation || null;
1183
+ this._state.hasMore = !!result.continuation;
1184
+ this._render();
1185
+
1186
+ } catch (err) {
1187
+ console.error('[TacelCanva] Failed to load more:', err);
1188
+ }
1189
+ }
1190
+
1191
+ async _loadFolders() {
1192
+ try {
1193
+ const tokens = await this._config.onGetToken(this._config.currentUserId);
1194
+ const channel = `${this._config.channelPrefix}:list-folders`;
1195
+ const result = await window.electron.invoke(channel, {
1196
+ userId: this._config.currentUserId,
1197
+ accessToken: tokens.access_token
1198
+ });
1199
+
1200
+ this._state.folders = result.items || [];
1201
+ } catch (err) {
1202
+ console.error('[TacelCanva] Failed to load folders:', err);
1203
+ }
1204
+ }
1205
+
1206
+ async _startOAuthFlow() {
1207
+ try {
1208
+ const { verifier, challenge } = await PKCEUtils.generatePKCE();
1209
+
1210
+ // Store verifier for later token exchange
1211
+ this._state.authState = verifier;
1212
+
1213
+ // Request OAuth URL from backend
1214
+ const channel = `${this._config.channelPrefix}:get-auth-url`;
1215
+ const authUrl = await window.electron.invoke(channel, {
1216
+ codeChallenge: challenge,
1217
+ userId: this._config.currentUserId
1218
+ });
1219
+
1220
+ // Open auth URL in browser
1221
+ window.electron.invoke('open-external', authUrl);
1222
+
1223
+ // Show waiting state
1224
+ this._showAuthWaiting();
1225
+
1226
+ } catch (err) {
1227
+ console.error('[TacelCanva] OAuth flow failed:', err);
1228
+ alert('Failed to start authentication. Please try again.');
1229
+ }
1230
+ }
1231
+
1232
+ _showAuthWaiting() {
1233
+ DomUtils.clear(this._container);
1234
+
1235
+ const waiting = DomUtils.el('div', { className: 'tc-auth-waiting' });
1236
+ waiting.innerHTML = `
1237
+ <div class="tc-auth-spinner"></div>
1238
+ <h3>Waiting for authorization...</h3>
1239
+ <p>Complete the authorization in your browser, then return here.</p>
1240
+ <button class="tc-btn tc-btn-secondary">Cancel</button>
1241
+ `;
1242
+
1243
+ const cancelBtn = waiting.querySelector('button');
1244
+ cancelBtn.addEventListener('click', () => {
1245
+ this._state.authState = null;
1246
+ this._render();
1247
+ });
1248
+
1249
+ this._container.appendChild(waiting);
1250
+ }
1251
+
1252
+ async completeOAuth(code) {
1253
+ if (!this._state.authState) {
1254
+ console.error('[TacelCanva] No auth state found');
1255
+ return;
1256
+ }
1257
+
1258
+ try {
1259
+ const channel = `${this._config.channelPrefix}:exchange-token`;
1260
+ const tokens = await window.electron.invoke(channel, {
1261
+ code,
1262
+ codeVerifier: this._state.authState,
1263
+ userId: this._config.currentUserId
1264
+ });
1265
+
1266
+ // Save tokens via callback
1267
+ await this._config.onSaveToken(this._config.currentUserId, tokens);
1268
+
1269
+ this._state.isAuthenticated = true;
1270
+ this._state.authState = null;
1271
+
1272
+ await this._loadDesigns();
1273
+ this._render();
1274
+
1275
+ } catch (err) {
1276
+ console.error('[TacelCanva] Token exchange failed:', err);
1277
+ alert('Authentication failed. Please try again.');
1278
+ this._state.authState = null;
1279
+ this._render();
1280
+ }
1281
+ }
1282
+
1283
+ async _loadDesigns() {
1284
+ this._state.isLoading = true;
1285
+ this._render();
1286
+
1287
+ try {
1288
+ const tokens = await this._config.onGetToken(this._config.currentUserId);
1289
+ if (!tokens) {
1290
+ throw new Error('No tokens found');
1291
+ }
1292
+
1293
+ // Try to load from cache first for instant display
1294
+ const cached = await this._loadDesignsFromCache();
1295
+ if (cached && cached.designs && cached.designs.length > 0) {
1296
+ this._state.designs = cached.designs;
1297
+ this._state.isLoading = false;
1298
+ this._render();
1299
+ console.log(`[TacelCanva] Loaded ${cached.designs.length} designs from cache (last sync: ${cached.lastSync})`);
1300
+
1301
+ // Load other data while showing cached designs
1302
+ await this._loadPersistedFavorites();
1303
+ await this._loadCollections();
1304
+ this._render();
1305
+
1306
+ // Background sync - fetch fresh data and update if changed
1307
+ this._backgroundSyncDesigns(tokens);
1308
+ return;
1309
+ }
1310
+
1311
+ // No cache - do full load
1312
+ await this._fullLoadDesigns(tokens);
1313
+
1314
+ } catch (err) {
1315
+ console.error('[TacelCanva] Failed to load designs:', err);
1316
+ if (err.message.includes('401') || err.message.includes('token')) {
1317
+ this._state.isAuthenticated = false;
1318
+ }
1319
+ } finally {
1320
+ this._state.isLoading = false;
1321
+ this._render();
1322
+ }
1323
+ }
1324
+
1325
+ async _loadDesignsFromCache() {
1326
+ try {
1327
+ return await window.electron.invoke('get-canva-designs-cache', this._config.currentUserId);
1328
+ } catch (err) {
1329
+ console.error('[TacelCanva] Failed to load cache:', err);
1330
+ return null;
1331
+ }
1332
+ }
1333
+
1334
+ async _saveDesignsToCache(designs) {
1335
+ try {
1336
+ await window.electron.invoke('save-canva-designs-cache', {
1337
+ userId: this._config.currentUserId,
1338
+ designs,
1339
+ lastSync: new Date().toISOString()
1340
+ });
1341
+ } catch (err) {
1342
+ console.error('[TacelCanva] Failed to save cache:', err);
1343
+ }
1344
+ }
1345
+
1346
+ async _fullLoadDesigns(tokens) {
1347
+ // Load ALL designs by following pagination
1348
+ let allDesigns = [];
1349
+ let continuation = null;
1350
+ let pageCount = 0;
1351
+ const maxPages = 20; // Safety limit
1352
+
1353
+ do {
1354
+ const channel = `${this._config.channelPrefix}:list-designs`;
1355
+ const result = await window.electron.invoke(channel, {
1356
+ userId: this._config.currentUserId,
1357
+ accessToken: tokens.access_token,
1358
+ continuation: continuation
1359
+ });
1360
+
1361
+ const items = result.items || [];
1362
+ allDesigns = [...allDesigns, ...items];
1363
+ continuation = result.continuation || null;
1364
+ pageCount++;
1365
+
1366
+ console.log(`[TacelCanva] Loaded page ${pageCount}: ${items.length} designs (total: ${allDesigns.length})`);
1367
+
1368
+ } while (continuation && pageCount < maxPages);
1369
+
1370
+ this._state.designs = allDesigns;
1371
+ this._state.continuation = continuation;
1372
+ this._state.hasMore = !!continuation;
1373
+
1374
+ console.log(`[TacelCanva] Total designs loaded: ${allDesigns.length}`);
1375
+
1376
+ // Save to cache
1377
+ await this._saveDesignsToCache(allDesigns);
1378
+
1379
+ // Load other data
1380
+ await this._loadPersistedFavorites();
1381
+ await this._loadCollections();
1382
+ }
1383
+
1384
+ async _backgroundSyncDesigns(tokens) {
1385
+ console.log('[TacelCanva] Starting background sync...');
1386
+
1387
+ try {
1388
+ // Fetch first page to check for changes
1389
+ const channel = `${this._config.channelPrefix}:list-designs`;
1390
+ const result = await window.electron.invoke(channel, {
1391
+ userId: this._config.currentUserId,
1392
+ accessToken: tokens.access_token
1393
+ });
1394
+
1395
+ const freshItems = result.items || [];
1396
+ const cachedIds = new Set(this._state.designs.map(d => d.id));
1397
+ const freshIds = new Set(freshItems.map(d => d.id));
1398
+
1399
+ // Check if there are new designs or updates
1400
+ let hasChanges = false;
1401
+ for (const item of freshItems) {
1402
+ if (!cachedIds.has(item.id)) {
1403
+ hasChanges = true;
1404
+ break;
1405
+ }
1406
+ // Check if updated_at changed
1407
+ const cached = this._state.designs.find(d => d.id === item.id);
1408
+ if (cached && cached.updated_at !== item.updated_at) {
1409
+ hasChanges = true;
1410
+ break;
1411
+ }
1412
+ }
1413
+
1414
+ if (hasChanges || result.continuation) {
1415
+ console.log('[TacelCanva] Changes detected, doing full refresh...');
1416
+ await this._fullLoadDesigns(tokens);
1417
+ this._render();
1418
+ this._showToast('Designs updated');
1419
+ } else {
1420
+ console.log('[TacelCanva] No changes detected, cache is up to date');
1421
+ }
1422
+
1423
+ } catch (err) {
1424
+ console.error('[TacelCanva] Background sync failed:', err);
1425
+ }
1426
+ }
1427
+
1428
+ async _loadPersistedFavorites() {
1429
+ try {
1430
+ const favorites = await window.electron.invoke('get-canva-favorites', this._config.currentUserId);
1431
+ if (favorites && Array.isArray(favorites)) {
1432
+ this._state.favorites = new Set(favorites);
1433
+ }
1434
+ } catch (err) {
1435
+ console.error('[TacelCanva] Failed to load favorites:', err);
1436
+ }
1437
+ }
1438
+
1439
+ async _persistFavorites() {
1440
+ try {
1441
+ const favorites = Array.from(this._state.favorites);
1442
+ await window.electron.invoke('save-canva-favorites', {
1443
+ userId: this._config.currentUserId,
1444
+ favorites
1445
+ });
1446
+ } catch (err) {
1447
+ console.error('[TacelCanva] Failed to save favorites:', err);
1448
+ }
1449
+ }
1450
+
1451
+ // ═══════════════════════════════════════════════════════════════
1452
+ // COLLECTIONS MANAGEMENT
1453
+ // ═══════════════════════════════════════════════════════════════
1454
+
1455
+ async _loadCollections() {
1456
+ try {
1457
+ const collections = await window.electron.invoke('get-canva-collections', this._config.currentUserId);
1458
+ if (collections && Array.isArray(collections)) {
1459
+ this._state.collections = collections;
1460
+ }
1461
+ } catch (err) {
1462
+ console.error('[TacelCanva] Failed to load collections:', err);
1463
+ }
1464
+ }
1465
+
1466
+ async _persistCollections() {
1467
+ try {
1468
+ await window.electron.invoke('save-canva-collections', {
1469
+ userId: this._config.currentUserId,
1470
+ collections: this._state.collections
1471
+ });
1472
+ } catch (err) {
1473
+ console.error('[TacelCanva] Failed to save collections:', err);
1474
+ }
1475
+ }
1476
+
1477
+ _showCreateCollectionModal() {
1478
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
1479
+ modal.innerHTML = `
1480
+ <div class="tc-collection-modal">
1481
+ <div class="tc-modal-header">
1482
+ <h3>Create Collection</h3>
1483
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
1484
+ </div>
1485
+ <div class="tc-modal-body">
1486
+ <div class="tc-form-group">
1487
+ <label>Collection Name</label>
1488
+ <input type="text" class="tc-input" placeholder="My Collection" autofocus>
1489
+ </div>
1490
+ <div class="tc-form-group">
1491
+ <label>Color</label>
1492
+ <div class="tc-color-picker">
1493
+ ${['#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899', '#6b7280'].map(c =>
1494
+ `<button class="tc-color-option" data-color="${c}" style="background: ${c}"></button>`
1495
+ ).join('')}
1496
+ </div>
1497
+ </div>
1498
+ </div>
1499
+ <div class="tc-modal-footer">
1500
+ <button class="tc-btn tc-btn-secondary tc-modal-cancel">Cancel</button>
1501
+ <button class="tc-btn tc-btn-primary tc-create-collection-btn">Create</button>
1502
+ </div>
1503
+ </div>
1504
+ `;
1505
+
1506
+ let selectedColor = '#3b82f6';
1507
+ const colorOptions = modal.querySelectorAll('.tc-color-option');
1508
+ colorOptions.forEach(opt => {
1509
+ if (opt.dataset.color === selectedColor) opt.classList.add('selected');
1510
+ opt.addEventListener('click', () => {
1511
+ colorOptions.forEach(o => o.classList.remove('selected'));
1512
+ opt.classList.add('selected');
1513
+ selectedColor = opt.dataset.color;
1514
+ });
1515
+ });
1516
+
1517
+ const input = modal.querySelector('input');
1518
+ const createBtn = modal.querySelector('.tc-create-collection-btn');
1519
+
1520
+ const close = () => modal.remove();
1521
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
1522
+ modal.querySelector('.tc-modal-cancel').addEventListener('click', close);
1523
+ modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
1524
+
1525
+ createBtn.addEventListener('click', () => {
1526
+ const name = input.value.trim();
1527
+ if (!name) {
1528
+ input.focus();
1529
+ return;
1530
+ }
1531
+ this._createCollection(name, selectedColor);
1532
+ close();
1533
+ });
1534
+
1535
+ input.addEventListener('keydown', (e) => {
1536
+ if (e.key === 'Enter') createBtn.click();
1537
+ });
1538
+
1539
+ this._container.appendChild(modal);
1540
+ input.focus();
1541
+ }
1542
+
1543
+ _createCollection(name, color) {
1544
+ const collection = {
1545
+ id: `col_${Date.now()}`,
1546
+ name,
1547
+ color,
1548
+ designIds: [],
1549
+ createdAt: new Date().toISOString(),
1550
+ };
1551
+ this._state.collections.push(collection);
1552
+ this._persistCollections();
1553
+ this._showToast(`Collection "${name}" created`);
1554
+ this._render();
1555
+ }
1556
+
1557
+ _showCollectionContextMenu(event, collection) {
1558
+ const menu = DomUtils.el('div', { className: 'tc-context-menu' });
1559
+ menu.style.left = `${event.clientX}px`;
1560
+ menu.style.top = `${event.clientY}px`;
1561
+
1562
+ const items = [
1563
+ { icon: '✏️', label: 'Rename', action: 'rename' },
1564
+ { icon: '🎨', label: 'Change Color', action: 'color' },
1565
+ { divider: true },
1566
+ { icon: '🗑️', label: 'Delete Collection', action: 'delete', danger: true },
1567
+ ];
1568
+
1569
+ items.forEach(item => {
1570
+ if (item.divider) {
1571
+ menu.appendChild(DomUtils.el('div', { className: 'tc-context-divider' }));
1572
+ } else {
1573
+ const menuItem = DomUtils.el('div', {
1574
+ className: `tc-context-item ${item.danger ? 'danger' : ''}`
1575
+ });
1576
+ menuItem.innerHTML = `<span class="tc-context-icon">${item.icon}</span><span>${item.label}</span>`;
1577
+ menuItem.addEventListener('click', () => {
1578
+ menu.remove();
1579
+ if (item.action === 'rename') this._renameCollection(collection);
1580
+ else if (item.action === 'color') this._changeCollectionColor(collection);
1581
+ else if (item.action === 'delete') this._deleteCollection(collection);
1582
+ });
1583
+ menu.appendChild(menuItem);
1584
+ }
1585
+ });
1586
+
1587
+ const closeHandler = (e) => {
1588
+ if (!menu.contains(e.target)) {
1589
+ menu.remove();
1590
+ document.removeEventListener('click', closeHandler);
1591
+ }
1592
+ };
1593
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
1594
+
1595
+ this._container.appendChild(menu);
1596
+ }
1597
+
1598
+ _renameCollection(collection) {
1599
+ const newName = prompt('Enter new name:', collection.name);
1600
+ if (newName && newName.trim()) {
1601
+ collection.name = newName.trim();
1602
+ this._persistCollections();
1603
+ this._render();
1604
+ }
1605
+ }
1606
+
1607
+ _changeCollectionColor(collection) {
1608
+ // Simple color picker using prompt (could be enhanced with modal)
1609
+ const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899'];
1610
+ const colorIndex = colors.indexOf(collection.color);
1611
+ const nextColor = colors[(colorIndex + 1) % colors.length];
1612
+ collection.color = nextColor;
1613
+ this._persistCollections();
1614
+ this._showToast('Color updated');
1615
+ this._render();
1616
+ }
1617
+
1618
+ _deleteCollection(collection) {
1619
+ if (!confirm(`Delete collection "${collection.name}"? Designs will not be deleted.`)) return;
1620
+ this._state.collections = this._state.collections.filter(c => c.id !== collection.id);
1621
+ if (this._state.currentCollection === collection.id) {
1622
+ this._state.currentCollection = null;
1623
+ this._state.filterType = 'all';
1624
+ }
1625
+ this._persistCollections();
1626
+ this._showToast('Collection deleted');
1627
+ this._render();
1628
+ }
1629
+
1630
+ _addToCollection(designId, collectionId) {
1631
+ const collection = this._state.collections.find(c => c.id === collectionId);
1632
+ if (collection && !collection.designIds.includes(designId)) {
1633
+ collection.designIds.push(designId);
1634
+ this._persistCollections();
1635
+ this._showToast(`Added to "${collection.name}"`);
1636
+ }
1637
+ }
1638
+
1639
+ _removeFromCollection(designId, collectionId) {
1640
+ const collection = this._state.collections.find(c => c.id === collectionId);
1641
+ if (collection) {
1642
+ collection.designIds = collection.designIds.filter(id => id !== designId);
1643
+ this._persistCollections();
1644
+ this._showToast(`Removed from "${collection.name}"`);
1645
+ this._render();
1646
+ }
1647
+ }
1648
+
1649
+ _openDesign(design) {
1650
+ if (design.urls && design.urls.edit_url) {
1651
+ window.electron.invoke('open-external', design.urls.edit_url);
1652
+ }
1653
+ }
1654
+
1655
+ async _exportDesign(design) {
1656
+ try {
1657
+ const tokens = await this._config.onGetToken(this._config.currentUserId);
1658
+ const channel = `${this._config.channelPrefix}:export-design`;
1659
+
1660
+ await window.electron.invoke(channel, {
1661
+ userId: this._config.currentUserId,
1662
+ accessToken: tokens.access_token,
1663
+ designId: design.id
1664
+ });
1665
+
1666
+ alert('Export started. You will be notified when ready.');
1667
+
1668
+ } catch (err) {
1669
+ console.error('[TacelCanva] Export failed:', err);
1670
+ alert('Failed to export design. Please try again.');
1671
+ }
1672
+ }
1673
+
1674
+ _createNewDesign() {
1675
+ // Open Canva homepage to create new design
1676
+ window.electron.invoke('open-external', 'https://www.canva.com/create');
1677
+ }
1678
+
1679
+ async _disconnect() {
1680
+ if (!confirm('Are you sure you want to disconnect your Canva account?')) {
1681
+ return;
1682
+ }
1683
+
1684
+ try {
1685
+ await this._config.onRevokeToken(this._config.currentUserId);
1686
+ this._state.isAuthenticated = false;
1687
+ this._state.designs = [];
1688
+ this._state.user = null;
1689
+ this._render();
1690
+ } catch (err) {
1691
+ console.error('[TacelCanva] Disconnect failed:', err);
1692
+ }
1693
+ }
1694
+
1695
+ // ═══════════════════════════════════════════════════════════════
1696
+ // FEATURE 1: BRAND KIT VIEW
1697
+ // ═══════════════════════════════════════════════════════════════
1698
+
1699
+ _renderBrandKitView() {
1700
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-brand-kit-view' });
1701
+
1702
+ view.innerHTML = `
1703
+ <div class="tc-view-header">
1704
+ <h2>Brand Kit</h2>
1705
+ <p>Access your brand colors, logos, and fonts</p>
1706
+ </div>
1707
+ <div class="tc-brand-kit-content">
1708
+ <div class="tc-brand-section">
1709
+ <h3>Brand Colors</h3>
1710
+ <div class="tc-color-swatches" id="brand-colors">
1711
+ <div class="tc-color-swatch-placeholder">Loading brand colors...</div>
1712
+ </div>
1713
+ </div>
1714
+ <div class="tc-brand-section">
1715
+ <h3>Logos</h3>
1716
+ <div class="tc-brand-logos" id="brand-logos">
1717
+ <div class="tc-logo-placeholder">Loading logos...</div>
1718
+ </div>
1719
+ </div>
1720
+ <div class="tc-brand-section">
1721
+ <h3>Fonts</h3>
1722
+ <div class="tc-brand-fonts" id="brand-fonts">
1723
+ <div class="tc-font-placeholder">Loading fonts...</div>
1724
+ </div>
1725
+ </div>
1726
+ </div>
1727
+ `;
1728
+
1729
+ // Load brand kit data
1730
+ this._loadBrandKit();
1731
+
1732
+ return view;
1733
+ }
1734
+
1735
+ async _loadBrandKit() {
1736
+ try {
1737
+ const tokens = await this._config.onGetToken(this._config.currentUserId);
1738
+ if (!tokens) return;
1739
+
1740
+ // Note: Canva Brand Kit API requires specific permissions
1741
+ // For now, show placeholder with manual color entry
1742
+ const colorsEl = this._container.querySelector('#brand-colors');
1743
+ if (colorsEl) {
1744
+ colorsEl.innerHTML = `
1745
+ <div class="tc-brand-colors-grid">
1746
+ <div class="tc-add-color-btn" title="Add brand color">
1747
+ <span>+</span>
1748
+ <span>Add Color</span>
1749
+ </div>
1750
+ </div>
1751
+ <p class="tc-hint">Click to add your brand colors for quick access</p>
1752
+ `;
1753
+
1754
+ colorsEl.querySelector('.tc-add-color-btn').addEventListener('click', () => {
1755
+ this._addBrandColor();
1756
+ });
1757
+
1758
+ // Load saved brand colors
1759
+ this._loadSavedBrandColors(colorsEl.querySelector('.tc-brand-colors-grid'));
1760
+ }
1761
+ } catch (err) {
1762
+ console.error('[TacelCanva] Failed to load brand kit:', err);
1763
+ }
1764
+ }
1765
+
1766
+ async _loadSavedBrandColors(container) {
1767
+ try {
1768
+ const brandData = await window.electron.invoke('get-canva-brand-kit', this._config.currentUserId);
1769
+ if (brandData && brandData.colors) {
1770
+ brandData.colors.forEach(color => {
1771
+ const swatch = DomUtils.el('div', { className: 'tc-color-swatch' });
1772
+ swatch.style.background = color.hex;
1773
+ swatch.innerHTML = `
1774
+ <span class="tc-color-hex">${color.hex}</span>
1775
+ <button class="tc-copy-btn" title="Copy hex code">📋</button>
1776
+ `;
1777
+ swatch.querySelector('.tc-copy-btn').addEventListener('click', (e) => {
1778
+ e.stopPropagation();
1779
+ navigator.clipboard.writeText(color.hex);
1780
+ this._showToast('Color copied!');
1781
+ });
1782
+ container.insertBefore(swatch, container.querySelector('.tc-add-color-btn'));
1783
+ });
1784
+ }
1785
+ } catch (err) {
1786
+ console.error('[TacelCanva] Failed to load brand colors:', err);
1787
+ }
1788
+ }
1789
+
1790
+ async _addBrandColor() {
1791
+ const hex = prompt('Enter hex color code:', '#');
1792
+ if (!hex || !hex.match(/^#[0-9A-Fa-f]{6}$/)) {
1793
+ if (hex) alert('Invalid hex color. Use format: #RRGGBB');
1794
+ return;
1795
+ }
1796
+
1797
+ try {
1798
+ let brandData = await window.electron.invoke('get-canva-brand-kit', this._config.currentUserId) || { colors: [] };
1799
+ brandData.colors.push({ hex, name: hex });
1800
+ await window.electron.invoke('save-canva-brand-kit', { userId: this._config.currentUserId, brandKit: brandData });
1801
+ this._render();
1802
+ } catch (err) {
1803
+ console.error('[TacelCanva] Failed to save brand color:', err);
1804
+ }
1805
+ }
1806
+
1807
+ // ═══════════════════════════════════════════════════════════════
1808
+ // FEATURE 2: TEMPLATES VIEW
1809
+ // ═══════════════════════════════════════════════════════════════
1810
+
1811
+ _renderTemplatesView() {
1812
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-templates-view' });
1813
+
1814
+ const categories = [
1815
+ { id: 'presentation', name: 'Presentations', icon: '📊' },
1816
+ { id: 'social-media', name: 'Social Media', icon: '📱' },
1817
+ { id: 'poster', name: 'Posters', icon: '🖼️' },
1818
+ { id: 'flyer', name: 'Flyers', icon: '📄' },
1819
+ { id: 'logo', name: 'Logos', icon: '🏷️' },
1820
+ { id: 'business-card', name: 'Business Cards', icon: '💼' },
1821
+ { id: 'invitation', name: 'Invitations', icon: '💌' },
1822
+ { id: 'resume', name: 'Resumes', icon: '📝' },
1823
+ ];
1824
+
1825
+ view.innerHTML = `
1826
+ <div class="tc-view-header">
1827
+ <h2>Design Templates</h2>
1828
+ <p>Start with a professionally designed template</p>
1829
+ </div>
1830
+ <div class="tc-template-categories">
1831
+ ${categories.map(cat => `
1832
+ <div class="tc-template-category" data-category="${cat.id}">
1833
+ <span class="tc-category-icon">${cat.icon}</span>
1834
+ <span class="tc-category-name">${cat.name}</span>
1835
+ </div>
1836
+ `).join('')}
1837
+ </div>
1838
+ `;
1839
+
1840
+ view.querySelectorAll('.tc-template-category').forEach(el => {
1841
+ el.addEventListener('click', () => {
1842
+ const category = el.dataset.category;
1843
+ window.electron.invoke('open-external', `https://www.canva.com/templates/?query=${category}`);
1844
+ });
1845
+ });
1846
+
1847
+ return view;
1848
+ }
1849
+
1850
+ // ═══════════════════════════════════════════════════════════════
1851
+ // FEATURE 3: SCHEDULED EXPORTS VIEW
1852
+ // ═══════════════════════════════════════════════════════════════
1853
+
1854
+ _renderScheduledView() {
1855
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-scheduled-view' });
1856
+
1857
+ view.innerHTML = `
1858
+ <div class="tc-view-header">
1859
+ <h2>Scheduled Exports</h2>
1860
+ <p>Schedule designs to be exported at specific times</p>
1861
+ <button class="tc-btn tc-btn-primary tc-schedule-new-btn">+ Schedule Export</button>
1862
+ </div>
1863
+ <div class="tc-scheduled-list">
1864
+ ${this._state.scheduledExports.length === 0 ? `
1865
+ <div class="tc-empty-state">
1866
+ <div class="tc-empty-icon">📅</div>
1867
+ <h3>No scheduled exports</h3>
1868
+ <p>Schedule a design to be exported at a specific date and time</p>
1869
+ </div>
1870
+ ` : this._state.scheduledExports.map(exp => `
1871
+ <div class="tc-scheduled-item" data-id="${exp.id}">
1872
+ <div class="tc-scheduled-design">
1873
+ <img src="${exp.thumbnail}" alt="${exp.designTitle}">
1874
+ <div class="tc-scheduled-info">
1875
+ <strong>${exp.designTitle}</strong>
1876
+ <span>${exp.format.toUpperCase()} • ${new Date(exp.scheduledFor).toLocaleString()}</span>
1877
+ </div>
1878
+ </div>
1879
+ <div class="tc-scheduled-actions">
1880
+ <button class="tc-btn tc-btn-icon tc-cancel-schedule" title="Cancel">✕</button>
1881
+ </div>
1882
+ </div>
1883
+ `).join('')}
1884
+ </div>
1885
+ `;
1886
+
1887
+ view.querySelector('.tc-schedule-new-btn')?.addEventListener('click', () => {
1888
+ this._showScheduleExportModal();
1889
+ });
1890
+
1891
+ view.querySelectorAll('.tc-cancel-schedule').forEach(btn => {
1892
+ btn.addEventListener('click', (e) => {
1893
+ const id = e.target.closest('.tc-scheduled-item').dataset.id;
1894
+ this._cancelScheduledExport(id);
1895
+ });
1896
+ });
1897
+
1898
+ return view;
1899
+ }
1900
+
1901
+ _showScheduleExportModal() {
1902
+ if (this._state.designs.length === 0) {
1903
+ alert('No designs available. Please load your designs first.');
1904
+ return;
1905
+ }
1906
+
1907
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
1908
+ modal.innerHTML = `
1909
+ <div class="tc-schedule-modal">
1910
+ <div class="tc-modal-header">
1911
+ <h3>Schedule Export</h3>
1912
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
1913
+ </div>
1914
+ <div class="tc-modal-body">
1915
+ <div class="tc-form-group">
1916
+ <label>Select Design</label>
1917
+ <select class="tc-input tc-design-select">
1918
+ ${this._state.designs.map(d => `<option value="${d.id}">${d.title || 'Untitled'}</option>`).join('')}
1919
+ </select>
1920
+ </div>
1921
+ <div class="tc-form-group">
1922
+ <label>Export Format</label>
1923
+ <select class="tc-input tc-format-select">
1924
+ <option value="png">PNG</option>
1925
+ <option value="jpg">JPG</option>
1926
+ <option value="pdf">PDF</option>
1927
+ </select>
1928
+ </div>
1929
+ <div class="tc-form-group">
1930
+ <label>Schedule For</label>
1931
+ <input type="datetime-local" class="tc-input tc-schedule-datetime">
1932
+ </div>
1933
+ </div>
1934
+ <div class="tc-modal-footer">
1935
+ <button class="tc-btn tc-btn-secondary tc-modal-cancel">Cancel</button>
1936
+ <button class="tc-btn tc-btn-primary tc-schedule-confirm">Schedule</button>
1937
+ </div>
1938
+ </div>
1939
+ `;
1940
+
1941
+ const close = () => modal.remove();
1942
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
1943
+ modal.querySelector('.tc-modal-cancel').addEventListener('click', close);
1944
+
1945
+ modal.querySelector('.tc-schedule-confirm').addEventListener('click', async () => {
1946
+ const designId = modal.querySelector('.tc-design-select').value;
1947
+ const format = modal.querySelector('.tc-format-select').value;
1948
+ const datetime = modal.querySelector('.tc-schedule-datetime').value;
1949
+
1950
+ if (!datetime) {
1951
+ alert('Please select a date and time');
1952
+ return;
1953
+ }
1954
+
1955
+ const design = this._state.designs.find(d => d.id === designId);
1956
+ const scheduled = {
1957
+ id: `sched_${Date.now()}`,
1958
+ designId,
1959
+ designTitle: design?.title || 'Untitled',
1960
+ thumbnail: design?.thumbnail?.url || '',
1961
+ format,
1962
+ scheduledFor: new Date(datetime).toISOString(),
1963
+ createdAt: new Date().toISOString(),
1964
+ };
1965
+
1966
+ this._state.scheduledExports.push(scheduled);
1967
+ await this._persistScheduledExports();
1968
+ this._showToast('Export scheduled');
1969
+ close();
1970
+ this._render();
1971
+ });
1972
+
1973
+ this._container.appendChild(modal);
1974
+ }
1975
+
1976
+ async _persistScheduledExports() {
1977
+ try {
1978
+ await window.electron.invoke('save-canva-scheduled-exports', {
1979
+ userId: this._config.currentUserId,
1980
+ exports: this._state.scheduledExports
1981
+ });
1982
+ } catch (err) {
1983
+ console.error('[TacelCanva] Failed to save scheduled exports:', err);
1984
+ }
1985
+ }
1986
+
1987
+ async _cancelScheduledExport(id) {
1988
+ this._state.scheduledExports = this._state.scheduledExports.filter(e => e.id !== id);
1989
+ await this._persistScheduledExports();
1990
+ this._showToast('Export cancelled');
1991
+ this._render();
1992
+ }
1993
+
1994
+ // ═══════════════════════════════════════════════════════════════
1995
+ // FEATURE 4: ANNOTATIONS (added to context menu and design cards)
1996
+ // ═══════════════════════════════════════════════════════════════
1997
+
1998
+ _showAnnotationsModal(design) {
1999
+ const existing = this._state.annotations[design.id] || { notes: '', tags: [], linkedTaskId: '' };
2000
+
2001
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
2002
+ modal.innerHTML = `
2003
+ <div class="tc-annotations-modal">
2004
+ <div class="tc-modal-header">
2005
+ <h3>Notes & Tags</h3>
2006
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
2007
+ </div>
2008
+ <div class="tc-modal-body">
2009
+ <div class="tc-design-preview-small">
2010
+ <img src="${design.thumbnail?.url || ''}" alt="${design.title}">
2011
+ <span>${design.title || 'Untitled'}</span>
2012
+ </div>
2013
+ <div class="tc-form-group">
2014
+ <label>Notes</label>
2015
+ <textarea class="tc-input tc-notes-input" rows="4" placeholder="Add notes about this design...">${existing.notes}</textarea>
2016
+ </div>
2017
+ <div class="tc-form-group">
2018
+ <label>Tags (comma separated)</label>
2019
+ <input type="text" class="tc-input tc-tags-input" placeholder="e.g., marketing, Q1, approved" value="${existing.tags.join(', ')}">
2020
+ </div>
2021
+ <div class="tc-form-group">
2022
+ <label>Link to Task ID (optional)</label>
2023
+ <input type="text" class="tc-input tc-task-input" placeholder="e.g., TASK-123" value="${existing.linkedTaskId}">
2024
+ </div>
2025
+ </div>
2026
+ <div class="tc-modal-footer">
2027
+ <button class="tc-btn tc-btn-secondary tc-modal-cancel">Cancel</button>
2028
+ <button class="tc-btn tc-btn-primary tc-save-annotations">Save</button>
2029
+ </div>
2030
+ </div>
2031
+ `;
2032
+
2033
+ const close = () => modal.remove();
2034
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
2035
+ modal.querySelector('.tc-modal-cancel').addEventListener('click', close);
2036
+
2037
+ modal.querySelector('.tc-save-annotations').addEventListener('click', async () => {
2038
+ const notes = modal.querySelector('.tc-notes-input').value;
2039
+ const tagsStr = modal.querySelector('.tc-tags-input').value;
2040
+ const linkedTaskId = modal.querySelector('.tc-task-input').value;
2041
+
2042
+ this._state.annotations[design.id] = {
2043
+ notes,
2044
+ tags: tagsStr.split(',').map(t => t.trim()).filter(Boolean),
2045
+ linkedTaskId,
2046
+ updatedAt: new Date().toISOString(),
2047
+ };
2048
+
2049
+ await this._persistAnnotations();
2050
+ this._showToast('Annotations saved');
2051
+ close();
2052
+ });
2053
+
2054
+ this._container.appendChild(modal);
2055
+ }
2056
+
2057
+ async _persistAnnotations() {
2058
+ try {
2059
+ await window.electron.invoke('save-canva-annotations', {
2060
+ userId: this._config.currentUserId,
2061
+ annotations: this._state.annotations
2062
+ });
2063
+ } catch (err) {
2064
+ console.error('[TacelCanva] Failed to save annotations:', err);
2065
+ }
2066
+ }
2067
+
2068
+ // ═══════════════════════════════════════════════════════════════
2069
+ // FEATURE 5: QUICK INSERT (added to context menu)
2070
+ // ═══════════════════════════════════════════════════════════════
2071
+
2072
+ _showQuickInsertMenu(design, event) {
2073
+ const menu = DomUtils.el('div', { className: 'tc-quick-insert-menu' });
2074
+ menu.style.left = `${event.clientX}px`;
2075
+ menu.style.top = `${event.clientY}px`;
2076
+
2077
+ const options = [
2078
+ { icon: '📧', label: 'Copy for Email', action: 'email' },
2079
+ { icon: '📋', label: 'Copy Image URL', action: 'url' },
2080
+ { icon: '💾', label: 'Download & Copy Path', action: 'download' },
2081
+ ];
2082
+
2083
+ menu.innerHTML = `
2084
+ <div class="tc-insert-header">Insert "${design.title || 'Design'}"</div>
2085
+ ${options.map(opt => `
2086
+ <div class="tc-insert-option" data-action="${opt.action}">
2087
+ <span>${opt.icon}</span>
2088
+ <span>${opt.label}</span>
2089
+ </div>
2090
+ `).join('')}
2091
+ `;
2092
+
2093
+ menu.querySelectorAll('.tc-insert-option').forEach(opt => {
2094
+ opt.addEventListener('click', async () => {
2095
+ const action = opt.dataset.action;
2096
+ menu.remove();
2097
+
2098
+ if (action === 'url') {
2099
+ navigator.clipboard.writeText(design.thumbnail?.url || '');
2100
+ this._showToast('Image URL copied');
2101
+ } else if (action === 'email') {
2102
+ // Copy as HTML img tag
2103
+ const html = `<img src="${design.thumbnail?.url}" alt="${design.title}" style="max-width: 100%;">`;
2104
+ navigator.clipboard.writeText(html);
2105
+ this._showToast('HTML copied for email');
2106
+ } else if (action === 'download') {
2107
+ this._showToast('Download started...');
2108
+ // Trigger download via IPC
2109
+ await window.electron.invoke('download-canva-design', {
2110
+ url: design.thumbnail?.url,
2111
+ filename: `${design.title || 'design'}.png`
2112
+ });
2113
+ }
2114
+ });
2115
+ });
2116
+
2117
+ const closeHandler = (e) => {
2118
+ if (!menu.contains(e.target)) {
2119
+ menu.remove();
2120
+ document.removeEventListener('click', closeHandler);
2121
+ }
2122
+ };
2123
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
2124
+
2125
+ this._container.appendChild(menu);
2126
+ }
2127
+
2128
+ // ═══════════════════════════════════════════════════════════════
2129
+ // FEATURE 6: COMPARE VIEW
2130
+ // ═══════════════════════════════════════════════════════════════
2131
+
2132
+ _renderCompareView() {
2133
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-compare-view' });
2134
+
2135
+ const compareDesigns = this._state.compareDesigns
2136
+ .map(id => this._state.designs.find(d => d.id === id))
2137
+ .filter(Boolean);
2138
+
2139
+ view.innerHTML = `
2140
+ <div class="tc-view-header">
2141
+ <h2>Compare Designs</h2>
2142
+ <p>View designs side by side</p>
2143
+ ${compareDesigns.length > 0 ? `<button class="tc-btn tc-btn-secondary tc-clear-compare">Clear All</button>` : ''}
2144
+ </div>
2145
+ <div class="tc-compare-container">
2146
+ ${compareDesigns.length === 0 ? `
2147
+ <div class="tc-empty-state">
2148
+ <div class="tc-empty-icon">⚖️</div>
2149
+ <h3>No designs to compare</h3>
2150
+ <p>Right-click on designs and select "Add to Compare" to compare them side by side</p>
2151
+ </div>
2152
+ ` : `
2153
+ <div class="tc-compare-grid" style="grid-template-columns: repeat(${Math.min(compareDesigns.length, 4)}, 1fr);">
2154
+ ${compareDesigns.map(d => `
2155
+ <div class="tc-compare-item" data-id="${d.id}">
2156
+ <button class="tc-remove-compare" title="Remove">✕</button>
2157
+ <img src="${d.thumbnail?.url || ''}" alt="${d.title}">
2158
+ <div class="tc-compare-info">
2159
+ <strong>${d.title || 'Untitled'}</strong>
2160
+ <span>Updated: ${new Date(d.updated_at).toLocaleDateString()}</span>
2161
+ </div>
2162
+ </div>
2163
+ `).join('')}
2164
+ </div>
2165
+ `}
2166
+ </div>
2167
+ `;
2168
+
2169
+ view.querySelector('.tc-clear-compare')?.addEventListener('click', () => {
2170
+ this._state.compareDesigns = [];
2171
+ this._render();
2172
+ });
2173
+
2174
+ view.querySelectorAll('.tc-remove-compare').forEach(btn => {
2175
+ btn.addEventListener('click', (e) => {
2176
+ const id = e.target.closest('.tc-compare-item').dataset.id;
2177
+ this._state.compareDesigns = this._state.compareDesigns.filter(i => i !== id);
2178
+ this._render();
2179
+ });
2180
+ });
2181
+
2182
+ return view;
2183
+ }
2184
+
2185
+ _addToCompare(designId) {
2186
+ if (!this._state.compareDesigns.includes(designId)) {
2187
+ if (this._state.compareDesigns.length >= 4) {
2188
+ this._showToast('Maximum 4 designs for comparison');
2189
+ return;
2190
+ }
2191
+ this._state.compareDesigns.push(designId);
2192
+ this._showToast('Added to compare');
2193
+ }
2194
+ }
2195
+
2196
+ // ═══════════════════════════════════════════════════════════════
2197
+ // FEATURE 7: ASSET LIBRARY VIEW
2198
+ // ═══════════════════════════════════════════════════════════════
2199
+
2200
+ _renderAssetLibraryView() {
2201
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-assets-view' });
2202
+
2203
+ const assets = this._state.assetLibrary
2204
+ .map(id => this._state.designs.find(d => d.id === id))
2205
+ .filter(Boolean);
2206
+
2207
+ view.innerHTML = `
2208
+ <div class="tc-view-header">
2209
+ <h2>Asset Library</h2>
2210
+ <p>Quick access to your reusable design assets</p>
2211
+ </div>
2212
+ <div class="tc-assets-grid">
2213
+ ${assets.length === 0 ? `
2214
+ <div class="tc-empty-state">
2215
+ <div class="tc-empty-icon">📦</div>
2216
+ <h3>No assets yet</h3>
2217
+ <p>Right-click on designs and select "Add to Asset Library" to save them here for quick access</p>
2218
+ </div>
2219
+ ` : assets.map(d => `
2220
+ <div class="tc-asset-card" data-id="${d.id}">
2221
+ <img src="${d.thumbnail?.url || ''}" alt="${d.title}">
2222
+ <div class="tc-asset-overlay">
2223
+ <button class="tc-asset-action tc-copy-asset" title="Copy URL">📋</button>
2224
+ <button class="tc-asset-action tc-download-asset" title="Download">⬇️</button>
2225
+ <button class="tc-asset-action tc-remove-asset" title="Remove">✕</button>
2226
+ </div>
2227
+ <div class="tc-asset-name">${d.title || 'Untitled'}</div>
2228
+ </div>
2229
+ `).join('')}
2230
+ </div>
2231
+ `;
2232
+
2233
+ view.querySelectorAll('.tc-copy-asset').forEach(btn => {
2234
+ btn.addEventListener('click', (e) => {
2235
+ e.stopPropagation();
2236
+ const id = e.target.closest('.tc-asset-card').dataset.id;
2237
+ const design = this._state.designs.find(d => d.id === id);
2238
+ if (design?.thumbnail?.url) {
2239
+ navigator.clipboard.writeText(design.thumbnail.url);
2240
+ this._showToast('URL copied');
2241
+ }
2242
+ });
2243
+ });
2244
+
2245
+ view.querySelectorAll('.tc-remove-asset').forEach(btn => {
2246
+ btn.addEventListener('click', async (e) => {
2247
+ e.stopPropagation();
2248
+ const id = e.target.closest('.tc-asset-card').dataset.id;
2249
+ this._state.assetLibrary = this._state.assetLibrary.filter(i => i !== id);
2250
+ await this._persistAssetLibrary();
2251
+ this._render();
2252
+ });
2253
+ });
2254
+
2255
+ return view;
2256
+ }
2257
+
2258
+ async _addToAssetLibrary(designId) {
2259
+ if (!this._state.assetLibrary.includes(designId)) {
2260
+ this._state.assetLibrary.push(designId);
2261
+ await this._persistAssetLibrary();
2262
+ this._showToast('Added to Asset Library');
2263
+ }
2264
+ }
2265
+
2266
+ async _persistAssetLibrary() {
2267
+ try {
2268
+ await window.electron.invoke('save-canva-asset-library', {
2269
+ userId: this._config.currentUserId,
2270
+ assets: this._state.assetLibrary
2271
+ });
2272
+ } catch (err) {
2273
+ console.error('[TacelCanva] Failed to save asset library:', err);
2274
+ }
2275
+ }
2276
+
2277
+ // ═══════════════════════════════════════════════════════════════
2278
+ // FEATURE 8: ANALYTICS VIEW
2279
+ // ═══════════════════════════════════════════════════════════════
2280
+
2281
+ _renderAnalyticsView() {
2282
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-analytics-view' });
2283
+
2284
+ // Calculate stats
2285
+ const totalDesigns = this._state.designs.length;
2286
+ const totalFavorites = this._state.favorites.size;
2287
+ const totalAssets = this._state.assetLibrary.length;
2288
+ const recentCount = this._state.recentlyViewed.length;
2289
+
2290
+ // Most viewed designs
2291
+ const viewCounts = this._state.analytics.views || {};
2292
+ const topViewed = Object.entries(viewCounts)
2293
+ .sort((a, b) => b[1] - a[1])
2294
+ .slice(0, 5)
2295
+ .map(([id, count]) => ({ design: this._state.designs.find(d => d.id === id), count }))
2296
+ .filter(item => item.design);
2297
+
2298
+ view.innerHTML = `
2299
+ <div class="tc-view-header">
2300
+ <h2>Analytics</h2>
2301
+ <p>Track your design usage and activity</p>
2302
+ </div>
2303
+ <div class="tc-analytics-stats">
2304
+ <div class="tc-stat-card">
2305
+ <div class="tc-stat-value">${totalDesigns}</div>
2306
+ <div class="tc-stat-label">Total Designs</div>
2307
+ </div>
2308
+ <div class="tc-stat-card">
2309
+ <div class="tc-stat-value">${totalFavorites}</div>
2310
+ <div class="tc-stat-label">Favorites</div>
2311
+ </div>
2312
+ <div class="tc-stat-card">
2313
+ <div class="tc-stat-value">${totalAssets}</div>
2314
+ <div class="tc-stat-label">Assets</div>
2315
+ </div>
2316
+ <div class="tc-stat-card">
2317
+ <div class="tc-stat-value">${recentCount}</div>
2318
+ <div class="tc-stat-label">Recently Viewed</div>
2319
+ </div>
2320
+ </div>
2321
+ <div class="tc-analytics-section">
2322
+ <h3>Most Viewed Designs</h3>
2323
+ ${topViewed.length === 0 ? '<p class="tc-hint">No view data yet</p>' : `
2324
+ <div class="tc-top-list">
2325
+ ${topViewed.map((item, i) => `
2326
+ <div class="tc-top-item">
2327
+ <span class="tc-top-rank">${i + 1}</span>
2328
+ <img src="${item.design.thumbnail?.url || ''}" alt="">
2329
+ <span class="tc-top-name">${item.design.title || 'Untitled'}</span>
2330
+ <span class="tc-top-count">${item.count} views</span>
2331
+ </div>
2332
+ `).join('')}
2333
+ </div>
2334
+ `}
2335
+ </div>
2336
+ `;
2337
+
2338
+ return view;
2339
+ }
2340
+
2341
+ _trackView(designId) {
2342
+ if (!this._state.analytics.views) this._state.analytics.views = {};
2343
+ this._state.analytics.views[designId] = (this._state.analytics.views[designId] || 0) + 1;
2344
+ this._state.analytics.lastUsed[designId] = new Date().toISOString();
2345
+ this._persistAnalytics();
2346
+ }
2347
+
2348
+ async _persistAnalytics() {
2349
+ try {
2350
+ await window.electron.invoke('save-canva-analytics', {
2351
+ userId: this._config.currentUserId,
2352
+ analytics: this._state.analytics
2353
+ });
2354
+ } catch (err) {
2355
+ console.error('[TacelCanva] Failed to save analytics:', err);
2356
+ }
2357
+ }
2358
+
2359
+ // ═══════════════════════════════════════════════════════════════
2360
+ // FEATURE 9: BATCH EXPORT (added to bulk actions)
2361
+ // ═══════════════════════════════════════════════════════════════
2362
+
2363
+ _showBatchExportModal() {
2364
+ const selected = Array.from(this._state.selectedDesigns);
2365
+ if (selected.length === 0) {
2366
+ alert('Please select designs to export');
2367
+ return;
2368
+ }
2369
+
2370
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
2371
+ modal.innerHTML = `
2372
+ <div class="tc-batch-export-modal">
2373
+ <div class="tc-modal-header">
2374
+ <h3>Batch Export (${selected.length} designs)</h3>
2375
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
2376
+ </div>
2377
+ <div class="tc-modal-body">
2378
+ <div class="tc-form-group">
2379
+ <label>Export Preset</label>
2380
+ <select class="tc-input tc-preset-select">
2381
+ <option value="png-standard">PNG Standard (1080px)</option>
2382
+ <option value="jpg-web">JPG Web Quality</option>
2383
+ <option value="pdf-print">PDF Print Quality</option>
2384
+ <option value="social-pack">Social Media Pack (multiple sizes)</option>
2385
+ </select>
2386
+ </div>
2387
+ <div class="tc-form-group">
2388
+ <label>File Naming</label>
2389
+ <select class="tc-input tc-naming-select">
2390
+ <option value="original">Original Name</option>
2391
+ <option value="date-prefix">Date Prefix (YYYY-MM-DD_name)</option>
2392
+ <option value="numbered">Numbered (001_name, 002_name)</option>
2393
+ </select>
2394
+ </div>
2395
+ <div class="tc-selected-preview">
2396
+ ${selected.slice(0, 4).map(id => {
2397
+ const d = this._state.designs.find(design => design.id === id);
2398
+ return d ? `<img src="${d.thumbnail?.url || ''}" alt="">` : '';
2399
+ }).join('')}
2400
+ ${selected.length > 4 ? `<span>+${selected.length - 4} more</span>` : ''}
2401
+ </div>
2402
+ </div>
2403
+ <div class="tc-modal-footer">
2404
+ <button class="tc-btn tc-btn-secondary tc-modal-cancel">Cancel</button>
2405
+ <button class="tc-btn tc-btn-primary tc-start-batch">Export All</button>
2406
+ </div>
2407
+ </div>
2408
+ `;
2409
+
2410
+ const close = () => modal.remove();
2411
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
2412
+ modal.querySelector('.tc-modal-cancel').addEventListener('click', close);
2413
+
2414
+ modal.querySelector('.tc-start-batch').addEventListener('click', () => {
2415
+ const preset = modal.querySelector('.tc-preset-select').value;
2416
+ const naming = modal.querySelector('.tc-naming-select').value;
2417
+ this._executeBatchExport(selected, preset, naming);
2418
+ close();
2419
+ });
2420
+
2421
+ this._container.appendChild(modal);
2422
+ }
2423
+
2424
+ async _executeBatchExport(designIds, preset, naming) {
2425
+ this._showToast(`Exporting ${designIds.length} designs...`);
2426
+
2427
+ // In a real implementation, this would call the Canva export API for each design
2428
+ // For now, we'll simulate the process
2429
+ for (let i = 0; i < designIds.length; i++) {
2430
+ const design = this._state.designs.find(d => d.id === designIds[i]);
2431
+ if (design) {
2432
+ console.log(`[TacelCanva] Exporting ${i + 1}/${designIds.length}: ${design.title}`);
2433
+ // await this._exportDesign(design);
2434
+ }
2435
+ }
2436
+
2437
+ this._showToast('Batch export complete!');
2438
+ this._state.selectedDesigns.clear();
2439
+ this._render();
2440
+ }
2441
+
2442
+ // ═══════════════════════════════════════════════════════════════
2443
+ // FEATURE 10: DESIGN REQUESTS VIEW
2444
+ // ═══════════════════════════════════════════════════════════════
2445
+
2446
+ _renderRequestsView() {
2447
+ const view = DomUtils.el('div', { className: 'tc-feature-view tc-requests-view' });
2448
+
2449
+ const pending = this._state.designRequests.filter(r => r.status === 'pending');
2450
+ const completed = this._state.designRequests.filter(r => r.status === 'completed');
2451
+
2452
+ view.innerHTML = `
2453
+ <div class="tc-view-header">
2454
+ <h2>Design Requests</h2>
2455
+ <p>Submit and track design requests</p>
2456
+ <button class="tc-btn tc-btn-primary tc-new-request-btn">+ New Request</button>
2457
+ </div>
2458
+ <div class="tc-requests-tabs">
2459
+ <button class="tc-tab active" data-tab="pending">Pending (${pending.length})</button>
2460
+ <button class="tc-tab" data-tab="completed">Completed (${completed.length})</button>
2461
+ </div>
2462
+ <div class="tc-requests-list" id="requests-list">
2463
+ ${this._renderRequestsList(pending)}
2464
+ </div>
2465
+ `;
2466
+
2467
+ view.querySelector('.tc-new-request-btn').addEventListener('click', () => {
2468
+ this._showNewRequestModal();
2469
+ });
2470
+
2471
+ view.querySelectorAll('.tc-tab').forEach(tab => {
2472
+ tab.addEventListener('click', () => {
2473
+ view.querySelectorAll('.tc-tab').forEach(t => t.classList.remove('active'));
2474
+ tab.classList.add('active');
2475
+ const list = tab.dataset.tab === 'pending' ? pending : completed;
2476
+ view.querySelector('#requests-list').innerHTML = this._renderRequestsList(list);
2477
+ });
2478
+ });
2479
+
2480
+ return view;
2481
+ }
2482
+
2483
+ _renderRequestsList(requests) {
2484
+ if (requests.length === 0) {
2485
+ return `
2486
+ <div class="tc-empty-state">
2487
+ <div class="tc-empty-icon">📝</div>
2488
+ <h3>No requests</h3>
2489
+ </div>
2490
+ `;
2491
+ }
2492
+
2493
+ return requests.map(req => `
2494
+ <div class="tc-request-card" data-id="${req.id}">
2495
+ <div class="tc-request-header">
2496
+ <span class="tc-request-title">${req.title}</span>
2497
+ <span class="tc-request-status tc-status-${req.status}">${req.status}</span>
2498
+ </div>
2499
+ <p class="tc-request-desc">${req.description}</p>
2500
+ <div class="tc-request-meta">
2501
+ <span>📐 ${req.dimensions || 'Any size'}</span>
2502
+ <span>📅 Due: ${req.deadline ? new Date(req.deadline).toLocaleDateString() : 'No deadline'}</span>
2503
+ <span>👤 ${req.requestedBy}</span>
2504
+ </div>
2505
+ ${req.status === 'pending' ? `
2506
+ <div class="tc-request-actions">
2507
+ <button class="tc-btn tc-btn-sm tc-complete-request">Mark Complete</button>
2508
+ <button class="tc-btn tc-btn-sm tc-btn-secondary tc-cancel-request">Cancel</button>
2509
+ </div>
2510
+ ` : ''}
2511
+ </div>
2512
+ `).join('');
2513
+ }
2514
+
2515
+ _showNewRequestModal() {
2516
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
2517
+ modal.innerHTML = `
2518
+ <div class="tc-request-modal">
2519
+ <div class="tc-modal-header">
2520
+ <h3>New Design Request</h3>
2521
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
2522
+ </div>
2523
+ <div class="tc-modal-body">
2524
+ <div class="tc-form-group">
2525
+ <label>Title *</label>
2526
+ <input type="text" class="tc-input tc-request-title" placeholder="e.g., Social media banner for Q2 campaign">
2527
+ </div>
2528
+ <div class="tc-form-group">
2529
+ <label>Description</label>
2530
+ <textarea class="tc-input tc-request-desc" rows="3" placeholder="Describe what you need..."></textarea>
2531
+ </div>
2532
+ <div class="tc-form-row">
2533
+ <div class="tc-form-group">
2534
+ <label>Dimensions</label>
2535
+ <select class="tc-input tc-request-dims">
2536
+ <option value="">Any size</option>
2537
+ <option value="1080x1080">Instagram Post (1080x1080)</option>
2538
+ <option value="1200x628">Facebook/LinkedIn (1200x628)</option>
2539
+ <option value="1920x1080">Presentation (1920x1080)</option>
2540
+ <option value="custom">Custom...</option>
2541
+ </select>
2542
+ </div>
2543
+ <div class="tc-form-group">
2544
+ <label>Deadline</label>
2545
+ <input type="date" class="tc-input tc-request-deadline">
2546
+ </div>
2547
+ </div>
2548
+ </div>
2549
+ <div class="tc-modal-footer">
2550
+ <button class="tc-btn tc-btn-secondary tc-modal-cancel">Cancel</button>
2551
+ <button class="tc-btn tc-btn-primary tc-submit-request">Submit Request</button>
2552
+ </div>
2553
+ </div>
2554
+ `;
2555
+
2556
+ const close = () => modal.remove();
2557
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
2558
+ modal.querySelector('.tc-modal-cancel').addEventListener('click', close);
2559
+
2560
+ modal.querySelector('.tc-submit-request').addEventListener('click', async () => {
2561
+ const title = modal.querySelector('.tc-request-title').value.trim();
2562
+ if (!title) {
2563
+ alert('Please enter a title');
2564
+ return;
2565
+ }
2566
+
2567
+ const request = {
2568
+ id: `req_${Date.now()}`,
2569
+ title,
2570
+ description: modal.querySelector('.tc-request-desc').value,
2571
+ dimensions: modal.querySelector('.tc-request-dims').value,
2572
+ deadline: modal.querySelector('.tc-request-deadline').value || null,
2573
+ requestedBy: this._config.currentUserName || 'Unknown',
2574
+ status: 'pending',
2575
+ createdAt: new Date().toISOString(),
2576
+ };
2577
+
2578
+ this._state.designRequests.push(request);
2579
+ await this._persistDesignRequests();
2580
+ this._showToast('Request submitted');
2581
+ close();
2582
+ this._render();
2583
+ });
2584
+
2585
+ this._container.appendChild(modal);
2586
+ }
2587
+
2588
+ async _persistDesignRequests() {
2589
+ try {
2590
+ await window.electron.invoke('save-canva-design-requests', {
2591
+ userId: this._config.currentUserId,
2592
+ requests: this._state.designRequests
2593
+ });
2594
+ } catch (err) {
2595
+ console.error('[TacelCanva] Failed to save design requests:', err);
2596
+ }
2597
+ }
2598
+
2599
+ // ═══════════════════════════════════════════════════════════════
2600
+ // LOAD ALL FEATURE DATA
2601
+ // ═══════════════════════════════════════════════════════════════
2602
+
2603
+ async _loadAllFeatureData() {
2604
+ try {
2605
+ // Load annotations
2606
+ const annotations = await window.electron.invoke('get-canva-annotations', this._config.currentUserId);
2607
+ if (annotations) this._state.annotations = annotations;
2608
+
2609
+ // Load asset library
2610
+ const assets = await window.electron.invoke('get-canva-asset-library', this._config.currentUserId);
2611
+ if (assets) this._state.assetLibrary = assets;
2612
+
2613
+ // Load analytics
2614
+ const analytics = await window.electron.invoke('get-canva-analytics', this._config.currentUserId);
2615
+ if (analytics) this._state.analytics = analytics;
2616
+
2617
+ // Load scheduled exports
2618
+ const scheduled = await window.electron.invoke('get-canva-scheduled-exports', this._config.currentUserId);
2619
+ if (scheduled) this._state.scheduledExports = scheduled;
2620
+
2621
+ // Load design requests
2622
+ const requests = await window.electron.invoke('get-canva-design-requests', this._config.currentUserId);
2623
+ if (requests) this._state.designRequests = requests;
2624
+
2625
+ } catch (err) {
2626
+ console.error('[TacelCanva] Failed to load feature data:', err);
2627
+ }
2628
+ }
2629
+
2630
+ // ═══════════════════════════════════════════════════════════════
2631
+ // INLINE EDITOR - Open design in embedded view or new window
2632
+ // ═══════════════════════════════════════════════════════════════
2633
+
2634
+ _showEditorModal(design) {
2635
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
2636
+ const editUrl = `https://www.canva.com/design/${design.id}/edit`;
2637
+
2638
+ modal.innerHTML = `
2639
+ <div class="tc-editor-modal">
2640
+ <div class="tc-modal-header">
2641
+ <h3>
2642
+ <span>✏️</span>
2643
+ <span>${design.title || 'Untitled Design'}</span>
2644
+ </h3>
2645
+ <div class="tc-editor-actions">
2646
+ <button class="tc-btn tc-btn-secondary tc-btn-small tc-open-external" title="Open in browser">
2647
+ <span>🔗</span> Open in Browser
2648
+ </button>
2649
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
2650
+ </div>
2651
+ </div>
2652
+ <div class="tc-editor-body">
2653
+ <div class="tc-editor-loading">
2654
+ <div class="tc-loader-spinner"></div>
2655
+ <p>Loading Canva editor...</p>
2656
+ <p class="tc-hint">If the editor doesn't load, click "Open in Browser"</p>
2657
+ </div>
2658
+ <iframe
2659
+ src="${editUrl}"
2660
+ class="tc-editor-iframe"
2661
+ allow="clipboard-read; clipboard-write"
2662
+ sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals"
2663
+ ></iframe>
2664
+ </div>
2665
+ </div>
2666
+ `;
2667
+
2668
+ const close = () => {
2669
+ modal.classList.add('closing');
2670
+ setTimeout(() => modal.remove(), 200);
2671
+ };
2672
+
2673
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
2674
+ modal.querySelector('.tc-open-external').addEventListener('click', () => {
2675
+ this._openDesign(design);
2676
+ close();
2677
+ });
2678
+ modal.addEventListener('click', (e) => {
2679
+ if (e.target === modal) close();
2680
+ });
2681
+
2682
+ // Handle iframe load
2683
+ const iframe = modal.querySelector('.tc-editor-iframe');
2684
+ const loading = modal.querySelector('.tc-editor-loading');
2685
+ iframe.addEventListener('load', () => {
2686
+ loading.style.display = 'none';
2687
+ iframe.style.display = 'block';
2688
+ });
2689
+ iframe.addEventListener('error', () => {
2690
+ loading.innerHTML = `
2691
+ <div class="tc-empty-icon">⚠️</div>
2692
+ <h3>Unable to load editor</h3>
2693
+ <p>Canva's editor cannot be embedded. Click below to open in your browser.</p>
2694
+ <button class="tc-btn tc-btn-primary tc-open-browser">Open in Browser</button>
2695
+ `;
2696
+ loading.querySelector('.tc-open-browser').addEventListener('click', () => {
2697
+ this._openDesign(design);
2698
+ close();
2699
+ });
2700
+ });
2701
+
2702
+ this._container.appendChild(modal);
2703
+ this._trackView(design.id);
2704
+ }
2705
+
2706
+ // ═══════════════════════════════════════════════════════════════
2707
+ // PREMIUM TOAST NOTIFICATION SYSTEM
2708
+ // ═══════════════════════════════════════════════════════════════
2709
+
2710
+ _initToastContainer() {
2711
+ if (!this._toastContainer) {
2712
+ this._toastContainer = DomUtils.el('div', { className: 'tc-toast-container' });
2713
+ this._container.appendChild(this._toastContainer);
2714
+ }
2715
+ }
2716
+
2717
+ _showToast(message, options = {}) {
2718
+ this._initToastContainer();
2719
+
2720
+ const { type = 'info', title, duration = 4000, action } = options;
2721
+ const icons = {
2722
+ success: '✓',
2723
+ error: '✕',
2724
+ warning: '⚠',
2725
+ info: 'ℹ'
2726
+ };
2727
+
2728
+ const toast = DomUtils.el('div', { className: `tc-toast ${type}` });
2729
+ toast.innerHTML = `
2730
+ <span class="tc-toast-icon">${icons[type] || icons.info}</span>
2731
+ <div class="tc-toast-content">
2732
+ ${title ? `<div class="tc-toast-title">${title}</div>` : ''}
2733
+ <div class="tc-toast-message">${message}</div>
2734
+ </div>
2735
+ ${action ? `<button class="tc-btn tc-btn-small tc-btn-secondary tc-toast-action">${action.label}</button>` : ''}
2736
+ <button class="tc-toast-close">✕</button>
2737
+ `;
2738
+
2739
+ const dismiss = () => {
2740
+ toast.classList.remove('show');
2741
+ setTimeout(() => toast.remove(), 400);
2742
+ };
2743
+
2744
+ toast.querySelector('.tc-toast-close').addEventListener('click', dismiss);
2745
+ if (action) {
2746
+ toast.querySelector('.tc-toast-action').addEventListener('click', () => {
2747
+ action.onClick();
2748
+ dismiss();
2749
+ });
2750
+ }
2751
+
2752
+ this._toastContainer.appendChild(toast);
2753
+
2754
+ // Trigger animation
2755
+ requestAnimationFrame(() => {
2756
+ toast.classList.add('show');
2757
+ });
2758
+
2759
+ // Auto dismiss
2760
+ if (duration > 0) {
2761
+ setTimeout(dismiss, duration);
2762
+ }
2763
+
2764
+ return { dismiss };
2765
+ }
2766
+
2767
+ // ═══════════════════════════════════════════════════════════════
2768
+ // KEYBOARD SHORTCUTS
2769
+ // ═══════════════════════════════════════════════════════════════
2770
+
2771
+ _initKeyboardShortcuts() {
2772
+ this._keyboardHandler = (e) => {
2773
+ // Don't trigger if typing in input
2774
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
2775
+
2776
+ const key = e.key.toLowerCase();
2777
+ const ctrl = e.ctrlKey || e.metaKey;
2778
+ const shift = e.shiftKey;
2779
+
2780
+ // Search: Ctrl+F or /
2781
+ if ((ctrl && key === 'f') || key === '/') {
2782
+ e.preventDefault();
2783
+ const searchInput = this._container.querySelector('.tc-search-input');
2784
+ if (searchInput) searchInput.focus();
2785
+ return;
2786
+ }
2787
+
2788
+ // Toggle view: V
2789
+ if (key === 'v' && !ctrl) {
2790
+ this._state.viewMode = this._state.viewMode === 'grid' ? 'list' : 'grid';
2791
+ this._render();
2792
+ return;
2793
+ }
2794
+
2795
+ // Select all: Ctrl+A
2796
+ if (ctrl && key === 'a' && this._state.currentView === 'gallery') {
2797
+ e.preventDefault();
2798
+ this._state.designs.forEach(d => this._state.selectedDesigns.add(d.id));
2799
+ this._render();
2800
+ return;
2801
+ }
2802
+
2803
+ // Deselect all: Escape
2804
+ if (key === 'escape') {
2805
+ if (this._state.selectedDesigns.size > 0) {
2806
+ this._state.selectedDesigns.clear();
2807
+ this._render();
2808
+ }
2809
+ this._closeContextMenu();
2810
+ return;
2811
+ }
2812
+
2813
+ // Help: ?
2814
+ if (key === '?' || (shift && key === '/')) {
2815
+ this._showKeyboardHelp();
2816
+ return;
2817
+ }
2818
+
2819
+ // Navigate views: 1-9
2820
+ const viewKeys = {
2821
+ '1': 'gallery',
2822
+ '2': 'favorites',
2823
+ '3': 'brandKit',
2824
+ '4': 'templates',
2825
+ '5': 'assets',
2826
+ '6': 'compare',
2827
+ '7': 'scheduled',
2828
+ '8': 'analytics',
2829
+ '9': 'requests'
2830
+ };
2831
+ if (viewKeys[key] && !ctrl) {
2832
+ this._state.currentView = viewKeys[key];
2833
+ if (key === '2') {
2834
+ this._state.filterType = 'favorites';
2835
+ this._state.currentView = 'gallery';
2836
+ }
2837
+ this._render();
2838
+ return;
2839
+ }
2840
+ };
2841
+
2842
+ document.addEventListener('keydown', this._keyboardHandler);
2843
+ }
2844
+
2845
+ _showKeyboardHelp() {
2846
+ const modal = DomUtils.el('div', { className: 'tc-modal-overlay' });
2847
+ modal.innerHTML = `
2848
+ <div class="tc-help-modal">
2849
+ <div class="tc-modal-header">
2850
+ <h3>⌨️ Keyboard Shortcuts</h3>
2851
+ <button class="tc-btn tc-btn-icon tc-modal-close">✕</button>
2852
+ </div>
2853
+ <div class="tc-modal-body">
2854
+ <div class="tc-shortcuts-grid">
2855
+ <div class="tc-shortcut-section">
2856
+ <h4>Navigation</h4>
2857
+ <div class="tc-shortcut"><kbd>1</kbd> <span>My Designs</span></div>
2858
+ <div class="tc-shortcut"><kbd>2</kbd> <span>Favorites</span></div>
2859
+ <div class="tc-shortcut"><kbd>3</kbd> <span>Brand Kit</span></div>
2860
+ <div class="tc-shortcut"><kbd>4</kbd> <span>Templates</span></div>
2861
+ <div class="tc-shortcut"><kbd>5</kbd> <span>Asset Library</span></div>
2862
+ <div class="tc-shortcut"><kbd>6</kbd> <span>Compare</span></div>
2863
+ <div class="tc-shortcut"><kbd>7</kbd> <span>Scheduled</span></div>
2864
+ <div class="tc-shortcut"><kbd>8</kbd> <span>Analytics</span></div>
2865
+ <div class="tc-shortcut"><kbd>9</kbd> <span>Requests</span></div>
2866
+ </div>
2867
+ <div class="tc-shortcut-section">
2868
+ <h4>Actions</h4>
2869
+ <div class="tc-shortcut"><kbd>/</kbd> or <kbd>Ctrl+F</kbd> <span>Search</span></div>
2870
+ <div class="tc-shortcut"><kbd>V</kbd> <span>Toggle Grid/List</span></div>
2871
+ <div class="tc-shortcut"><kbd>Ctrl+A</kbd> <span>Select All</span></div>
2872
+ <div class="tc-shortcut"><kbd>Esc</kbd> <span>Deselect / Close</span></div>
2873
+ <div class="tc-shortcut"><kbd>?</kbd> <span>Show Help</span></div>
2874
+ </div>
2875
+ </div>
2876
+ </div>
2877
+ </div>
2878
+ `;
2879
+
2880
+ const close = () => modal.remove();
2881
+ modal.querySelector('.tc-modal-close').addEventListener('click', close);
2882
+ modal.addEventListener('click', (e) => {
2883
+ if (e.target === modal) close();
2884
+ });
2885
+
2886
+ this._container.appendChild(modal);
2887
+ }
2888
+
2889
+ _removeKeyboardShortcuts() {
2890
+ if (this._keyboardHandler) {
2891
+ document.removeEventListener('keydown', this._keyboardHandler);
2892
+ }
2893
+ }
2894
+
2895
+ destroy() {
2896
+ this._removeKeyboardShortcuts();
2897
+ DomUtils.clear(this._container);
2898
+ this._container.classList.remove('tacel-canva');
2899
+ }
2900
+ }
2901
+
2902
+ const TacelCanva = {
2903
+ initialize(container, config) {
2904
+ return new TacelCanvaInstance(container, config);
2905
+ }
2906
+ };
2907
+
2908
+ if (typeof module !== 'undefined' && module.exports) {
2909
+ module.exports = { TacelCanva, TacelCanvaInstance };
2910
+ }