@carmaclouds/core 2.3.1

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.
Files changed (118) hide show
  1. package/dist/cache/CacheManager.d.ts.map +1 -0
  2. package/dist/cache/CacheManager.js +131 -0
  3. package/dist/cache/CacheManager.js.map +1 -0
  4. package/dist/index.d.ts +18 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +22 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/ir/index.d.ts +11 -0
  9. package/dist/ir/index.d.ts.map +1 -0
  10. package/dist/ir/index.js +9 -0
  11. package/dist/ir/index.js.map +1 -0
  12. package/dist/ir/normalize.d.ts +10 -0
  13. package/dist/ir/normalize.d.ts.map +1 -0
  14. package/dist/ir/normalize.js +207 -0
  15. package/dist/ir/normalize.js.map +1 -0
  16. package/dist/ir/persistence.d.ts +26 -0
  17. package/dist/ir/persistence.d.ts.map +1 -0
  18. package/dist/ir/persistence.js +21 -0
  19. package/dist/ir/persistence.js.map +1 -0
  20. package/dist/ir/sync.d.ts +12 -0
  21. package/dist/ir/sync.d.ts.map +1 -0
  22. package/dist/ir/sync.js +36 -0
  23. package/dist/ir/sync.js.map +1 -0
  24. package/dist/ir/types.d.ts +143 -0
  25. package/dist/ir/types.d.ts.map +1 -0
  26. package/dist/ir/types.js +13 -0
  27. package/dist/ir/types.js.map +1 -0
  28. package/dist/ir/views/dnd5e.d.ts +40 -0
  29. package/dist/ir/views/dnd5e.d.ts.map +1 -0
  30. package/dist/ir/views/dnd5e.js +50 -0
  31. package/dist/ir/views/dnd5e.js.map +1 -0
  32. package/dist/render/character.d.ts +19 -0
  33. package/dist/render/character.d.ts.map +1 -0
  34. package/dist/render/character.js +156 -0
  35. package/dist/render/character.js.map +1 -0
  36. package/dist/render/h.d.ts +27 -0
  37. package/dist/render/h.d.ts.map +1 -0
  38. package/dist/render/h.js +64 -0
  39. package/dist/render/h.js.map +1 -0
  40. package/dist/render/index.d.ts +11 -0
  41. package/dist/render/index.d.ts.map +1 -0
  42. package/dist/render/index.js +8 -0
  43. package/dist/render/index.js.map +1 -0
  44. package/dist/render/mount.d.ts +31 -0
  45. package/dist/render/mount.d.ts.map +1 -0
  46. package/dist/render/mount.js +63 -0
  47. package/dist/render/mount.js.map +1 -0
  48. package/dist/supabase/fields.d.ts.map +1 -0
  49. package/dist/supabase/fields.js +120 -0
  50. package/dist/supabase/fields.js.map +1 -0
  51. package/dist/types/character.d.ts.map +1 -0
  52. package/dist/types/character.js +5 -0
  53. package/dist/types/character.js.map +1 -0
  54. package/package.json +73 -0
  55. package/src/browser.js +51 -0
  56. package/src/cache/CacheManager.ts +174 -0
  57. package/src/common/browser-polyfill.js +319 -0
  58. package/src/common/debug.js +123 -0
  59. package/src/common/html-utils.js +134 -0
  60. package/src/common/theme-manager.js +265 -0
  61. package/src/index.ts +25 -0
  62. package/src/ir/__fixtures__/dnd5e-character.json +75962 -0
  63. package/src/ir/__fixtures__/non-dnd-character.json +14218 -0
  64. package/src/ir/index.ts +10 -0
  65. package/src/ir/normalize.ts +245 -0
  66. package/src/ir/persistence.ts +37 -0
  67. package/src/ir/sync.ts +49 -0
  68. package/src/ir/types.ts +161 -0
  69. package/src/ir/views/dnd5e.ts +94 -0
  70. package/src/lib/indexeddb-cache.js +320 -0
  71. package/src/modules/action-announcements.js +102 -0
  72. package/src/modules/action-display.js +1557 -0
  73. package/src/modules/action-executor.js +860 -0
  74. package/src/modules/action-filters.js +167 -0
  75. package/src/modules/action-options.js +117 -0
  76. package/src/modules/card-creator.js +142 -0
  77. package/src/modules/character-portrait.js +169 -0
  78. package/src/modules/character-trait-popups.js +959 -0
  79. package/src/modules/character-traits.js +814 -0
  80. package/src/modules/class-feature-edge-cases.js +1320 -0
  81. package/src/modules/color-utils.js +69 -0
  82. package/src/modules/combat-maneuver-edge-cases.js +660 -0
  83. package/src/modules/companions-manager.js +178 -0
  84. package/src/modules/concentration-tracker.js +178 -0
  85. package/src/modules/data-manager.js +514 -0
  86. package/src/modules/dice-roller.js +719 -0
  87. package/src/modules/effects-manager.js +743 -0
  88. package/src/modules/feature-modals.js +1264 -0
  89. package/src/modules/formula-resolver.js +444 -0
  90. package/src/modules/gm-mode.js +184 -0
  91. package/src/modules/health-modals.js +399 -0
  92. package/src/modules/hp-management.js +752 -0
  93. package/src/modules/inventory-manager.js +242 -0
  94. package/src/modules/macro-system.js +825 -0
  95. package/src/modules/notification-system.js +92 -0
  96. package/src/modules/racial-feature-edge-cases.js +746 -0
  97. package/src/modules/resource-manager.js +775 -0
  98. package/src/modules/sheet-builder.js +654 -0
  99. package/src/modules/spell-action-modals.js +583 -0
  100. package/src/modules/spell-cards.js +602 -0
  101. package/src/modules/spell-casting.js +723 -0
  102. package/src/modules/spell-display.js +314 -0
  103. package/src/modules/spell-edge-cases.js +509 -0
  104. package/src/modules/spell-macros.js +201 -0
  105. package/src/modules/spell-modals.js +1221 -0
  106. package/src/modules/spell-slots.js +224 -0
  107. package/src/modules/status-bar-bridge.js +101 -0
  108. package/src/modules/ui-utilities.js +284 -0
  109. package/src/modules/warlock-invocations.js +219 -0
  110. package/src/modules/window-management.js +211 -0
  111. package/src/render/character.ts +234 -0
  112. package/src/render/h.ts +74 -0
  113. package/src/render/index.ts +10 -0
  114. package/src/render/mount.ts +94 -0
  115. package/src/supabase/client.js +1383 -0
  116. package/src/supabase/config.js +60 -0
  117. package/src/supabase/fields.ts +129 -0
  118. package/src/types/character.ts +85 -0
@@ -0,0 +1,514 @@
1
+ /**
2
+ * Character Data Manager Module
3
+ *
4
+ * Handles loading, saving, and syncing character data.
5
+ * Loaded as a plain script (no ES6 modules) to export to globalThis.
6
+ *
7
+ * Functions exported to globalThis:
8
+ * - saveCharacterData()
9
+ * - sendSyncMessage()
10
+ * - loadCharacterWithTabs()
11
+ * - loadAndBuildTabs()
12
+ * - getActiveCharacterId()
13
+ * - setActiveCharacter(characterId)
14
+ * - buildCharacterTabs(profiles, activeCharacterId)
15
+ * - validateCharacterData(data)
16
+ *
17
+ * State variables:
18
+ * - currentSlotId
19
+ * - syncDebounceTimer
20
+ * - characterCache
21
+ */
22
+
23
+ (function() {
24
+ 'use strict';
25
+
26
+ // ===== STATE VARIABLES =====
27
+
28
+ let currentSlotId = null;
29
+ let syncDebounceTimer = null;
30
+ const characterCache = new Map();
31
+
32
+ // ===== CHARACTER DATA FUNCTIONS =====
33
+
34
+ /**
35
+ * Save character data to browser storage and trigger sync
36
+ */
37
+ function saveCharacterData() {
38
+ // Requires characterData to be available from global scope
39
+ if (typeof characterData === 'undefined' || !characterData) {
40
+ debug.warn('⚠️ No character data to save');
41
+ return;
42
+ }
43
+
44
+ // CRITICAL: Save to browser storage to persist through refresh/close
45
+ if (typeof browserAPI !== 'undefined') {
46
+ debug.log('💾 saveCharacterData called with:', {
47
+ hasId: !!characterData.id,
48
+ id: characterData.id,
49
+ hasDicecloudCharacterId: !!characterData.dicecloud_character_id,
50
+ dicecloud_character_id: characterData.dicecloud_character_id,
51
+ name: characterData.name,
52
+ currentSlotId: currentSlotId
53
+ });
54
+
55
+ browserAPI.runtime.sendMessage({
56
+ action: 'storeCharacterData',
57
+ data: characterData,
58
+ slotId: currentSlotId // CRITICAL: Pass slotId for proper persistence
59
+ }).then(() => {
60
+ debug.log(`💾 Saved character data to browser storage (slotId: ${currentSlotId})`);
61
+ }).catch(err => {
62
+ debug.error('❌ Failed to save character data:', err);
63
+ });
64
+ }
65
+
66
+ // Debounce sync messages to prevent flickering
67
+ // Clear any pending sync
68
+ if (syncDebounceTimer) {
69
+ clearTimeout(syncDebounceTimer);
70
+ }
71
+
72
+ // Schedule sync after a short delay
73
+ syncDebounceTimer = setTimeout(() => {
74
+ sendSyncMessage();
75
+ syncDebounceTimer = null;
76
+ }, 300); // 300ms debounce delay
77
+ }
78
+
79
+ /**
80
+ * Send sync message to DiceCloud
81
+ */
82
+ function sendSyncMessage() {
83
+ // Requires characterData to be available from global scope
84
+ if (typeof characterData === 'undefined' || !characterData) {
85
+ debug.warn('⚠️ No character data to sync');
86
+ return;
87
+ }
88
+
89
+ // Send sync message to DiceCloud if experimental sync is available
90
+ debug.log('🔄 Sending character data update to DiceCloud sync...');
91
+
92
+ // Extract Channel Divinity from characterData.resources array (this has the current values after use)
93
+ let channelDivinityForSync = null;
94
+ const channelDivinityResource = characterData.resources?.find(r =>
95
+ r.name === 'Channel Divinity' ||
96
+ r.variableName === 'channelDivinityCleric' ||
97
+ r.variableName === 'channelDivinityPaladin' ||
98
+ r.variableName === 'channelDivinity' ||
99
+ r.varName === 'channelDivinity'
100
+ );
101
+ if (channelDivinityResource) {
102
+ channelDivinityForSync = {
103
+ current: channelDivinityResource.current || 0,
104
+ max: channelDivinityResource.max || 0
105
+ };
106
+ }
107
+
108
+ // Use the existing resources array which contains current values
109
+ const resourcesForSync = characterData.resources || [];
110
+
111
+ // Debug logging to see what we're sending
112
+ console.log('[SYNC DEBUG] ========== SYNC MESSAGE DATA ==========');
113
+ console.log('[SYNC DEBUG] Character Name:', characterData.name);
114
+ console.log('[SYNC DEBUG] HP:', characterData.hitPoints?.current, '/', characterData.hitPoints?.max);
115
+ console.log('[SYNC DEBUG] Temp HP:', characterData.temporaryHP);
116
+ console.log('[SYNC DEBUG] Spell Slots:', characterData.spellSlots);
117
+ console.log('[SYNC DEBUG] Channel Divinity (extracted):', channelDivinityForSync);
118
+ console.log('[SYNC DEBUG] Channel Divinity (raw resource):', channelDivinityResource);
119
+ console.log('[SYNC DEBUG] Resources (count):', resourcesForSync?.length);
120
+ console.log('[SYNC DEBUG] Resources (full):', resourcesForSync);
121
+ console.log('[SYNC DEBUG] Actions (count):', characterData.actions?.length);
122
+ console.log('[SYNC DEBUG] Actions (full):', characterData.actions);
123
+ console.log('[SYNC DEBUG] Death Saves:', characterData.deathSaves);
124
+ console.log('[SYNC DEBUG] Inspiration:', characterData.inspiration);
125
+ console.log('[SYNC DEBUG] =========================================');
126
+
127
+ const syncMessage = {
128
+ type: 'characterDataUpdate',
129
+ characterData: {
130
+ name: characterData.name,
131
+ hp: characterData.hitPoints.current,
132
+ tempHp: characterData.temporaryHP || 0,
133
+ maxHp: characterData.hitPoints.max,
134
+ spellSlots: characterData.spellSlots || {},
135
+ channelDivinity: channelDivinityForSync,
136
+ resources: resourcesForSync,
137
+ actions: characterData.actions || [],
138
+ deathSaves: characterData.deathSaves,
139
+ inspiration: characterData.inspiration,
140
+ lastRoll: characterData.lastRoll
141
+ }
142
+ };
143
+
144
+ // Try to send to DiceCloud sync
145
+ window.postMessage(syncMessage, '*');
146
+
147
+ // Also send to Roll20 via relay
148
+ sendToRoll20(syncMessage);
149
+ sendToRoll20({
150
+ action: 'updateCharacterData',
151
+ data: characterData,
152
+ characterId: characterData.id || characterData.dicecloud_character_id || currentSlotId
153
+ });
154
+ debug.log('💾 Sent character data update to Roll20');
155
+ }
156
+
157
+ /**
158
+ * Validate character data has required fields
159
+ */
160
+ function validateCharacterData(data) {
161
+ if (!data) return { valid: false, missing: ['all data'] };
162
+
163
+ const hasSpells = Array.isArray(data.spells);
164
+ const hasActions = Array.isArray(data.actions);
165
+
166
+ if (!hasSpells || !hasActions) {
167
+ const missing = [];
168
+ if (!hasSpells) missing.push('spells');
169
+ if (!hasActions) missing.push('actions');
170
+ return { valid: false, missing };
171
+ }
172
+
173
+ return { valid: true, missing: [] };
174
+ }
175
+
176
+ /**
177
+ * Get the active character ID from storage
178
+ */
179
+ async function getActiveCharacterId() {
180
+ if (typeof browserAPI === 'undefined') {
181
+ debug.warn('⚠️ browserAPI not available');
182
+ return null;
183
+ }
184
+
185
+ // Use Promise-based API (works in both Chrome and Firefox with our polyfill)
186
+ const result = await browserAPI.storage.local.get(['activeCharacterId']);
187
+ return result.activeCharacterId || null;
188
+ }
189
+
190
+ /**
191
+ * Set the active character ID in storage
192
+ */
193
+ async function setActiveCharacter(characterId) {
194
+ if (typeof browserAPI === 'undefined') {
195
+ debug.warn('⚠️ browserAPI not available');
196
+ return;
197
+ }
198
+
199
+ try {
200
+ await browserAPI.storage.local.set({
201
+ activeCharacterId: characterId
202
+ });
203
+ console.log(`✅ Set active character: ${characterId}`);
204
+ } catch (error) {
205
+ console.error('❌ Failed to set active character:', error);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Load profiles and build tabs (without building sheet)
211
+ */
212
+ async function loadAndBuildTabs() {
213
+ if (typeof browserAPI === 'undefined') {
214
+ debug.warn('⚠️ browserAPI not available');
215
+ return;
216
+ }
217
+
218
+ try {
219
+ debug.log('📋 Loading character profiles for tabs...');
220
+
221
+ // Get all character profiles
222
+ const profilesResponse = await browserAPI.runtime.sendMessage({ action: 'getAllCharacterProfiles' });
223
+ const profiles = profilesResponse.success ? profilesResponse.profiles : {};
224
+ debug.log('📋 Profiles loaded:', Object.keys(profiles));
225
+
226
+ // Get active character ID (this is the slotId like "slot-1")
227
+ const activeCharacterId = await getActiveCharacterId();
228
+ debug.log('📋 Active character ID:', activeCharacterId);
229
+
230
+ // Build character tabs
231
+ buildCharacterTabs(profiles, activeCharacterId);
232
+ } catch (error) {
233
+ debug.error('❌ Failed to load and build tabs:', error);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Load character data and build tabs
239
+ */
240
+ async function loadCharacterWithTabs() {
241
+ if (typeof browserAPI === 'undefined') {
242
+ debug.warn('⚠️ browserAPI not available');
243
+ return;
244
+ }
245
+
246
+ // Check if DOM is ready (domReady should be available from global scope)
247
+ if (typeof domReady !== 'undefined' && !domReady) {
248
+ debug.log('⏳ DOM not ready, queuing loadCharacterWithTabs...');
249
+ if (typeof pendingOperations !== 'undefined') {
250
+ pendingOperations.push(loadCharacterWithTabs);
251
+ }
252
+ return;
253
+ }
254
+
255
+ try {
256
+ // Build tabs first
257
+ await loadAndBuildTabs();
258
+
259
+ // Get and store current slot ID for persistence
260
+ currentSlotId = await getActiveCharacterId();
261
+ debug.log('📋 Current slot ID set to:', currentSlotId);
262
+
263
+ // Get active character data
264
+ let activeCharacter = null;
265
+
266
+ // Check if this is a database character
267
+ if (currentSlotId && currentSlotId.startsWith('db-')) {
268
+ // Load from database
269
+ const characterId = currentSlotId.replace('db-', '');
270
+ try {
271
+ const dbResponse = await browserAPI.runtime.sendMessage({
272
+ action: 'getCharacterDataFromDatabase',
273
+ characterId: characterId
274
+ });
275
+ if (dbResponse.success) {
276
+ activeCharacter = dbResponse.data;
277
+ debug.log('✅ Loaded character from database:', activeCharacter.name);
278
+ }
279
+ } catch (dbError) {
280
+ debug.warn('⚠️ Failed to load database character:', dbError);
281
+ }
282
+ } else {
283
+ // Load from local storage
284
+ const activeResponse = await browserAPI.runtime.sendMessage({ action: 'getCharacterData' });
285
+ activeCharacter = activeResponse.success ? activeResponse.data : null;
286
+ }
287
+
288
+ // Load active character
289
+ if (activeCharacter) {
290
+ // Validate character data
291
+ const validation = validateCharacterData(activeCharacter);
292
+
293
+ if (!validation.valid) {
294
+ debug.warn('⚠️ Character data is incomplete or outdated');
295
+ debug.warn(`Missing data: ${validation.missing.join(', ')}`);
296
+
297
+ // Show error message to user
298
+ const characterName = activeCharacter.name || activeCharacter.character_name || 'this character';
299
+
300
+ const errorContainer = document.getElementById('main-content');
301
+ if (errorContainer) {
302
+ errorContainer.innerHTML = `
303
+ <div style="padding: 40px; text-align: center; color: var(--text-primary);">
304
+ <h2 style="color: #e74c3c; margin-bottom: 20px;">⚠️ Incomplete Character Data</h2>
305
+ <p style="margin-bottom: 15px; font-size: 1.1em;">
306
+ The character data for <strong>${characterName}</strong> is missing ${validation.missing.join(' and ')}.
307
+ </p>
308
+ <p style="margin-bottom: 15px; color: var(--text-secondary);">
309
+ This usually happens when loading old cloud data that was saved before spells and actions were synced.
310
+ </p>
311
+ <div style="background: #2c3e50; padding: 20px; border-radius: 8px; margin: 20px 0;">
312
+ <p style="margin-bottom: 10px; font-weight: bold;">To fix this:</p>
313
+ <ol style="text-align: left; max-width: 500px; margin: 0 auto; line-height: 1.8;">
314
+ <li>Go to your character on <a href="https://dicecloud.com" target="_blank" style="color: #3498db;">DiceCloud.com</a></li>
315
+ <li>Click the <strong>"Sync to Extension"</strong> button on the character page</li>
316
+ <li>Wait for the sync to complete</li>
317
+ <li>Reopen this character sheet</li>
318
+ </ol>
319
+ </div>
320
+ <p style="color: var(--text-secondary); font-size: 0.9em;">
321
+ Character ID: ${activeCharacter.id || activeCharacter.dicecloud_character_id || 'unknown'}
322
+ </p>
323
+ </div>
324
+ `;
325
+ }
326
+
327
+ // Don't continue loading the incomplete character
328
+ return;
329
+ }
330
+
331
+ // Set global characterData
332
+ if (typeof globalThis.characterData !== 'undefined') {
333
+ globalThis.characterData = activeCharacter;
334
+ }
335
+
336
+ // Build sheet (buildSheet should be available from global scope)
337
+ if (typeof buildSheet !== 'undefined') {
338
+ buildSheet(activeCharacter);
339
+ }
340
+
341
+ // Initialize racial traits, feat traits, class features
342
+ if (typeof initRacialTraits !== 'undefined') initRacialTraits();
343
+ if (typeof initFeatTraits !== 'undefined') initFeatTraits();
344
+ if (typeof initClassFeatures !== 'undefined') initClassFeatures();
345
+ } else {
346
+ debug.error('❌ No character data found');
347
+ }
348
+ } catch (error) {
349
+ debug.error('❌ Failed to load characters:', error);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Build character tabs UI
355
+ */
356
+ function buildCharacterTabs(profiles, activeCharacterId) {
357
+ const tabsContainer = document.getElementById('character-tabs');
358
+ if (!tabsContainer) {
359
+ debug.warn('⚠️ character-tabs container not found!');
360
+ return;
361
+ }
362
+
363
+ debug.log(`🏷️ Building character tabs. Active: ${activeCharacterId}`);
364
+ debug.log(`📋 Profiles:`, Object.keys(profiles));
365
+
366
+ tabsContainer.innerHTML = '';
367
+ const maxSlots = 10; // Support up to 10 character slots
368
+
369
+ // First, add database characters. Include both:
370
+ // 1. Keys starting with `db-` (direct database characters)
371
+ // 2. Characters with source='database' or hasCloudVersion=true (local profiles with cloud sync)
372
+ const databaseCharacters = Object.entries(profiles).filter(([slotId, profile]) =>
373
+ slotId.startsWith('db-') ||
374
+ profile.source === 'database' ||
375
+ profile.hasCloudVersion === true
376
+ );
377
+
378
+ // Add database character tabs
379
+ databaseCharacters.forEach(([slotId, charInSlot], index) => {
380
+ const isActive = slotId === activeCharacterId;
381
+
382
+ const displayName = charInSlot.name || charInSlot.character_name || (charInSlot._fullData && (charInSlot._fullData.character_name || charInSlot._fullData.name)) || 'Unknown';
383
+ debug.log(`🌐 DB Character: ${displayName} (active: ${isActive})`);
384
+
385
+ const tab = document.createElement('div');
386
+ tab.className = 'character-tab database-tab';
387
+ if (isActive) {
388
+ tab.classList.add('active');
389
+ }
390
+ tab.dataset.slotId = slotId;
391
+
392
+ // Create special styling for database characters
393
+ tab.innerHTML = `
394
+ <span class="slot-number">🌐</span>
395
+ <span class="char-name">${displayName}</span>
396
+ <span class="char-details">${charInSlot.level || 1} ${charInSlot.class || 'Unknown'}</span>
397
+ `;
398
+
399
+ // Add click handler
400
+ tab.addEventListener('click', (e) => {
401
+ debug.log(`🖱️ Database tab clicked for ${slotId}`, charInSlot.name);
402
+ if (typeof switchToCharacter === 'function') {
403
+ switchToCharacter(slotId);
404
+ }
405
+ });
406
+
407
+ tabsContainer.appendChild(tab);
408
+ });
409
+
410
+ // Add separator if we have both database and local characters
411
+ if (databaseCharacters.length > 0) {
412
+ const separator = document.createElement('div');
413
+ separator.className = 'tab-separator';
414
+ separator.innerHTML = '<span style="color: var(--text-secondary); font-size: 0.8em;">Local Characters</span>';
415
+ tabsContainer.appendChild(separator);
416
+ }
417
+
418
+ // Create tabs for local slots (skip characters already shown in database section)
419
+ for (let slotNum = 1; slotNum <= maxSlots; slotNum++) {
420
+ const slotId = `slot-${slotNum}`;
421
+ // Find character in this slot using slotId as key
422
+ const charInSlot = profiles[slotId];
423
+
424
+ // Skip if this character was already shown in the database section
425
+ if (charInSlot && (charInSlot.source === 'database' || charInSlot.hasCloudVersion === true)) {
426
+ debug.log(` ⏭️ Slot ${slotNum}: ${charInSlot.name} (skipped - shown in cloud section)`);
427
+ continue;
428
+ }
429
+
430
+ if (charInSlot) {
431
+ debug.log(` 📌 Slot ${slotNum}: ${charInSlot.name} (active: ${slotId === activeCharacterId})`);
432
+ }
433
+
434
+ const tab = document.createElement('div');
435
+ tab.className = 'character-tab';
436
+ tab.dataset.slotId = slotId;
437
+
438
+ if (charInSlot) {
439
+ const isActive = slotId === activeCharacterId;
440
+
441
+ if (isActive) {
442
+ tab.classList.add('active');
443
+ }
444
+
445
+ tab.innerHTML = `
446
+ <span class="slot-number">${slotNum}</span>
447
+ <span class="char-name">${charInSlot.name || 'Unknown'}</span>
448
+ <span class="close-tab" title="Clear slot">✕</span>
449
+ `;
450
+
451
+ // Click to switch character
452
+ tab.addEventListener('click', (e) => {
453
+ debug.log(`🖱️ Tab clicked for ${slotId}`, charInSlot.name);
454
+ if (!e.target.classList.contains('close-tab')) {
455
+ if (typeof switchToCharacter === 'function') {
456
+ switchToCharacter(slotId);
457
+ }
458
+ }
459
+ });
460
+
461
+ // Close button - show options modal
462
+ const closeBtn = tab.querySelector('.close-tab');
463
+ closeBtn.addEventListener('click', (e) => {
464
+ e.stopPropagation();
465
+ if (typeof showClearCharacterOptions === 'function') {
466
+ showClearCharacterOptions(slotId, slotNum, charInSlot.name);
467
+ }
468
+ });
469
+ } else {
470
+ // Empty slot
471
+ tab.classList.add('empty');
472
+ tab.innerHTML = `
473
+ <span class="slot-number">${slotNum}</span>
474
+ <span class="char-name">Empty Slot</span>
475
+ `;
476
+ }
477
+
478
+ tabsContainer.appendChild(tab);
479
+ }
480
+ }
481
+
482
+ // ===== EXPORTS =====
483
+
484
+ globalThis.saveCharacterData = saveCharacterData;
485
+ globalThis.sendSyncMessage = sendSyncMessage;
486
+ globalThis.loadCharacterWithTabs = loadCharacterWithTabs;
487
+ globalThis.loadAndBuildTabs = loadAndBuildTabs;
488
+ globalThis.getActiveCharacterId = getActiveCharacterId;
489
+ globalThis.setActiveCharacter = setActiveCharacter;
490
+ globalThis.buildCharacterTabs = buildCharacterTabs;
491
+ globalThis.validateCharacterData = validateCharacterData;
492
+
493
+ // Export state variables with getters and setters
494
+ Object.defineProperty(globalThis, 'currentSlotId', {
495
+ get: () => {
496
+ debug.log(`📍 currentSlotId getter called, returning: ${currentSlotId}`);
497
+ return currentSlotId;
498
+ },
499
+ set: (value) => {
500
+ debug.log(`📍 currentSlotId setter called with value: ${value}`);
501
+ currentSlotId = value;
502
+ }
503
+ });
504
+
505
+ Object.defineProperty(globalThis, 'syncDebounceTimer', {
506
+ get: () => syncDebounceTimer,
507
+ set: (value) => { syncDebounceTimer = value; }
508
+ });
509
+
510
+ Object.defineProperty(globalThis, 'characterCache', {
511
+ get: () => characterCache
512
+ });
513
+
514
+ })();