@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.
- package/dist/cache/CacheManager.d.ts.map +1 -0
- package/dist/cache/CacheManager.js +131 -0
- package/dist/cache/CacheManager.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/index.d.ts +11 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +9 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/normalize.d.ts +10 -0
- package/dist/ir/normalize.d.ts.map +1 -0
- package/dist/ir/normalize.js +207 -0
- package/dist/ir/normalize.js.map +1 -0
- package/dist/ir/persistence.d.ts +26 -0
- package/dist/ir/persistence.d.ts.map +1 -0
- package/dist/ir/persistence.js +21 -0
- package/dist/ir/persistence.js.map +1 -0
- package/dist/ir/sync.d.ts +12 -0
- package/dist/ir/sync.d.ts.map +1 -0
- package/dist/ir/sync.js +36 -0
- package/dist/ir/sync.js.map +1 -0
- package/dist/ir/types.d.ts +143 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/ir/types.js +13 -0
- package/dist/ir/types.js.map +1 -0
- package/dist/ir/views/dnd5e.d.ts +40 -0
- package/dist/ir/views/dnd5e.d.ts.map +1 -0
- package/dist/ir/views/dnd5e.js +50 -0
- package/dist/ir/views/dnd5e.js.map +1 -0
- package/dist/render/character.d.ts +19 -0
- package/dist/render/character.d.ts.map +1 -0
- package/dist/render/character.js +156 -0
- package/dist/render/character.js.map +1 -0
- package/dist/render/h.d.ts +27 -0
- package/dist/render/h.d.ts.map +1 -0
- package/dist/render/h.js +64 -0
- package/dist/render/h.js.map +1 -0
- package/dist/render/index.d.ts +11 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/mount.d.ts +31 -0
- package/dist/render/mount.d.ts.map +1 -0
- package/dist/render/mount.js +63 -0
- package/dist/render/mount.js.map +1 -0
- package/dist/supabase/fields.d.ts.map +1 -0
- package/dist/supabase/fields.js +120 -0
- package/dist/supabase/fields.js.map +1 -0
- package/dist/types/character.d.ts.map +1 -0
- package/dist/types/character.js +5 -0
- package/dist/types/character.js.map +1 -0
- package/package.json +73 -0
- package/src/browser.js +51 -0
- package/src/cache/CacheManager.ts +174 -0
- package/src/common/browser-polyfill.js +319 -0
- package/src/common/debug.js +123 -0
- package/src/common/html-utils.js +134 -0
- package/src/common/theme-manager.js +265 -0
- package/src/index.ts +25 -0
- package/src/ir/__fixtures__/dnd5e-character.json +75962 -0
- package/src/ir/__fixtures__/non-dnd-character.json +14218 -0
- package/src/ir/index.ts +10 -0
- package/src/ir/normalize.ts +245 -0
- package/src/ir/persistence.ts +37 -0
- package/src/ir/sync.ts +49 -0
- package/src/ir/types.ts +161 -0
- package/src/ir/views/dnd5e.ts +94 -0
- package/src/lib/indexeddb-cache.js +320 -0
- package/src/modules/action-announcements.js +102 -0
- package/src/modules/action-display.js +1557 -0
- package/src/modules/action-executor.js +860 -0
- package/src/modules/action-filters.js +167 -0
- package/src/modules/action-options.js +117 -0
- package/src/modules/card-creator.js +142 -0
- package/src/modules/character-portrait.js +169 -0
- package/src/modules/character-trait-popups.js +959 -0
- package/src/modules/character-traits.js +814 -0
- package/src/modules/class-feature-edge-cases.js +1320 -0
- package/src/modules/color-utils.js +69 -0
- package/src/modules/combat-maneuver-edge-cases.js +660 -0
- package/src/modules/companions-manager.js +178 -0
- package/src/modules/concentration-tracker.js +178 -0
- package/src/modules/data-manager.js +514 -0
- package/src/modules/dice-roller.js +719 -0
- package/src/modules/effects-manager.js +743 -0
- package/src/modules/feature-modals.js +1264 -0
- package/src/modules/formula-resolver.js +444 -0
- package/src/modules/gm-mode.js +184 -0
- package/src/modules/health-modals.js +399 -0
- package/src/modules/hp-management.js +752 -0
- package/src/modules/inventory-manager.js +242 -0
- package/src/modules/macro-system.js +825 -0
- package/src/modules/notification-system.js +92 -0
- package/src/modules/racial-feature-edge-cases.js +746 -0
- package/src/modules/resource-manager.js +775 -0
- package/src/modules/sheet-builder.js +654 -0
- package/src/modules/spell-action-modals.js +583 -0
- package/src/modules/spell-cards.js +602 -0
- package/src/modules/spell-casting.js +723 -0
- package/src/modules/spell-display.js +314 -0
- package/src/modules/spell-edge-cases.js +509 -0
- package/src/modules/spell-macros.js +201 -0
- package/src/modules/spell-modals.js +1221 -0
- package/src/modules/spell-slots.js +224 -0
- package/src/modules/status-bar-bridge.js +101 -0
- package/src/modules/ui-utilities.js +284 -0
- package/src/modules/warlock-invocations.js +219 -0
- package/src/modules/window-management.js +211 -0
- package/src/render/character.ts +234 -0
- package/src/render/h.ts +74 -0
- package/src/render/index.ts +10 -0
- package/src/render/mount.ts +94 -0
- package/src/supabase/client.js +1383 -0
- package/src/supabase/config.js +60 -0
- package/src/supabase/fields.ts +129 -0
- package/src/types/character.ts +85 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spell Slots Module
|
|
3
|
+
*
|
|
4
|
+
* Handles spell slot display and manual adjustment.
|
|
5
|
+
* - Displays spell slots grid (regular + Pact Magic)
|
|
6
|
+
* - Shows total slots summary
|
|
7
|
+
* - Manual slot adjustment via click
|
|
8
|
+
*
|
|
9
|
+
* Loaded as a plain script (no ES6 modules) to export to window.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(function() {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build and display spell slots grid
|
|
17
|
+
*/
|
|
18
|
+
function buildSpellSlotsDisplay() {
|
|
19
|
+
const container = document.getElementById('spell-slots-container');
|
|
20
|
+
const debug = window.debug || console;
|
|
21
|
+
|
|
22
|
+
if (!container) {
|
|
23
|
+
debug.warn('⚠️ Spell slots container not found in DOM');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!characterData || !characterData.spellSlots) {
|
|
28
|
+
container.innerHTML = '<p style="text-align: center; color: #666;">No spell slots available</p>';
|
|
29
|
+
debug.log('⚠️ No spell slots in character data');
|
|
30
|
+
// Collapse the section when empty
|
|
31
|
+
if (typeof collapseSectionByContainerId === 'function') {
|
|
32
|
+
collapseSectionByContainerId('spell-slots-container');
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const slotsGrid = document.createElement('div');
|
|
38
|
+
slotsGrid.className = 'spell-slots-grid';
|
|
39
|
+
|
|
40
|
+
let hasAnySlots = false;
|
|
41
|
+
let totalCurrentSlots = 0;
|
|
42
|
+
let totalMaxSlots = 0;
|
|
43
|
+
|
|
44
|
+
// Check for Pact Magic (Warlock) - stored separately from regular slots
|
|
45
|
+
const pactMagicSlotLevel = characterData.spellSlots?.pactMagicSlotLevel ||
|
|
46
|
+
characterData.otherVariables?.pactMagicSlotLevel ||
|
|
47
|
+
characterData.otherVariables?.pactSlotLevelVisible ||
|
|
48
|
+
characterData.otherVariables?.pactSlotLevel ||
|
|
49
|
+
characterData.otherVariables?.slotLevel;
|
|
50
|
+
const pactMagicSlots = characterData.spellSlots?.pactMagicSlots ??
|
|
51
|
+
characterData.otherVariables?.pactMagicSlots ??
|
|
52
|
+
characterData.otherVariables?.pactSlot ?? 0;
|
|
53
|
+
const pactMagicSlotsMax = characterData.spellSlots?.pactMagicSlotsMax ??
|
|
54
|
+
characterData.otherVariables?.pactMagicSlotsMax ??
|
|
55
|
+
characterData.otherVariables?.pactSlotMax ?? 0;
|
|
56
|
+
const hasPactMagic = pactMagicSlotsMax > 0;
|
|
57
|
+
// Default slot level to 5 (max pact level) if we have slots but couldn't detect level
|
|
58
|
+
const effectivePactLevel = pactMagicSlotLevel || (hasPactMagic ? 5 : 0);
|
|
59
|
+
|
|
60
|
+
debug.log(`🔮 Spell slots display - Pact Magic: level=${pactMagicSlotLevel} (effective=${effectivePactLevel}), slots=${pactMagicSlots}/${pactMagicSlotsMax}, hasPact=${hasPactMagic}`);
|
|
61
|
+
|
|
62
|
+
// Add Pact Magic slots first if present
|
|
63
|
+
if (hasPactMagic) {
|
|
64
|
+
hasAnySlots = true;
|
|
65
|
+
totalCurrentSlots += pactMagicSlots;
|
|
66
|
+
totalMaxSlots += pactMagicSlotsMax;
|
|
67
|
+
|
|
68
|
+
const slotCard = document.createElement('div');
|
|
69
|
+
slotCard.className = pactMagicSlots > 0 ? 'spell-slot-card pact-magic' : 'spell-slot-card pact-magic empty';
|
|
70
|
+
slotCard.style.cssText = 'background: linear-gradient(135deg, #6b3fa0, #9b59b6); border: 2px solid #8e44ad;';
|
|
71
|
+
|
|
72
|
+
slotCard.innerHTML = `
|
|
73
|
+
<div class="spell-slot-level">Pact (${effectivePactLevel})</div>
|
|
74
|
+
<div class="spell-slot-count">${pactMagicSlots}/${pactMagicSlotsMax}</div>
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// Add click to manually adjust Pact Magic slots
|
|
78
|
+
slotCard.addEventListener('click', () => {
|
|
79
|
+
adjustSpellSlot(`pact:${effectivePactLevel}`, pactMagicSlots, pactMagicSlotsMax, true);
|
|
80
|
+
});
|
|
81
|
+
slotCard.style.cursor = 'pointer';
|
|
82
|
+
slotCard.title = 'Click to adjust Pact Magic slots (recharge on short rest)';
|
|
83
|
+
|
|
84
|
+
slotsGrid.appendChild(slotCard);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check each level (1-9) for regular spell slots
|
|
88
|
+
for (let level = 1; level <= 9; level++) {
|
|
89
|
+
const slotVar = `level${level}SpellSlots`;
|
|
90
|
+
const slotMaxVar = `level${level}SpellSlotsMax`;
|
|
91
|
+
|
|
92
|
+
// Support both flat keys (level1SpellSlotsMax) and nested format ({ level1: { current, max } })
|
|
93
|
+
const nestedSlot = characterData.spellSlots[`level${level}`];
|
|
94
|
+
const maxSlots = characterData.spellSlots[slotMaxVar] || nestedSlot?.max || 0;
|
|
95
|
+
|
|
96
|
+
// Only show if character has regular slots at this level
|
|
97
|
+
if (maxSlots > 0) {
|
|
98
|
+
hasAnySlots = true;
|
|
99
|
+
const currentSlots = characterData.spellSlots[slotVar] ?? nestedSlot?.current ?? maxSlots;
|
|
100
|
+
|
|
101
|
+
// Track totals
|
|
102
|
+
totalCurrentSlots += currentSlots;
|
|
103
|
+
totalMaxSlots += maxSlots;
|
|
104
|
+
|
|
105
|
+
const slotCard = document.createElement('div');
|
|
106
|
+
slotCard.className = currentSlots > 0 ? 'spell-slot-card' : 'spell-slot-card empty';
|
|
107
|
+
|
|
108
|
+
slotCard.innerHTML = `
|
|
109
|
+
<div class="spell-slot-level">Level ${level}</div>
|
|
110
|
+
<div class="spell-slot-count">${currentSlots}/${maxSlots}</div>
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
// Add click to manually adjust slots with hover effect
|
|
114
|
+
slotCard.addEventListener('click', () => {
|
|
115
|
+
adjustSpellSlot(level, currentSlots, maxSlots);
|
|
116
|
+
});
|
|
117
|
+
slotCard.style.cursor = 'pointer';
|
|
118
|
+
slotCard.title = 'Click to adjust spell slots';
|
|
119
|
+
|
|
120
|
+
slotsGrid.appendChild(slotCard);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (hasAnySlots) {
|
|
125
|
+
container.innerHTML = '';
|
|
126
|
+
|
|
127
|
+
// Add total slots summary
|
|
128
|
+
const summaryCard = document.createElement('div');
|
|
129
|
+
summaryCard.className = 'spell-slots-summary';
|
|
130
|
+
summaryCard.style.cssText = `
|
|
131
|
+
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
|
|
132
|
+
color: white;
|
|
133
|
+
padding: 12px;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
text-align: center;
|
|
136
|
+
margin-bottom: 15px;
|
|
137
|
+
font-weight: bold;
|
|
138
|
+
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.3);
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const totalPercent = totalMaxSlots > 0 ? (totalCurrentSlots / totalMaxSlots) * 100 : 0;
|
|
142
|
+
summaryCard.innerHTML = `
|
|
143
|
+
<div style="font-size: 14px; opacity: 0.9;">Total Spell Slots</div>
|
|
144
|
+
<div style="font-size: 20px; margin: 4px 0;">${totalCurrentSlots}/${totalMaxSlots}</div>
|
|
145
|
+
<div style="font-size: 12px; opacity: 0.8;">${Math.round(totalPercent)}% remaining</div>
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
container.appendChild(summaryCard);
|
|
149
|
+
container.appendChild(slotsGrid);
|
|
150
|
+
|
|
151
|
+
// Add a small note
|
|
152
|
+
const note = document.createElement('p');
|
|
153
|
+
note.style.cssText = 'text-align: center; color: #666; font-size: 0.85em; margin-top: 8px;';
|
|
154
|
+
note.textContent = 'Click a slot to manually adjust';
|
|
155
|
+
container.appendChild(note);
|
|
156
|
+
|
|
157
|
+
debug.log(`✨ Spell slots display: ${totalCurrentSlots}/${totalMaxSlots} total slots`);
|
|
158
|
+
// Expand the section when it has content
|
|
159
|
+
if (typeof expandSectionByContainerId === 'function') {
|
|
160
|
+
expandSectionByContainerId('spell-slots-container');
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
container.innerHTML = '<p style="text-align: center; color: #666;">No spell slots available</p>';
|
|
164
|
+
debug.log('⚠️ Character has 0 max slots for all levels');
|
|
165
|
+
// Collapse the section when empty
|
|
166
|
+
if (typeof collapseSectionByContainerId === 'function') {
|
|
167
|
+
collapseSectionByContainerId('spell-slots-container');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Manually adjust a spell slot
|
|
174
|
+
* @param {number|string} level - Spell level or "pact:level" for Pact Magic
|
|
175
|
+
* @param {number} current - Current slots
|
|
176
|
+
* @param {number} max - Maximum slots
|
|
177
|
+
* @param {boolean} isPactMagic - Whether this is a Pact Magic slot
|
|
178
|
+
*/
|
|
179
|
+
function adjustSpellSlot(level, current, max, isPactMagic = false) {
|
|
180
|
+
// Check if this is a Pact Magic slot (format: "pact:${level}")
|
|
181
|
+
const isPact = isPactMagic || (typeof level === 'string' && level.startsWith('pact:'));
|
|
182
|
+
const actualLevel = isPact ? parseInt(level.toString().split(':')[1] || level) : level;
|
|
183
|
+
|
|
184
|
+
const slotLabel = isPact ? `Pact Magic (Level ${actualLevel})` : `Level ${actualLevel}`;
|
|
185
|
+
const newValue = prompt(`Adjust ${slotLabel} Spell Slots\n\nCurrent: ${current}/${max}\n\nEnter new current value (0-${max}):`);
|
|
186
|
+
|
|
187
|
+
if (newValue === null) return; // Cancelled
|
|
188
|
+
|
|
189
|
+
const parsed = parseInt(newValue);
|
|
190
|
+
if (isNaN(parsed) || parsed < 0 || parsed > max) {
|
|
191
|
+
if (typeof showNotification === 'function') {
|
|
192
|
+
showNotification('❌ Invalid value', 'error');
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isPact) {
|
|
198
|
+
characterData.spellSlots.pactMagicSlots = parsed;
|
|
199
|
+
} else {
|
|
200
|
+
const slotVar = `level${actualLevel}SpellSlots`;
|
|
201
|
+
characterData.spellSlots[slotVar] = parsed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (typeof saveCharacterData === 'function') {
|
|
205
|
+
saveCharacterData();
|
|
206
|
+
}
|
|
207
|
+
if (typeof buildSheet === 'function') {
|
|
208
|
+
buildSheet(characterData);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof showNotification === 'function') {
|
|
212
|
+
showNotification(`✅ ${slotLabel} slots set to ${parsed}/${max}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Export functions to globalThis
|
|
217
|
+
Object.assign(globalThis, {
|
|
218
|
+
buildSpellSlotsDisplay,
|
|
219
|
+
adjustSpellSlot
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
console.log('✅ Spell Slots module loaded');
|
|
223
|
+
|
|
224
|
+
})();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Bar Bridge Module
|
|
3
|
+
*
|
|
4
|
+
* Handles communication between the main character sheet and the status bar popup.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - initStatusBarButton()
|
|
9
|
+
* - sendStatusUpdate()
|
|
10
|
+
* - statusBarWindow (variable)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
(function() {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
// Track status bar window
|
|
17
|
+
let statusBarWindow = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize status bar button
|
|
21
|
+
* Must be called after DOM is ready
|
|
22
|
+
*/
|
|
23
|
+
function initStatusBarButton() {
|
|
24
|
+
const statusBarBtn = document.getElementById('status-bar-btn');
|
|
25
|
+
if (statusBarBtn) {
|
|
26
|
+
statusBarBtn.addEventListener('click', () => {
|
|
27
|
+
// Check if browserAPI is available
|
|
28
|
+
if (typeof browserAPI === 'undefined' || !browserAPI) {
|
|
29
|
+
if (typeof showNotification !== 'undefined') {
|
|
30
|
+
showNotification('❌ Extension API not available', 'error');
|
|
31
|
+
}
|
|
32
|
+
debug.warn('⚠️ browserAPI not available');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Open status bar window
|
|
37
|
+
const width = 350;
|
|
38
|
+
const height = 500;
|
|
39
|
+
const left = window.screenX + window.outerWidth - width - 50;
|
|
40
|
+
const top = window.screenY + 50;
|
|
41
|
+
|
|
42
|
+
statusBarWindow = window.open(
|
|
43
|
+
browserAPI.runtime.getURL('src/status-bar.html'),
|
|
44
|
+
'status-bar',
|
|
45
|
+
`width=${width},height=${height},left=${left},top=${top},scrollbars=no,resizable=yes`
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (!statusBarWindow) {
|
|
49
|
+
if (typeof showNotification !== 'undefined') {
|
|
50
|
+
showNotification('❌ Failed to open status bar - please allow popups', 'error');
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
debug.log('📊 Status bar opened');
|
|
56
|
+
|
|
57
|
+
// Send initial data after a short delay
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
sendStatusUpdate();
|
|
60
|
+
}, 500);
|
|
61
|
+
});
|
|
62
|
+
debug.log('✅ Status bar button initialized');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send status update to status bar window
|
|
68
|
+
* Requires characterData to be available in global scope
|
|
69
|
+
*/
|
|
70
|
+
function sendStatusUpdate() {
|
|
71
|
+
if (!statusBarWindow || statusBarWindow.closed) return;
|
|
72
|
+
|
|
73
|
+
// characterData should be available from popup-sheet.js global scope
|
|
74
|
+
if (typeof characterData === 'undefined') {
|
|
75
|
+
debug.warn('⚠️ characterData not available for status update');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const statusData = {
|
|
80
|
+
action: 'updateStatusData',
|
|
81
|
+
data: {
|
|
82
|
+
name: characterData.name || characterData.character_name,
|
|
83
|
+
hitPoints: characterData.hitPoints || characterData.hit_points,
|
|
84
|
+
concentrating: characterData.concentrating || false,
|
|
85
|
+
concentrationSpell: characterData.concentrationSpell || '',
|
|
86
|
+
activeBuffs: characterData.activeBuffs || [],
|
|
87
|
+
activeDebuffs: characterData.activeDebuffs || [],
|
|
88
|
+
spellSlots: characterData.spellSlots || {}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
statusBarWindow.postMessage(statusData, '*');
|
|
93
|
+
debug.log('📊 Sent status update to status bar');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Export to globalThis
|
|
97
|
+
globalThis.initStatusBarButton = initStatusBarButton;
|
|
98
|
+
globalThis.sendStatusUpdate = sendStatusUpdate;
|
|
99
|
+
globalThis.statusBarWindow = statusBarWindow;
|
|
100
|
+
|
|
101
|
+
})();
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Utilities Module
|
|
3
|
+
*
|
|
4
|
+
* Generic UI helper functions:
|
|
5
|
+
* - Collapsible sections (expand/collapse sheet sections)
|
|
6
|
+
* - Color palette selector (notification color picker)
|
|
7
|
+
* - Close button handler
|
|
8
|
+
* - Supabase color sync
|
|
9
|
+
*
|
|
10
|
+
* These are reusable UI components that don't contain domain-specific logic.
|
|
11
|
+
*
|
|
12
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
13
|
+
*
|
|
14
|
+
* Functions exported to globalThis:
|
|
15
|
+
* - initCollapsibleSections()
|
|
16
|
+
* - collapseSectionByContainerId(containerId)
|
|
17
|
+
* - expandSectionByContainerId(containerId)
|
|
18
|
+
* - createColorPalette(selectedColor)
|
|
19
|
+
* - initColorPalette()
|
|
20
|
+
* - syncColorToSupabase(color)
|
|
21
|
+
* - initCloseButton()
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
(function() {
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
// ===== COLLAPSIBLE SECTIONS =====
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize collapsible sections
|
|
31
|
+
* Makes all section headers clickable to toggle visibility
|
|
32
|
+
*/
|
|
33
|
+
function initCollapsibleSections() {
|
|
34
|
+
const sections = document.querySelectorAll('.section h3');
|
|
35
|
+
|
|
36
|
+
sections.forEach(header => {
|
|
37
|
+
header.addEventListener('click', function() {
|
|
38
|
+
const section = this.parentElement;
|
|
39
|
+
const content = section.querySelector('.section-content');
|
|
40
|
+
|
|
41
|
+
// Toggle collapsed class
|
|
42
|
+
this.classList.toggle('collapsed');
|
|
43
|
+
content.classList.toggle('collapsed');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Helper function to collapse a section by its container ID
|
|
50
|
+
* @param {string} containerId - The ID of the container element
|
|
51
|
+
*/
|
|
52
|
+
function collapseSectionByContainerId(containerId) {
|
|
53
|
+
const container = document.getElementById(containerId);
|
|
54
|
+
if (!container) return;
|
|
55
|
+
|
|
56
|
+
const section = container.closest('.section');
|
|
57
|
+
if (!section) return;
|
|
58
|
+
|
|
59
|
+
const header = section.querySelector('h3');
|
|
60
|
+
const content = section.querySelector('.section-content');
|
|
61
|
+
|
|
62
|
+
if (header && content) {
|
|
63
|
+
header.classList.add('collapsed');
|
|
64
|
+
content.classList.add('collapsed');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Helper function to expand a section by its container ID
|
|
70
|
+
* @param {string} containerId - The ID of the container element
|
|
71
|
+
*/
|
|
72
|
+
function expandSectionByContainerId(containerId) {
|
|
73
|
+
const container = document.getElementById(containerId);
|
|
74
|
+
if (!container) return;
|
|
75
|
+
|
|
76
|
+
const section = container.closest('.section');
|
|
77
|
+
if (!section) return;
|
|
78
|
+
|
|
79
|
+
const header = section.querySelector('h3');
|
|
80
|
+
const content = section.querySelector('.section-content');
|
|
81
|
+
|
|
82
|
+
if (header && content) {
|
|
83
|
+
header.classList.remove('collapsed');
|
|
84
|
+
content.classList.remove('collapsed');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ===== COLOR PALETTE =====
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create color palette HTML
|
|
92
|
+
* @param {string} selectedColor - Currently selected color hex value
|
|
93
|
+
* @returns {string} HTML string for color palette
|
|
94
|
+
*/
|
|
95
|
+
function createColorPalette(selectedColor) {
|
|
96
|
+
const colors = [
|
|
97
|
+
{ name: 'Blue', value: '#3498db', emoji: '🔵' },
|
|
98
|
+
{ name: 'Red', value: '#e74c3c', emoji: '🔴' },
|
|
99
|
+
{ name: 'Green', value: '#27ae60', emoji: '🟢' },
|
|
100
|
+
{ name: 'Purple', value: '#9b59b6', emoji: '🟣' },
|
|
101
|
+
{ name: 'Orange', value: '#e67e22', emoji: '🟠' },
|
|
102
|
+
{ name: 'Yellow', value: '#f1c40f', emoji: '🟡' },
|
|
103
|
+
{ name: 'Grey', value: '#95a5a6', emoji: '⚪' },
|
|
104
|
+
{ name: 'Black', value: '#34495e', emoji: '⚫' },
|
|
105
|
+
{ name: 'Brown', value: '#8b4513', emoji: '🟤' }
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
return colors.map(color => {
|
|
109
|
+
const isSelected = color.value === selectedColor;
|
|
110
|
+
return `
|
|
111
|
+
<div class="color-swatch"
|
|
112
|
+
data-color="${color.value}"
|
|
113
|
+
style="font-size: 1.5em; cursor: pointer; transition: all 0.2s; opacity: ${isSelected ? '1' : '0.85'}; transform: ${isSelected ? 'scale(1.15)' : 'scale(1)'}; filter: ${isSelected ? 'drop-shadow(0 0 4px white)' : 'none'}; text-align: center;"
|
|
114
|
+
title="${color.name}">${color.emoji}</div>
|
|
115
|
+
`;
|
|
116
|
+
}).join('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Global flag to track if document-level click listener has been added
|
|
120
|
+
let colorPaletteDocumentListenerAdded = false;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initialize color palette selector
|
|
124
|
+
* Sets up the color picker dropdown for notification colors
|
|
125
|
+
*/
|
|
126
|
+
function initColorPalette() {
|
|
127
|
+
// Check if characterData is available
|
|
128
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
129
|
+
debug.warn('⚠️ characterData not available for color palette initialization');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Set default color if not set
|
|
134
|
+
if (!characterData.notificationColor) {
|
|
135
|
+
characterData.notificationColor = '#3498db';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const toggleBtnOld = document.getElementById('color-toggle');
|
|
139
|
+
const palette = document.getElementById('color-palette');
|
|
140
|
+
|
|
141
|
+
if (!toggleBtnOld || !palette) return;
|
|
142
|
+
|
|
143
|
+
// Clone and replace toggle button to remove old listeners
|
|
144
|
+
const toggleBtn = toggleBtnOld.cloneNode(true);
|
|
145
|
+
toggleBtnOld.parentNode.replaceChild(toggleBtn, toggleBtnOld);
|
|
146
|
+
|
|
147
|
+
// Toggle palette visibility
|
|
148
|
+
toggleBtn.addEventListener('click', (e) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
const isVisible = palette.style.display === 'grid';
|
|
151
|
+
palette.style.display = isVisible ? 'none' : 'grid';
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Add document-level click listener only once
|
|
155
|
+
if (!colorPaletteDocumentListenerAdded) {
|
|
156
|
+
// Close palette when clicking outside
|
|
157
|
+
document.addEventListener('click', (e) => {
|
|
158
|
+
const currentToggleBtn = document.getElementById('color-toggle');
|
|
159
|
+
const currentPalette = document.getElementById('color-palette');
|
|
160
|
+
if (currentPalette && currentToggleBtn) {
|
|
161
|
+
if (!currentPalette.contains(e.target) && e.target !== currentToggleBtn && !currentToggleBtn.contains(e.target)) {
|
|
162
|
+
currentPalette.style.display = 'none';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
colorPaletteDocumentListenerAdded = true;
|
|
167
|
+
debug.log('🎨 Added document-level color palette click listener');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add click handlers to color swatches
|
|
171
|
+
document.querySelectorAll('.color-swatch').forEach(swatch => {
|
|
172
|
+
swatch.addEventListener('click', (e) => {
|
|
173
|
+
const newColor = e.target.dataset.color;
|
|
174
|
+
const oldColor = characterData.notificationColor;
|
|
175
|
+
characterData.notificationColor = newColor;
|
|
176
|
+
|
|
177
|
+
// Update all swatches appearance
|
|
178
|
+
document.querySelectorAll('.color-swatch').forEach(s => {
|
|
179
|
+
const isSelected = s.dataset.color === newColor;
|
|
180
|
+
s.style.opacity = isSelected ? '1' : '0.6';
|
|
181
|
+
s.style.transform = isSelected ? 'scale(1.2)' : 'scale(1)';
|
|
182
|
+
s.style.filter = isSelected ? 'drop-shadow(0 0 4px white)' : 'none';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Update the toggle button emoji (using current element in DOM)
|
|
186
|
+
const newEmoji = getColorEmoji(newColor);
|
|
187
|
+
const colorEmojiEl = document.getElementById('color-emoji');
|
|
188
|
+
if (colorEmojiEl) {
|
|
189
|
+
colorEmojiEl.textContent = newEmoji;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Close the palette
|
|
193
|
+
palette.style.display = 'none';
|
|
194
|
+
|
|
195
|
+
// Refresh the portrait with the new color
|
|
196
|
+
if (typeof displayCharacterPortrait === 'function') {
|
|
197
|
+
displayCharacterPortrait('char-portrait', characterData, 120);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Save to storage
|
|
201
|
+
saveCharacterData();
|
|
202
|
+
|
|
203
|
+
// Sync to Supabase if available
|
|
204
|
+
syncColorToSupabase(newColor);
|
|
205
|
+
|
|
206
|
+
showNotification(`🎨 Notification color changed to ${e.target.title}!`);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Sync color selection to Supabase
|
|
213
|
+
* @param {string} color - Hex color value to sync
|
|
214
|
+
*/
|
|
215
|
+
async function syncColorToSupabase(color) {
|
|
216
|
+
try {
|
|
217
|
+
// Check if browserAPI is available
|
|
218
|
+
if (typeof browserAPI === 'undefined' || !browserAPI) {
|
|
219
|
+
debug.warn('⚠️ browserAPI not available, cannot sync color to Supabase');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Send message to background script to sync to Supabase
|
|
224
|
+
const response = await browserAPI.runtime.sendMessage({
|
|
225
|
+
action: 'syncCharacterColor',
|
|
226
|
+
characterId: characterData.id,
|
|
227
|
+
color: color
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (response && response.success) {
|
|
231
|
+
debug.log('🎨 Color synced to Supabase successfully');
|
|
232
|
+
} else {
|
|
233
|
+
debug.warn('⚠️ Failed to sync color to Supabase:', response?.error);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
debug.warn('⚠️ Error syncing color to Supabase:', error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ===== CLOSE BUTTON =====
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Initialize close button
|
|
244
|
+
* Adds click handler to close the popup window
|
|
245
|
+
*/
|
|
246
|
+
function initCloseButton() {
|
|
247
|
+
const closeBtn = document.getElementById('close-btn');
|
|
248
|
+
if (closeBtn) {
|
|
249
|
+
closeBtn.addEventListener('click', () => {
|
|
250
|
+
window.close();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ===== EXPORTS =====
|
|
256
|
+
|
|
257
|
+
// Export functions to globalThis
|
|
258
|
+
globalThis.initCollapsibleSections = initCollapsibleSections;
|
|
259
|
+
globalThis.collapseSectionByContainerId = collapseSectionByContainerId;
|
|
260
|
+
globalThis.expandSectionByContainerId = expandSectionByContainerId;
|
|
261
|
+
globalThis.createColorPalette = createColorPalette;
|
|
262
|
+
globalThis.initColorPalette = initColorPalette;
|
|
263
|
+
globalThis.syncColorToSupabase = syncColorToSupabase;
|
|
264
|
+
globalThis.initCloseButton = initCloseButton;
|
|
265
|
+
|
|
266
|
+
debug.log('✅ UI Utilities module loaded');
|
|
267
|
+
|
|
268
|
+
// ===== AUTO-INITIALIZATION =====
|
|
269
|
+
|
|
270
|
+
// Initialize collapsible sections when DOM is ready
|
|
271
|
+
if (document.readyState === 'loading') {
|
|
272
|
+
document.addEventListener('DOMContentLoaded', initCollapsibleSections);
|
|
273
|
+
} else {
|
|
274
|
+
initCollapsibleSections();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Initialize close button when DOM is ready
|
|
278
|
+
if (document.readyState === 'loading') {
|
|
279
|
+
document.addEventListener('DOMContentLoaded', initCloseButton);
|
|
280
|
+
} else {
|
|
281
|
+
initCloseButton();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
})();
|