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/README.md +637 -0
- package/canva-api.js +271 -0
- package/canva.css +2928 -0
- package/canva.js +2910 -0
- package/index.js +20 -0
- package/package.json +38 -0
- package/themes.js +705 -0
- package/utils/dom.js +98 -0
- package/utils/pkce.js +57 -0
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
|
+
}
|