@whykusanagi/corrupted-theme 0.1.5 → 0.1.7
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/CHANGELOG.md +53 -0
- package/README.md +207 -42
- package/docs/COMPONENTS_REFERENCE.md +142 -35
- package/docs/governance/VERSION_MANAGEMENT.md +2 -2
- package/docs/governance/VERSION_REFERENCES.md +30 -32
- package/docs/platforms/NPM_PACKAGE.md +8 -7
- package/examples/basic/multi-gallery.html +155 -0
- package/examples/button.html +5 -2
- package/examples/card.html +5 -2
- package/examples/extensions-showcase.html +5 -2
- package/examples/form.html +5 -2
- package/examples/index.html +8 -5
- package/examples/interactive-components.html +223 -0
- package/examples/layout.html +5 -2
- package/examples/nikke-team-builder.html +6 -3
- package/examples/showcase-complete.html +14 -13
- package/examples/showcase.html +6 -3
- package/package.json +6 -5
- package/src/core/corrupted-text.js +25 -5
- package/src/core/event-tracker.js +46 -0
- package/src/core/timer-registry.js +94 -0
- package/src/core/typing-animation.js +36 -17
- package/src/css/components.css +108 -0
- package/src/lib/carousel.js +308 -0
- package/src/lib/celeste-widget.js +178 -47
- package/src/lib/character-corruption.js +33 -8
- package/src/lib/components.js +357 -25
- package/src/lib/corrupted-text.js +21 -5
- package/src/lib/corruption-loading.js +40 -10
- package/src/lib/countdown-widget.js +25 -6
- package/src/lib/gallery.js +420 -354
|
@@ -42,6 +42,69 @@ class CelesteAgent {
|
|
|
42
42
|
this.messageContainer = null;
|
|
43
43
|
this.inputField = null;
|
|
44
44
|
this.sendButton = null;
|
|
45
|
+
|
|
46
|
+
// Lifecycle tracking (inline — this file uses IIFE-style globals, not ES imports)
|
|
47
|
+
this._trackedListeners = [];
|
|
48
|
+
this._styleElement = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Track an event listener for cleanup in destroy()
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
_trackListener(target, event, handler, options) {
|
|
56
|
+
target.addEventListener(event, handler, options);
|
|
57
|
+
this._trackedListeners.push({ target, event, handler, options });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Remove all tracked event listeners
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
_removeAllListeners() {
|
|
65
|
+
for (const { target, event, handler, options } of this._trackedListeners) {
|
|
66
|
+
target.removeEventListener(event, handler, options);
|
|
67
|
+
}
|
|
68
|
+
this._trackedListeners = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Tear down the widget: remove DOM, listeners, and global references.
|
|
73
|
+
* Safe to call multiple times.
|
|
74
|
+
*/
|
|
75
|
+
destroy() {
|
|
76
|
+
this._removeAllListeners();
|
|
77
|
+
|
|
78
|
+
if (this.chatButton && this.chatButton.parentNode) {
|
|
79
|
+
this.chatButton.parentNode.removeChild(this.chatButton);
|
|
80
|
+
}
|
|
81
|
+
if (this.chatWindow && this.chatWindow.parentNode) {
|
|
82
|
+
this.chatWindow.parentNode.removeChild(this.chatWindow);
|
|
83
|
+
}
|
|
84
|
+
if (this._styleElement && this._styleElement.parentNode) {
|
|
85
|
+
this._styleElement.parentNode.removeChild(this._styleElement);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.chatButton = null;
|
|
89
|
+
this.chatWindow = null;
|
|
90
|
+
this._styleElement = null;
|
|
91
|
+
this.isInitialized = false;
|
|
92
|
+
this.isOpen = false;
|
|
93
|
+
this.conversationHistory = [];
|
|
94
|
+
|
|
95
|
+
if (window.CelesteAgent === this) {
|
|
96
|
+
delete window.CelesteAgent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Update page context without re-creating UI.
|
|
102
|
+
* Used on SPA navigation (popstate).
|
|
103
|
+
*/
|
|
104
|
+
async updateContext() {
|
|
105
|
+
if (!this.isInitialized) return;
|
|
106
|
+
this.currentContext = await this.getPageContext();
|
|
107
|
+
this.loadContextualPrompt();
|
|
45
108
|
}
|
|
46
109
|
|
|
47
110
|
/**
|
|
@@ -444,44 +507,98 @@ class CelesteAgent {
|
|
|
444
507
|
* Create UI elements
|
|
445
508
|
*/
|
|
446
509
|
createUI() {
|
|
447
|
-
// Create chat button
|
|
510
|
+
// Create chat button (safe DOM construction — no innerHTML with interpolation)
|
|
448
511
|
this.chatButton = document.createElement('div');
|
|
449
512
|
this.chatButton.className = 'celeste-chat-button';
|
|
450
513
|
const avatarUrl = this.getAssetUrl('https://s3.whykusanagi.xyz/Celeste_Vel_Icon.png');
|
|
451
|
-
this.chatButton.innerHTML = `
|
|
452
|
-
<div class="celeste-button-content">
|
|
453
|
-
<img src="${avatarUrl}" alt="Celeste AI" class="celeste-avatar" onerror="this.style.display='none'; this.parentElement.style.background='linear-gradient(135deg, #d94f90 0%, #b61b70 100%)';">
|
|
454
|
-
<span class="celeste-button-text">Chat with Celeste</span>
|
|
455
|
-
</div>
|
|
456
|
-
`;
|
|
457
|
-
this.chatButton.addEventListener('click', () => this.toggleChat());
|
|
458
514
|
|
|
459
|
-
|
|
515
|
+
const buttonContent = document.createElement('div');
|
|
516
|
+
buttonContent.className = 'celeste-button-content';
|
|
517
|
+
|
|
518
|
+
const avatarImg = document.createElement('img');
|
|
519
|
+
avatarImg.src = avatarUrl;
|
|
520
|
+
avatarImg.alt = 'Celeste AI';
|
|
521
|
+
avatarImg.className = 'celeste-avatar';
|
|
522
|
+
avatarImg.addEventListener('error', function() {
|
|
523
|
+
this.style.display = 'none';
|
|
524
|
+
this.parentElement.style.background = 'linear-gradient(135deg, #d94f90 0%, #b61b70 100%)';
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const buttonText = document.createElement('span');
|
|
528
|
+
buttonText.className = 'celeste-button-text';
|
|
529
|
+
buttonText.textContent = 'Chat with Celeste';
|
|
530
|
+
|
|
531
|
+
buttonContent.appendChild(avatarImg);
|
|
532
|
+
buttonContent.appendChild(buttonText);
|
|
533
|
+
this.chatButton.appendChild(buttonContent);
|
|
534
|
+
this._trackListener(this.chatButton, 'click', () => this.toggleChat());
|
|
535
|
+
|
|
536
|
+
// Create chat window (safe DOM construction — no innerHTML with interpolation)
|
|
460
537
|
this.chatWindow = document.createElement('div');
|
|
461
538
|
this.chatWindow.className = 'celeste-chat-window';
|
|
462
539
|
const headerAvatarUrl = this.getAssetUrl('https://s3.whykusanagi.xyz/Celeste_Vel_Icon.png');
|
|
463
|
-
this.chatWindow.innerHTML = `
|
|
464
|
-
<div class="celeste-chat-header">
|
|
465
|
-
<div class="celeste-header-content">
|
|
466
|
-
<img src="${headerAvatarUrl}" alt="Celeste AI" class="celeste-header-avatar" onerror="this.style.display='none';">
|
|
467
|
-
<div class="celeste-header-info">
|
|
468
|
-
<h3><strong>CelesteAI</strong></h3>
|
|
469
|
-
<p><strong>Your helpful Onee-san assistant</strong></p>
|
|
470
|
-
</div>
|
|
471
|
-
</div>
|
|
472
|
-
<button class="celeste-close-btn">×</button>
|
|
473
|
-
</div>
|
|
474
|
-
<div class="celeste-chat-messages"></div>
|
|
475
|
-
<div class="celeste-chat-input">
|
|
476
|
-
<input type="text" placeholder="Ask Celeste anything..." class="celeste-input-field">
|
|
477
|
-
<button class="celeste-send-btn">Send</button>
|
|
478
|
-
</div>
|
|
479
|
-
`;
|
|
480
540
|
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
541
|
+
// Header
|
|
542
|
+
const chatHeader = document.createElement('div');
|
|
543
|
+
chatHeader.className = 'celeste-chat-header';
|
|
544
|
+
|
|
545
|
+
const headerContent = document.createElement('div');
|
|
546
|
+
headerContent.className = 'celeste-header-content';
|
|
547
|
+
|
|
548
|
+
const headerAvatar = document.createElement('img');
|
|
549
|
+
headerAvatar.src = headerAvatarUrl;
|
|
550
|
+
headerAvatar.alt = 'Celeste AI';
|
|
551
|
+
headerAvatar.className = 'celeste-header-avatar';
|
|
552
|
+
headerAvatar.addEventListener('error', function() { this.style.display = 'none'; });
|
|
553
|
+
|
|
554
|
+
const headerInfo = document.createElement('div');
|
|
555
|
+
headerInfo.className = 'celeste-header-info';
|
|
556
|
+
const h3 = document.createElement('h3');
|
|
557
|
+
const h3Strong = document.createElement('strong');
|
|
558
|
+
h3Strong.textContent = 'CelesteAI';
|
|
559
|
+
h3.appendChild(h3Strong);
|
|
560
|
+
const p = document.createElement('p');
|
|
561
|
+
const pStrong = document.createElement('strong');
|
|
562
|
+
pStrong.textContent = 'Your helpful Onee-san assistant';
|
|
563
|
+
p.appendChild(pStrong);
|
|
564
|
+
headerInfo.appendChild(h3);
|
|
565
|
+
headerInfo.appendChild(p);
|
|
566
|
+
|
|
567
|
+
headerContent.appendChild(headerAvatar);
|
|
568
|
+
headerContent.appendChild(headerInfo);
|
|
569
|
+
|
|
570
|
+
const closeBtn = document.createElement('button');
|
|
571
|
+
closeBtn.className = 'celeste-close-btn';
|
|
572
|
+
closeBtn.textContent = '\u00D7';
|
|
573
|
+
|
|
574
|
+
chatHeader.appendChild(headerContent);
|
|
575
|
+
chatHeader.appendChild(closeBtn);
|
|
576
|
+
|
|
577
|
+
// Messages container
|
|
578
|
+
const messagesDiv = document.createElement('div');
|
|
579
|
+
messagesDiv.className = 'celeste-chat-messages';
|
|
580
|
+
|
|
581
|
+
// Input area
|
|
582
|
+
const inputDiv = document.createElement('div');
|
|
583
|
+
inputDiv.className = 'celeste-chat-input';
|
|
584
|
+
const inputField = document.createElement('input');
|
|
585
|
+
inputField.type = 'text';
|
|
586
|
+
inputField.placeholder = 'Ask Celeste anything...';
|
|
587
|
+
inputField.className = 'celeste-input-field';
|
|
588
|
+
const sendBtn = document.createElement('button');
|
|
589
|
+
sendBtn.className = 'celeste-send-btn';
|
|
590
|
+
sendBtn.textContent = 'Send';
|
|
591
|
+
inputDiv.appendChild(inputField);
|
|
592
|
+
inputDiv.appendChild(sendBtn);
|
|
593
|
+
|
|
594
|
+
this.chatWindow.appendChild(chatHeader);
|
|
595
|
+
this.chatWindow.appendChild(messagesDiv);
|
|
596
|
+
this.chatWindow.appendChild(inputDiv);
|
|
597
|
+
|
|
598
|
+
// Add event listeners (tracked for cleanup)
|
|
599
|
+
this._trackListener(this.chatWindow.querySelector('.celeste-close-btn'), 'click', () => this.closeChat());
|
|
600
|
+
this._trackListener(this.chatWindow.querySelector('.celeste-send-btn'), 'click', () => this.sendMessage());
|
|
601
|
+
this._trackListener(this.chatWindow.querySelector('.celeste-input-field'), 'keypress', (e) => {
|
|
485
602
|
if (e.key === 'Enter') this.sendMessage();
|
|
486
603
|
});
|
|
487
604
|
|
|
@@ -497,7 +614,10 @@ class CelesteAgent {
|
|
|
497
614
|
* Add CSS styles
|
|
498
615
|
*/
|
|
499
616
|
addStyles() {
|
|
617
|
+
// Reuse existing style element if re-initializing
|
|
618
|
+
if (this._styleElement) return;
|
|
500
619
|
const style = document.createElement('style');
|
|
620
|
+
this._styleElement = style;
|
|
501
621
|
style.textContent = `
|
|
502
622
|
.celeste-chat-button {
|
|
503
623
|
position: fixed;
|
|
@@ -792,9 +912,10 @@ class CelesteAgent {
|
|
|
792
912
|
const messageContainer = this.chatWindow.querySelector('.celeste-chat-messages');
|
|
793
913
|
const messageDiv = document.createElement('div');
|
|
794
914
|
messageDiv.className = `celeste-message ${sender}`;
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
915
|
+
const bubble = document.createElement('div');
|
|
916
|
+
bubble.className = 'celeste-message-bubble';
|
|
917
|
+
bubble.textContent = text;
|
|
918
|
+
messageDiv.appendChild(bubble);
|
|
798
919
|
messageContainer.appendChild(messageDiv);
|
|
799
920
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
|
800
921
|
|
|
@@ -809,18 +930,23 @@ class CelesteAgent {
|
|
|
809
930
|
const messageContainer = this.chatWindow.querySelector('.celeste-chat-messages');
|
|
810
931
|
const typingDiv = document.createElement('div');
|
|
811
932
|
typingDiv.className = 'celeste-message celeste';
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
933
|
+
|
|
934
|
+
// Safe DOM construction — no innerHTML
|
|
935
|
+
const bubble = document.createElement('div');
|
|
936
|
+
bubble.className = 'celeste-message-bubble';
|
|
937
|
+
const typing = document.createElement('div');
|
|
938
|
+
typing.className = 'celeste-typing';
|
|
939
|
+
typing.appendChild(document.createTextNode('Celeste is typing'));
|
|
940
|
+
const dots = document.createElement('div');
|
|
941
|
+
dots.className = 'celeste-typing-dots';
|
|
942
|
+
for (let i = 0; i < 3; i++) {
|
|
943
|
+
const dot = document.createElement('div');
|
|
944
|
+
dot.className = 'celeste-typing-dot';
|
|
945
|
+
dots.appendChild(dot);
|
|
946
|
+
}
|
|
947
|
+
typing.appendChild(dots);
|
|
948
|
+
bubble.appendChild(typing);
|
|
949
|
+
typingDiv.appendChild(bubble);
|
|
824
950
|
messageContainer.appendChild(typingDiv);
|
|
825
951
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
|
826
952
|
return typingDiv;
|
|
@@ -1074,6 +1200,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1074
1200
|
return;
|
|
1075
1201
|
}
|
|
1076
1202
|
|
|
1203
|
+
// Destroy previous instance if it exists (SPA re-init safety)
|
|
1204
|
+
if (window.CelesteAgent && typeof window.CelesteAgent.destroy === 'function') {
|
|
1205
|
+
window.CelesteAgent.destroy();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1077
1208
|
const celesteAgent = new CelesteAgent();
|
|
1078
1209
|
celesteAgent.initialize();
|
|
1079
1210
|
|
|
@@ -1081,9 +1212,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1081
1212
|
window.CelesteAgent = celesteAgent;
|
|
1082
1213
|
});
|
|
1083
1214
|
|
|
1084
|
-
// Update context on page navigation
|
|
1215
|
+
// Update context on page navigation (don't re-create UI, just refresh context)
|
|
1085
1216
|
window.addEventListener('popstate', () => {
|
|
1086
1217
|
if (window.CelesteAgent) {
|
|
1087
|
-
window.CelesteAgent.
|
|
1218
|
+
window.CelesteAgent.updateContext();
|
|
1088
1219
|
}
|
|
1089
1220
|
});
|
|
@@ -46,6 +46,14 @@ export const INTENSITY = {
|
|
|
46
46
|
MAX_READABLE: 0.45
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* WeakMap for tracking corruption interval IDs per element.
|
|
51
|
+
* Using WeakMap instead of dataset avoids string coercion issues
|
|
52
|
+
* and allows GC when elements are removed from DOM.
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
const _intervalMap = new WeakMap();
|
|
56
|
+
|
|
49
57
|
/**
|
|
50
58
|
* Character sets for corruption
|
|
51
59
|
* Organized by usage frequency to match CLI behavior
|
|
@@ -265,13 +273,14 @@ export function initAutoCorruption() {
|
|
|
265
273
|
// Check if element still exists in DOM
|
|
266
274
|
if (!document.contains(element)) {
|
|
267
275
|
clearInterval(intervalId);
|
|
276
|
+
_intervalMap.delete(element);
|
|
268
277
|
return;
|
|
269
278
|
}
|
|
270
279
|
element.textContent = corruptTextJapanese(originalText, intensity);
|
|
271
280
|
}, interval);
|
|
272
281
|
|
|
273
|
-
// Store interval ID for cleanup
|
|
274
|
-
element
|
|
282
|
+
// Store interval ID for cleanup via WeakMap
|
|
283
|
+
_intervalMap.set(element, intervalId);
|
|
275
284
|
}
|
|
276
285
|
|
|
277
286
|
// Mark as initialized
|
|
@@ -285,11 +294,12 @@ export function initAutoCorruption() {
|
|
|
285
294
|
* @param {HTMLElement} element - Element to stop corrupting
|
|
286
295
|
*/
|
|
287
296
|
export function stopAutoCorruption(element) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
delete
|
|
297
|
+
const intervalId = _intervalMap.get(element);
|
|
298
|
+
if (intervalId != null) {
|
|
299
|
+
clearInterval(intervalId);
|
|
300
|
+
_intervalMap.delete(element);
|
|
292
301
|
}
|
|
302
|
+
delete element.dataset.corruptionInitialized;
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
/**
|
|
@@ -309,16 +319,29 @@ export function restartAutoCorruption(element) {
|
|
|
309
319
|
const intervalId = setInterval(() => {
|
|
310
320
|
if (!document.contains(element)) {
|
|
311
321
|
clearInterval(intervalId);
|
|
322
|
+
_intervalMap.delete(element);
|
|
312
323
|
return;
|
|
313
324
|
}
|
|
314
325
|
element.textContent = corruptTextJapanese(originalText, intensity);
|
|
315
326
|
}, interval);
|
|
316
327
|
|
|
317
|
-
element
|
|
328
|
+
_intervalMap.set(element, intervalId);
|
|
318
329
|
element.dataset.corruptionInitialized = 'true';
|
|
319
330
|
}
|
|
320
331
|
}
|
|
321
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Stop all active auto-corruption intervals
|
|
335
|
+
*
|
|
336
|
+
* Finds all initialized auto-corrupt elements and stops their intervals.
|
|
337
|
+
* Useful for cleanup on page transitions or component teardown.
|
|
338
|
+
*/
|
|
339
|
+
export function destroyAllAutoCorruption() {
|
|
340
|
+
document.querySelectorAll('.auto-corrupt[data-corruption-initialized="true"]').forEach(element => {
|
|
341
|
+
stopAutoCorruption(element);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
322
345
|
/**
|
|
323
346
|
* Utility: Create a corrupted text element
|
|
324
347
|
*
|
|
@@ -361,11 +384,12 @@ export function createCorruptedElement(text, options = {}) {
|
|
|
361
384
|
const intervalId = setInterval(() => {
|
|
362
385
|
if (!document.contains(element)) {
|
|
363
386
|
clearInterval(intervalId);
|
|
387
|
+
_intervalMap.delete(element);
|
|
364
388
|
return;
|
|
365
389
|
}
|
|
366
390
|
element.textContent = corruptTextJapanese(text, intensity);
|
|
367
391
|
}, interval);
|
|
368
|
-
element
|
|
392
|
+
_intervalMap.set(element, intervalId);
|
|
369
393
|
}
|
|
370
394
|
|
|
371
395
|
element.dataset.corruptionInitialized = 'true';
|
|
@@ -555,6 +579,7 @@ export default {
|
|
|
555
579
|
initAutoCorruption,
|
|
556
580
|
stopAutoCorruption,
|
|
557
581
|
restartAutoCorruption,
|
|
582
|
+
destroyAllAutoCorruption,
|
|
558
583
|
createCorruptedElement,
|
|
559
584
|
getRandomPhrase,
|
|
560
585
|
INTENSITY,
|