hexo-theme-gnix 6.2.0 → 8.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 +6 -2
- package/include/hexo/encrypt.js +42 -0
- package/include/hexo/feed.js +329 -0
- package/include/util/common.js +7 -9
- package/languages/en.yml +6 -3
- package/languages/zh-CN.yml +6 -3
- package/layout/archive.jsx +86 -65
- package/layout/comment/twikoo.jsx +2 -11
- package/layout/comment/waline.jsx +2 -2
- package/layout/common/article.jsx +5 -8
- package/layout/common/article_cover.jsx +11 -1
- package/layout/common/article_media.jsx +2 -4
- package/layout/common/footer.jsx +11 -31
- package/layout/common/head.jsx +6 -14
- package/layout/common/navbar.jsx +4 -4
- package/layout/common/scripts.jsx +6 -6
- package/layout/common/theme_selector.jsx +5 -6
- package/layout/common/toc.jsx +8 -14
- package/layout/index.jsx +2 -4
- package/layout/misc/article_licensing.jsx +4 -2
- package/layout/misc/open_graph.jsx +4 -4
- package/layout/misc/paginator.jsx +10 -4
- package/layout/misc/structured_data.jsx +3 -4
- package/layout/plugin/busuanzi.jsx +1 -1
- package/layout/plugin/cookie_consent.jsx +40 -31
- package/layout/plugin/swup.jsx +2 -22
- package/layout/search/insight.jsx +16 -3
- package/package.json +12 -8
- package/scripts/hot-reload.js +92 -0
- package/scripts/index.js +2 -0
- package/source/css/archive.css +251 -0
- package/source/css/default.css +250 -284
- package/source/css/encrypt.css +55 -0
- package/source/css/responsive/desktop.css +0 -119
- package/source/css/responsive/mobile.css +7 -23
- package/source/css/responsive/touch.css +9 -103
- package/source/css/shiki/shiki.css +7 -22
- package/source/css/twikoo.css +290 -830
- package/source/img/og_image.webp +0 -0
- package/source/js/archive-breadcrumb.js +132 -0
- package/source/js/busuanzi.js +1 -12
- package/source/js/components/accordion.js +192 -0
- package/source/js/components/chat.js +239 -0
- package/source/js/components/device-carousel.js +260 -0
- package/source/js/components/image-carousel.js +410 -0
- package/source/js/components/text-image-section.js +180 -0
- package/source/js/components/theme-stacked.js +526 -0
- package/source/js/components/tree.js +437 -0
- package/source/js/decrypt.js +112 -0
- package/source/js/insight.js +75 -65
- package/source/js/main.js +192 -99
- package/source/js/mdit/mermaid.js +12 -4
- package/source/js/swup.bundle.js +1 -0
- package/source/js/theme-selector.js +94 -113
- package/source/img/og_image.png +0 -0
- package/source/js/host/swup/Swup.umd.min.js +0 -1
- package/source/js/host/swup/head-plugin.umd.min.js +0 -1
- package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
- package/source/js/mdit/shiki.js +0 -158
|
Binary file
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
function parseArchiveLocation(pathname, archiveDir) {
|
|
3
|
+
const segments = String(pathname || "")
|
|
4
|
+
.replace(/\/+$/, "")
|
|
5
|
+
.split("/")
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
|
|
8
|
+
const index = segments.lastIndexOf(archiveDir);
|
|
9
|
+
if (index === -1) return { year: null, month: null };
|
|
10
|
+
|
|
11
|
+
const yearRaw = segments[index + 1] || null;
|
|
12
|
+
const monthRaw = segments[index + 2] || null;
|
|
13
|
+
|
|
14
|
+
const year = yearRaw && /^\d{4}$/.test(yearRaw) ? Number(yearRaw) : null;
|
|
15
|
+
const month = monthRaw && /^\d{1,2}$/.test(monthRaw) ? Number(monthRaw) : null;
|
|
16
|
+
|
|
17
|
+
if (month && (month < 1 || month > 12)) return { year, month: null };
|
|
18
|
+
return { year, month };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getMenuForTrigger(trigger) {
|
|
22
|
+
const menu = trigger?.nextElementSibling;
|
|
23
|
+
if (!menu?.classList.contains("archive-breadcrumb__menu")) return null;
|
|
24
|
+
return menu;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getOptions(menu) {
|
|
28
|
+
return Array.from(menu.querySelectorAll(".archive-breadcrumb__option"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setSelected(menu, isSelectedFn) {
|
|
32
|
+
const options = getOptions(menu);
|
|
33
|
+
for (const option of options) {
|
|
34
|
+
option.setAttribute("aria-selected", isSelectedFn(option) ? "true" : "false");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function syncFromLocation(root) {
|
|
39
|
+
const archiveDir = root.dataset.archiveDir || "archives";
|
|
40
|
+
const { year, month } = parseArchiveLocation(window.location.pathname, archiveDir);
|
|
41
|
+
|
|
42
|
+
const yearLabel = root.querySelector('[data-label="year"]');
|
|
43
|
+
const monthLabel = root.querySelector('[data-label="month"]');
|
|
44
|
+
if (yearLabel) yearLabel.textContent = year ? String(year) : "*";
|
|
45
|
+
if (monthLabel) monthLabel.textContent = month ? String(month).padStart(2, "0") : "*";
|
|
46
|
+
|
|
47
|
+
const yearTrigger = root.querySelector('.archive-breadcrumb__picker[data-picker="year"] .archive-breadcrumb__trigger');
|
|
48
|
+
const yearMenu = yearTrigger ? getMenuForTrigger(yearTrigger) : null;
|
|
49
|
+
if (yearMenu) {
|
|
50
|
+
setSelected(yearMenu, (opt) => {
|
|
51
|
+
const text = opt.textContent?.trim();
|
|
52
|
+
if (!year) return text === "*";
|
|
53
|
+
return text === String(year);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const monthTrigger = root.querySelector('.archive-breadcrumb__picker[data-picker="month"] .archive-breadcrumb__trigger');
|
|
58
|
+
const monthMenu = monthTrigger ? getMenuForTrigger(monthTrigger) : null;
|
|
59
|
+
if (monthTrigger) monthTrigger.disabled = !year;
|
|
60
|
+
if (monthMenu) {
|
|
61
|
+
setSelected(monthMenu, (opt) => {
|
|
62
|
+
const text = opt.textContent?.trim();
|
|
63
|
+
if (!year) return text === "*";
|
|
64
|
+
if (!month) return text === "*";
|
|
65
|
+
return text === String(month).padStart(2, "0");
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
closeAll(root);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function closeAll(root) {
|
|
73
|
+
const triggers = root.querySelectorAll(".archive-breadcrumb__trigger");
|
|
74
|
+
for (const trigger of triggers) {
|
|
75
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function openMenu(trigger) {
|
|
80
|
+
if (trigger.disabled) return;
|
|
81
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toggleMenu(trigger, root) {
|
|
85
|
+
if (trigger.disabled) return;
|
|
86
|
+
const expanded = trigger.getAttribute("aria-expanded") === "true";
|
|
87
|
+
closeAll(root);
|
|
88
|
+
if (!expanded) {
|
|
89
|
+
openMenu(trigger);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function navigateToOption(option) {
|
|
94
|
+
const href = option?.dataset?.href;
|
|
95
|
+
if (!href) return;
|
|
96
|
+
window.location.assign(href);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function init(root) {
|
|
100
|
+
root.addEventListener("click", (e) => {
|
|
101
|
+
const target = e.target.closest?.(".archive-breadcrumb__option, .archive-breadcrumb__trigger");
|
|
102
|
+
if (!target) {
|
|
103
|
+
closeAll(root);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (target.classList.contains("archive-breadcrumb__option")) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
navigateToOption(target);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (target.classList.contains("archive-breadcrumb__trigger")) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
toggleMenu(target, root);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
syncFromLocation(root);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function initAll() {
|
|
123
|
+
const roots = document.querySelectorAll("[data-archive-breadcrumb]");
|
|
124
|
+
for (const root of roots) {
|
|
125
|
+
if (root._gnixArchiveBreadcrumbInited) continue;
|
|
126
|
+
root._gnixArchiveBreadcrumbInited = true;
|
|
127
|
+
init(root);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
initAll();
|
|
132
|
+
})();
|
package/source/js/busuanzi.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
!(() => {
|
|
2
2
|
const TYPES = ["site_pv", "site_uv", "page_pv", "page_uv"];
|
|
3
|
-
const script = document.currentScript;
|
|
4
|
-
const api = script.getAttribute("data-api") || "https://bsz.dusays.com:9001/api";
|
|
5
3
|
const STORAGE_KEY = "bsz-id";
|
|
6
4
|
const BASE = { site_pv: 12801, site_uv: 2450 };
|
|
7
5
|
|
|
8
6
|
const update = () => {
|
|
9
7
|
const xhr = new XMLHttpRequest();
|
|
10
|
-
xhr.open("POST", api, true);
|
|
8
|
+
xhr.open("POST", "https://bsz.dusays.com:9001/api", true);
|
|
11
9
|
|
|
12
10
|
const token = localStorage.getItem(STORAGE_KEY);
|
|
13
11
|
token && xhr.setRequestHeader("Authorization", `Bearer ${token}`);
|
|
@@ -33,13 +31,4 @@
|
|
|
33
31
|
};
|
|
34
32
|
|
|
35
33
|
update();
|
|
36
|
-
|
|
37
|
-
if (script.hasAttribute("pjax")) {
|
|
38
|
-
const pushState = history.pushState;
|
|
39
|
-
history.pushState = function (...args) {
|
|
40
|
-
pushState.apply(this, ...args);
|
|
41
|
-
update();
|
|
42
|
-
};
|
|
43
|
-
addEventListener("popstate", update);
|
|
44
|
-
}
|
|
45
34
|
})();
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accordion Custom Element
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* <x-accordion>
|
|
6
|
+
* <accordion-item title="Introduction">
|
|
7
|
+
* Content here...
|
|
8
|
+
* </accordion-item>
|
|
9
|
+
* <accordion-item title="Design Patterns">
|
|
10
|
+
* More content...
|
|
11
|
+
* </accordion-item>
|
|
12
|
+
* </x-accordion>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
let styleSheetInjected = false;
|
|
16
|
+
|
|
17
|
+
class Accordion extends HTMLElement {
|
|
18
|
+
connectedCallback() {
|
|
19
|
+
this.injectStyles();
|
|
20
|
+
this.render();
|
|
21
|
+
this.setupListeners();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
injectStyles() {
|
|
25
|
+
if (styleSheetInjected) return;
|
|
26
|
+
|
|
27
|
+
const style = `
|
|
28
|
+
x-accordion {
|
|
29
|
+
display: block;
|
|
30
|
+
margin: 1em 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.accordion-item {
|
|
34
|
+
border-bottom: 1px solid var(--surface0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.accordion-item:last-child {
|
|
38
|
+
border-bottom: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.accordion-header {
|
|
42
|
+
width: 100%;
|
|
43
|
+
padding: 16px 0;
|
|
44
|
+
border: none;
|
|
45
|
+
color: var(--text);
|
|
46
|
+
background: transparent;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 12px;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
transition: color 0.2s;
|
|
52
|
+
text-align: left;
|
|
53
|
+
& :hover {
|
|
54
|
+
color: var(--subtext0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.accordion-icon {
|
|
59
|
+
font-size: 16px;
|
|
60
|
+
font-weight: 300;
|
|
61
|
+
color: var(--subtext0);
|
|
62
|
+
transition: transform 0.2s ease;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
width: 16px;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.accordion-item.active .accordion-icon {
|
|
70
|
+
transform: rotate(45deg);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.accordion-content {
|
|
74
|
+
max-height: 0;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
transition: max-height 0.3s ease;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.accordion-content-inner {
|
|
80
|
+
padding: 0 0 16px 28px;
|
|
81
|
+
color: var(--subtext1);
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
line-height: 1.6;
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const styleEl = document.createElement("style");
|
|
88
|
+
styleEl.textContent = style;
|
|
89
|
+
document.head.appendChild(styleEl);
|
|
90
|
+
styleSheetInjected = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
render() {
|
|
94
|
+
const items = this.querySelectorAll("accordion-item");
|
|
95
|
+
if (items.length === 0) return; // Already rendered or empty
|
|
96
|
+
|
|
97
|
+
const itemsHTML = Array.from(items)
|
|
98
|
+
.map((item) => {
|
|
99
|
+
const title = item.getAttribute("title") || "Item";
|
|
100
|
+
|
|
101
|
+
// Filter out nested accordion-item elements to handle malformed HTML
|
|
102
|
+
const contentNodes = Array.from(item.childNodes).filter((node) => {
|
|
103
|
+
return node.nodeType !== Node.ELEMENT_NODE || node.tagName.toLowerCase() !== "accordion-item";
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const content = contentNodes
|
|
107
|
+
.map((node) => {
|
|
108
|
+
return node.nodeType === Node.TEXT_NODE ? node.textContent : node.outerHTML;
|
|
109
|
+
})
|
|
110
|
+
.join("");
|
|
111
|
+
|
|
112
|
+
return `
|
|
113
|
+
<div class="accordion-item">
|
|
114
|
+
<button class="accordion-header" aria-expanded="false">
|
|
115
|
+
<span class="accordion-icon">+</span>
|
|
116
|
+
<span>${title}</span>
|
|
117
|
+
</button>
|
|
118
|
+
<div class="accordion-content">
|
|
119
|
+
<div class="accordion-content-inner content">${content}</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
})
|
|
124
|
+
.join("");
|
|
125
|
+
|
|
126
|
+
this.innerHTML = itemsHTML;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setupListeners() {
|
|
130
|
+
const headers = this.querySelectorAll(".accordion-header");
|
|
131
|
+
|
|
132
|
+
headers.forEach((header) => {
|
|
133
|
+
header.addEventListener("click", () => {
|
|
134
|
+
const item = header.closest(".accordion-item");
|
|
135
|
+
const isActive = item.classList.contains("active");
|
|
136
|
+
const content = item.querySelector(".accordion-content");
|
|
137
|
+
|
|
138
|
+
// Close all items (accordion mode - single expansion)
|
|
139
|
+
this.querySelectorAll(".accordion-item").forEach((i) => {
|
|
140
|
+
i.classList.remove("active");
|
|
141
|
+
i.querySelector(".accordion-header").setAttribute("aria-expanded", "false");
|
|
142
|
+
const c = i.querySelector(".accordion-content");
|
|
143
|
+
if (c) c.style.maxHeight = null;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Open clicked item if it wasn't active
|
|
147
|
+
if (!isActive) {
|
|
148
|
+
item.classList.add("active");
|
|
149
|
+
header.setAttribute("aria-expanded", "true");
|
|
150
|
+
if (content) {
|
|
151
|
+
content.style.maxHeight = `${content.scrollHeight}px`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Public method to expand a specific item by index
|
|
159
|
+
expandItem(index) {
|
|
160
|
+
const items = this.querySelectorAll(".accordion-item");
|
|
161
|
+
if (items[index]) {
|
|
162
|
+
items[index].querySelector(".accordion-header").click();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Public method to collapse all items
|
|
167
|
+
collapseAll() {
|
|
168
|
+
this.querySelectorAll(".accordion-item").forEach((item) => {
|
|
169
|
+
item.classList.remove("active");
|
|
170
|
+
item.querySelector(".accordion-header").setAttribute("aria-expanded", "false");
|
|
171
|
+
const content = item.querySelector(".accordion-content");
|
|
172
|
+
if (content) content.style.maxHeight = null;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static get observedAttributes() {
|
|
177
|
+
return ["single-expand"];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
attributeChangedCallback() {
|
|
181
|
+
// Future: support multi-expand mode
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Define accordion-item as a placeholder for slot content
|
|
186
|
+
class AccordionItem extends HTMLElement {}
|
|
187
|
+
|
|
188
|
+
// Register custom elements
|
|
189
|
+
customElements.define("x-accordion", Accordion);
|
|
190
|
+
customElements.define("accordion-item", AccordionItem);
|
|
191
|
+
|
|
192
|
+
export { Accordion, AccordionItem };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Component Custom Element
|
|
3
|
+
* Displays group chat conversations with avatars, names, timestamps, and messages
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <x-chat>
|
|
7
|
+
* <chat-message
|
|
8
|
+
* name="User Name"
|
|
9
|
+
* avatar="/path/to/avatar.png"
|
|
10
|
+
* timestamp="2024-01-15 10:30"
|
|
11
|
+
* is-me
|
|
12
|
+
* >
|
|
13
|
+
* Message content here...
|
|
14
|
+
* </chat-message>
|
|
15
|
+
* <chat-message name="Other User" avatar="/path/to/avatar.png" timestamp="2024-01-15 10:32">
|
|
16
|
+
* Another message...
|
|
17
|
+
* </chat-message>
|
|
18
|
+
* </x-chat>
|
|
19
|
+
*
|
|
20
|
+
* Or with data attribute:
|
|
21
|
+
* <x-chat messages='[{"name": "User", "content": "Hello", "timestamp": "10:30"}]'></x-chat>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const CHAT_STYLES = `
|
|
25
|
+
:host {
|
|
26
|
+
display: block;
|
|
27
|
+
font-family: var(--font-sans, sans-serif);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.chat-container {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 16px;
|
|
34
|
+
padding: 16px;
|
|
35
|
+
background: var(--base, #1e1e2e);
|
|
36
|
+
border-radius: 12px;
|
|
37
|
+
max-height: 500px;
|
|
38
|
+
overflow-y: auto;
|
|
39
|
+
scrollbar-width: none;
|
|
40
|
+
-ms-overflow-style: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.chat-container::-webkit-scrollbar {
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.chat-message {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: 12px;
|
|
50
|
+
align-items: flex-start;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.chat-message.is-me {
|
|
54
|
+
flex-direction: row-reverse;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.avatar {
|
|
58
|
+
width: 40px;
|
|
59
|
+
height: 40px;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.avatar-placeholder {
|
|
66
|
+
width: 40px;
|
|
67
|
+
height: 40px;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
font-size: 18px;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
color: var(--text, #cdd6f4);
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
background: var(--surface1, #45475a);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.message-content {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: 4px;
|
|
83
|
+
max-width: 70%;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.chat-message.is-me .message-content {
|
|
87
|
+
align-items: flex-end;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.message-header {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 8px;
|
|
94
|
+
font-size: 12px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.chat-message.is-me .message-header {
|
|
98
|
+
flex-direction: row-reverse;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sender-name {
|
|
102
|
+
font-weight: 600;
|
|
103
|
+
color: var(--subtext1, #bac2de);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.timestamp {
|
|
107
|
+
color: var(--subtext0, #a6adc8);
|
|
108
|
+
font-size: 11px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.message-bubble {
|
|
112
|
+
padding: 10px 14px;
|
|
113
|
+
border-radius: 16px;
|
|
114
|
+
background: var(--surface1, #45475a);
|
|
115
|
+
color: var(--text, #cdd6f4);
|
|
116
|
+
line-height: 1.5;
|
|
117
|
+
font-size: 14px;
|
|
118
|
+
word-wrap: break-word;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.chat-message.is-me .message-bubble {
|
|
122
|
+
background: var(--blue, #89b4fa);
|
|
123
|
+
color: var(--base, #1e1e2e);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.message-bubble a {
|
|
127
|
+
color: var(--blue, #89b4fa);
|
|
128
|
+
text-decoration: underline;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.message-bubble code {
|
|
132
|
+
font-family: var(--font-mono, 'Maple Mono', 'Fira Code', monospace);
|
|
133
|
+
font-size: 13px;
|
|
134
|
+
background: var(--mantle, #181825);
|
|
135
|
+
padding: 2px 6px;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.empty-state {
|
|
140
|
+
text-align: center;
|
|
141
|
+
padding: 40px 20px;
|
|
142
|
+
color: var(--subtext0, #a6adc8);
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
class Chat extends HTMLElement {
|
|
147
|
+
constructor() {
|
|
148
|
+
super();
|
|
149
|
+
this.attachShadow({ mode: "open" });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
connectedCallback() {
|
|
153
|
+
this.render();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getMessagesFromSlots() {
|
|
157
|
+
const messages = this.querySelectorAll("chat-message");
|
|
158
|
+
return Array.from(messages).map((msg) => ({
|
|
159
|
+
name: msg.getAttribute("name") || "Anonymous",
|
|
160
|
+
avatar: msg.getAttribute("avatar") || "",
|
|
161
|
+
timestamp: msg.getAttribute("timestamp") || "",
|
|
162
|
+
isMe: msg.hasAttribute("is-me"),
|
|
163
|
+
content: msg.innerHTML.trim(),
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getMessagesFromAttribute() {
|
|
168
|
+
const data = this.getAttribute("messages");
|
|
169
|
+
if (!data) return [];
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(data);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.warn("Invalid messages JSON:", e);
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
render() {
|
|
179
|
+
const messages = this.getMessagesFromSlots();
|
|
180
|
+
|
|
181
|
+
const messagesHTML = messages.map((msg) => this.renderMessage(msg)).join("");
|
|
182
|
+
|
|
183
|
+
this.shadowRoot.innerHTML = `
|
|
184
|
+
<style>${CHAT_STYLES}</style>
|
|
185
|
+
<div class="chat-container">
|
|
186
|
+
${messagesHTML}
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
renderMessage(msg) {
|
|
192
|
+
const initial = msg.name.charAt(0).toUpperCase();
|
|
193
|
+
const avatarHTML = msg.avatar ? `<img src="${msg.avatar}" alt="${msg.name}" class="avatar" loading="lazy"/>` : `<div class="avatar-placeholder">${initial}</div>`;
|
|
194
|
+
|
|
195
|
+
return `
|
|
196
|
+
<div class="chat-message ${msg.isMe ? "is-me" : ""}">
|
|
197
|
+
${avatarHTML}
|
|
198
|
+
<div class="message-content">
|
|
199
|
+
<div class="message-header">
|
|
200
|
+
<span class="sender-name">${msg.name}</span>
|
|
201
|
+
<span class="timestamp">${msg.timestamp}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="message-bubble">${msg.content}</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static get observedAttributes() {
|
|
210
|
+
return ["messages"];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
214
|
+
if (name === "messages" && oldValue !== newValue) {
|
|
215
|
+
this.render();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Public API
|
|
220
|
+
addMessage(msg) {
|
|
221
|
+
const messages = this.getMessagesFromAttribute();
|
|
222
|
+
messages.push(msg);
|
|
223
|
+
this.setAttribute("messages", JSON.stringify(messages));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
clear() {
|
|
227
|
+
this.innerHTML = "";
|
|
228
|
+
this.render();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Placeholder for slot content
|
|
233
|
+
class ChatMessage extends HTMLElement {}
|
|
234
|
+
|
|
235
|
+
// Register custom elements
|
|
236
|
+
customElements.define("x-chat", Chat);
|
|
237
|
+
customElements.define("chat-message", ChatMessage);
|
|
238
|
+
|
|
239
|
+
export { Chat, ChatMessage };
|