hexo-theme-gnix 11.0.0 → 13.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.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Friends list custom elements.
3
+ *
4
+ * Usage:
5
+ * <friends-list id="friends">
6
+ * <friend-card
7
+ * name="Example"
8
+ * href="https://example.com"
9
+ * display-url="example.com"
10
+ * avatar="https://example.com/avatar.png"
11
+ * description="Personal site"
12
+ * feed="https://example.com/atom.xml"
13
+ * open-label="Open"
14
+ * ></friend-card>
15
+ * </friends-list>
16
+ */
17
+
18
+ let friendsListStyleSheetInjected = false;
19
+
20
+ function escapeHtml(value) {
21
+ const span = document.createElement("span");
22
+ span.textContent = value == null ? "" : String(value);
23
+ return span.innerHTML;
24
+ }
25
+
26
+ function escapeAttribute(value) {
27
+ return escapeHtml(value).replace(/"/g, "&quot;");
28
+ }
29
+
30
+ function displayUrlFromHref(href) {
31
+ try {
32
+ const url = new URL(href);
33
+ return url.hostname.replace(/^www\./, "www.");
34
+ } catch {
35
+ return href.replace(/^https?:\/\//, "").replace(/\/$/, "");
36
+ }
37
+ }
38
+
39
+ function injectFriendsListStyles() {
40
+ if (friendsListStyleSheetInjected) return;
41
+
42
+ const style = `
43
+ friends-list {
44
+ display: grid;
45
+ grid-template-columns: repeat(6, 1fr);
46
+ padding: 0;
47
+ }
48
+
49
+ friend-card {
50
+ position: relative;
51
+ grid-column: span 2;
52
+ padding: 1.5rem;
53
+ background: var(--base);
54
+ border: .5px solid var(--surface0);
55
+ cursor: pointer;
56
+ overflow: hidden;
57
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
58
+ min-height: 5.25rem;
59
+ display: flex;
60
+ flex-direction: column;
61
+ justify-content: center;
62
+ }
63
+
64
+ friend-card:focus-within,
65
+ friend-card:hover {
66
+ border-color: var(--lavender);
67
+ box-shadow: 0 0.75rem 2.5rem -0.75rem hsl(from var(--text) h s l / 0.1);
68
+ }
69
+
70
+ friend-card:nth-last-child(1):nth-child(3n+1) {
71
+ grid-column: span 6;
72
+ }
73
+
74
+ friend-card:nth-last-child(1):nth-child(3n+2),
75
+ friend-card:nth-last-child(2):nth-child(3n+1) {
76
+ grid-column: span 3;
77
+ }
78
+
79
+ friend-card::before {
80
+ content: '';
81
+ position: absolute;
82
+ inset: 0;
83
+ background:
84
+ radial-gradient(circle at 100% 100%, hsl(from var(--lavender) h s l / 0.14), transparent 55%);
85
+ opacity: 0;
86
+ transition: opacity 0.3s ease;
87
+ z-index: 0;
88
+ }
89
+
90
+ friend-card:focus-within::before,
91
+ friend-card:hover::before {
92
+ opacity: 1;
93
+ }
94
+
95
+ .friend-hit {
96
+ position: absolute;
97
+ inset: 0;
98
+ z-index: 2;
99
+ border-radius: inherit;
100
+ }
101
+
102
+ .friend-avatar {
103
+ position: absolute;
104
+ bottom: -0.5rem;
105
+ right: -0.5rem;
106
+ width: 6rem;
107
+ height: 6rem;
108
+ border-radius: 50%;
109
+ background-color: var(--surface0);
110
+ object-fit: cover;
111
+ opacity: 0.08;
112
+ filter: grayscale(0.5);
113
+ transform: rotate(-8deg);
114
+ transition: all 0.3s ease;
115
+ z-index: 0;
116
+ pointer-events: none;
117
+ border: none;
118
+ }
119
+
120
+ friend-card:focus-within .friend-avatar,
121
+ friend-card:hover .friend-avatar {
122
+ opacity: 0.15;
123
+ filter: grayscale(0);
124
+ transform: scale(1.1) rotate(-8deg);
125
+ }
126
+
127
+ .friend-detail {
128
+ position: relative;
129
+ z-index: 1;
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 0.25rem;
133
+ margin: 0;
134
+ }
135
+
136
+ h3.friend-name {
137
+ font-family: var(--font-serif);
138
+ font-style: italic;
139
+ font-synthesis: none;
140
+ font-size: 0.92rem;
141
+ font-weight: bolder;
142
+ color: var(--text);
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 0.5rem;
146
+ line-height: 1.3;
147
+ margin: 0;
148
+ }
149
+
150
+ .friend-name .rss-link {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ width: 1.25rem;
155
+ height: 1.25rem;
156
+ color: var(--subtext0);
157
+ text-decoration: none;
158
+ transition: all 0.2s ease;
159
+ border: 1px solid var(--surface0);
160
+ border-radius: 50%;
161
+ font-size: 0.75rem;
162
+ position: relative;
163
+ z-index: 3;
164
+ flex: 0 0 auto;
165
+ }
166
+
167
+ .friend-name .rss-link:hover {
168
+ color: var(--lavender);
169
+ border-color: var(--lavender);
170
+ }
171
+
172
+ .friend-url {
173
+ font-family: var(--font-mono);
174
+ font-size: 0.75rem;
175
+ font-weight: 400;
176
+ color: var(--subtext0);
177
+ opacity: 0.7;
178
+ transition: all 0.2s ease;
179
+ white-space: nowrap;
180
+ overflow: hidden;
181
+ text-overflow: ellipsis;
182
+ margin: 0;
183
+ }
184
+
185
+ friend-card:hover .friend-url {
186
+ opacity: 1;
187
+ color: var(--lavender);
188
+ }
189
+
190
+ .friend-desc {
191
+ font-family: var(--font-sans-serif);
192
+ font-size: 0.8rem;
193
+ color: var(--subtext1);
194
+ margin: 0.5rem 0 0;
195
+ padding: 0;
196
+ overflow: hidden;
197
+ }
198
+
199
+ @media (max-width: 768px) {
200
+ friends-list {
201
+ grid-template-columns: 1fr;
202
+ }
203
+
204
+ friend-card,
205
+ friend-card:nth-last-child(1):nth-child(3n+1),
206
+ friend-card:nth-last-child(1):nth-child(3n+2),
207
+ friend-card:nth-last-child(2):nth-child(3n+1) {
208
+ grid-column: span 1;
209
+ }
210
+
211
+ friend-card:nth-child(1) {
212
+ min-height: 6.25rem;
213
+ }
214
+ }
215
+
216
+ @media (max-width: 480px) {
217
+ friend-card {
218
+ padding: 1.1rem;
219
+ }
220
+ }
221
+ `;
222
+
223
+ const styleEl = document.createElement("style");
224
+ styleEl.textContent = style;
225
+ document.head.appendChild(styleEl);
226
+ friendsListStyleSheetInjected = true;
227
+ }
228
+
229
+ class FriendsList extends HTMLElement {
230
+ connectedCallback() {
231
+ injectFriendsListStyles();
232
+ if (!this.hasAttribute("role")) this.setAttribute("role", "list");
233
+ }
234
+ }
235
+
236
+ class FriendCard extends HTMLElement {
237
+ connectedCallback() {
238
+ injectFriendsListStyles();
239
+ this.render();
240
+ }
241
+
242
+ render() {
243
+ const name = this.getAttribute("name") || "";
244
+ const href = this.getAttribute("href") || "";
245
+ const displayUrl = this.getAttribute("display-url") || displayUrlFromHref(href);
246
+ const avatar = this.getAttribute("avatar") || "";
247
+ const description = this.getAttribute("description") || "";
248
+ const feed = this.getAttribute("feed") || "";
249
+ const openLabel = this.getAttribute("open-label") || "Open";
250
+ const ariaLabel = href && name ? `${openLabel} ${name}` : "";
251
+
252
+ this.setAttribute("role", "listitem");
253
+ this.innerHTML = `
254
+ ${href ? `<a class="friend-hit" href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer" aria-label="${escapeAttribute(ariaLabel)}"></a>` : ""}
255
+ ${avatar ? `<img class="friend-avatar" src="${escapeAttribute(avatar)}" alt="" width="96" height="96" loading="lazy" decoding="async">` : ""}
256
+ <article class="friend-detail">
257
+ <h3 class="friend-name">
258
+ ${escapeHtml(name)}
259
+ ${feed ? `<a class="rss-link" target="_blank" rel="noopener noreferrer" href="${escapeAttribute(feed)}" title="RSS Feed">◎</a>` : ""}
260
+ </h3>
261
+ ${displayUrl ? `<div class="friend-url">${escapeHtml(displayUrl)}</div>` : ""}
262
+ ${description ? `<div class="friend-desc">${escapeHtml(description)}</div>` : ""}
263
+ </article>
264
+ `;
265
+ }
266
+ }
267
+
268
+ if (!customElements.get("friends-list")) customElements.define("friends-list", FriendsList);
269
+ if (!customElements.get("friend-card")) customElements.define("friend-card", FriendCard);
270
+
271
+ export { FriendCard, FriendsList };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Tabs Custom Element
3
+ *
4
+ * Usage:
5
+ * <x-tabs>
6
+ * <x-tab title="JavaScript" sync-id="js" active>
7
+ * Content here...
8
+ * </x-tab>
9
+ * <x-tab title="CSS" sync-id="css">
10
+ * More content...
11
+ * </x-tab>
12
+ * </x-tabs>
13
+ */
14
+
15
+ let styleSheetInjected = false;
16
+ let tabsCounter = 0;
17
+
18
+ function escapeHtml(value) {
19
+ const span = document.createElement("span");
20
+ span.textContent = value;
21
+ return span.innerHTML;
22
+ }
23
+
24
+ class Tabs extends HTMLElement {
25
+ connectedCallback() {
26
+ this.injectStyles();
27
+ this.render();
28
+ if (!this.hasAttribute("data-initialized")) {
29
+ this.setupListeners();
30
+ this.setAttribute("data-initialized", "true");
31
+ }
32
+ }
33
+
34
+ injectStyles() {
35
+ if (styleSheetInjected) return;
36
+
37
+ const style = `
38
+ x-tabs {
39
+ display: block;
40
+ overflow: hidden;
41
+ margin: var(--tabs-margin, 10px auto);
42
+ }
43
+
44
+ .x-tabs-header {
45
+ display: flex;
46
+ gap: var(--tabs-header-gap, 0);
47
+ padding: var(--tabs-header-padding, 0 0.5rem);
48
+ white-space: nowrap;
49
+ overflow-x: auto;
50
+ overflow-y: hidden;
51
+ }
52
+
53
+ .x-tabs-tab {
54
+ flex: 0 0 auto;
55
+ padding: var(--tabs-tab-padding, 0.5em 1rem);
56
+ border: none;
57
+ border-bottom: var(--tabs-tab-border-width, 2px) solid transparent;
58
+ background: transparent;
59
+ color: var(--tabs-tab-color, var(--subtext0));
60
+ font: inherit;
61
+ cursor: pointer;
62
+ position: relative;
63
+ transition:
64
+ color 0.3s ease,
65
+ border-color 0.3s ease;
66
+ outline: 0;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .x-tabs-tab:hover,
71
+ .x-tabs-tab[aria-selected="true"] {
72
+ color: var(--tabs-tab-active-color, var(--text));
73
+ border-color: var(--tabs-tab-active-border-color, var(--text));
74
+ }
75
+
76
+ .x-tabs-tab:focus-visible {
77
+ outline: 2px solid var(--tabs-focus-color, var(--text));
78
+ outline-offset: 2px;
79
+ }
80
+
81
+ .x-tabs-panels {
82
+ padding: var(--tabs-panel-padding, 0.8em 0 10px 0);
83
+ }
84
+
85
+ .x-tabs-panel[hidden] {
86
+ display: none;
87
+ }
88
+ `;
89
+
90
+ const styleEl = document.createElement("style");
91
+ styleEl.textContent = style;
92
+ document.head.appendChild(styleEl);
93
+ styleSheetInjected = true;
94
+ }
95
+
96
+ render() {
97
+ const tabs = Array.from(this.children).filter((child) => child.tagName.toLowerCase() === "x-tab");
98
+ if (tabs.length === 0) return;
99
+
100
+ const instanceId = tabsCounter++;
101
+ const activeIndex = Math.max(
102
+ 0,
103
+ tabs.findIndex((tab) => tab.hasAttribute("active")),
104
+ );
105
+
106
+ const headers = tabs
107
+ .map((tab, index) => {
108
+ const title = tab.getAttribute("title") || `Tab ${index + 1}`;
109
+ const syncId = tab.getAttribute("sync-id") || "";
110
+ const selected = index === activeIndex;
111
+
112
+ return `
113
+ <button
114
+ class="x-tabs-tab"
115
+ type="button"
116
+ role="tab"
117
+ id="x-tabs-${instanceId}-tab-${index}"
118
+ aria-controls="x-tabs-${instanceId}-panel-${index}"
119
+ aria-selected="${selected}"
120
+ tabindex="${selected ? "0" : "-1"}"
121
+ data-index="${index}"
122
+ ${syncId ? `data-sync-id="${escapeHtml(syncId)}"` : ""}
123
+ >${escapeHtml(title)}</button>
124
+ `;
125
+ })
126
+ .join("");
127
+
128
+ const panels = tabs
129
+ .map((tab, index) => {
130
+ const content = Array.from(tab.childNodes)
131
+ .map((node) => (node.nodeType === Node.TEXT_NODE ? node.textContent : node.outerHTML))
132
+ .join("");
133
+ const selected = index === activeIndex;
134
+
135
+ return `
136
+ <div
137
+ class="x-tabs-panel content"
138
+ role="tabpanel"
139
+ id="x-tabs-${instanceId}-panel-${index}"
140
+ aria-labelledby="x-tabs-${instanceId}-tab-${index}"
141
+ data-index="${index}"
142
+ ${selected ? "" : "hidden"}
143
+ >${content}</div>
144
+ `;
145
+ })
146
+ .join("");
147
+
148
+ this.innerHTML = `
149
+ <div class="x-tabs-header" role="tablist">
150
+ ${headers}
151
+ </div>
152
+ <div class="x-tabs-panels">
153
+ ${panels}
154
+ </div>
155
+ `;
156
+ }
157
+
158
+ setupListeners() {
159
+ this.addEventListener("click", (event) => {
160
+ const target = event.target instanceof Element ? event.target : event.target.parentElement;
161
+ const button = target?.closest(".x-tabs-tab");
162
+ if (!button || !this.contains(button)) return;
163
+
164
+ this.activateTab(Number(button.dataset.index));
165
+ this.syncRelatedTabs(button.dataset.syncId);
166
+ });
167
+
168
+ this.addEventListener("keydown", (event) => {
169
+ const target = event.target instanceof Element ? event.target : event.target.parentElement;
170
+ const button = target?.closest(".x-tabs-tab");
171
+ if (!button || !this.contains(button)) return;
172
+
173
+ const buttons = this.tabButtons;
174
+ const currentIndex = buttons.indexOf(button);
175
+ let nextIndex = currentIndex;
176
+
177
+ switch (event.key) {
178
+ case "ArrowLeft":
179
+ case "ArrowUp":
180
+ nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;
181
+ break;
182
+ case "ArrowRight":
183
+ case "ArrowDown":
184
+ nextIndex = (currentIndex + 1) % buttons.length;
185
+ break;
186
+ case "Home":
187
+ nextIndex = 0;
188
+ break;
189
+ case "End":
190
+ nextIndex = buttons.length - 1;
191
+ break;
192
+ default:
193
+ return;
194
+ }
195
+
196
+ event.preventDefault();
197
+ buttons[nextIndex]?.focus();
198
+ this.activateTab(nextIndex);
199
+ this.syncRelatedTabs(buttons[nextIndex]?.dataset.syncId);
200
+ });
201
+ }
202
+
203
+ get tabButtons() {
204
+ return Array.from(this.querySelectorAll(".x-tabs-tab"));
205
+ }
206
+
207
+ activateTab(index) {
208
+ if (!Number.isInteger(index)) return;
209
+
210
+ const buttons = this.tabButtons;
211
+ const panels = Array.from(this.querySelectorAll(".x-tabs-panel"));
212
+
213
+ buttons.forEach((button, buttonIndex) => {
214
+ const selected = buttonIndex === index;
215
+ button.setAttribute("aria-selected", String(selected));
216
+ button.tabIndex = selected ? 0 : -1;
217
+ });
218
+
219
+ panels.forEach((panel, panelIndex) => {
220
+ panel.hidden = panelIndex !== index;
221
+ });
222
+ }
223
+
224
+ syncRelatedTabs(syncId) {
225
+ if (!syncId) return;
226
+
227
+ document.querySelectorAll(".x-tabs-tab[data-sync-id]").forEach((button) => {
228
+ if (button.dataset.syncId !== syncId) return;
229
+
230
+ const tabs = button.closest("x-tabs");
231
+ if (!tabs || tabs === this) return;
232
+ tabs.activateTab(Number(button.dataset.index));
233
+ });
234
+ }
235
+ }
236
+
237
+ class Tab extends HTMLElement {}
238
+
239
+ if (!customElements.get("x-tabs")) customElements.define("x-tabs", Tabs);
240
+ if (!customElements.get("x-tab")) customElements.define("x-tab", Tab);
241
+
242
+ export { Tab, Tabs };