@anglefeint/astro-theme 0.1.38 → 0.1.40
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/package.json +1 -1
- package/src/components/Giscus.astro +48 -0
- package/src/content-schema.ts +51 -26
- package/src/i18n/messages.ts +18 -0
- package/src/layouts/BlogPost.astro +24 -24
- package/src/scripts/about/modals.js +36 -28
- package/src/scripts/blogpost/interactions.js +47 -29
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
type GiscusComments = {
|
|
3
|
+
REPO: string;
|
|
4
|
+
REPO_ID: string;
|
|
5
|
+
CATEGORY: string;
|
|
6
|
+
CATEGORY_ID: string;
|
|
7
|
+
MAPPING: string;
|
|
8
|
+
TERM: string;
|
|
9
|
+
NUMBER: string;
|
|
10
|
+
STRICT: string;
|
|
11
|
+
REACTIONS_ENABLED: string;
|
|
12
|
+
EMIT_METADATA: string;
|
|
13
|
+
INPUT_POSITION: string;
|
|
14
|
+
THEME: string;
|
|
15
|
+
LANG: string;
|
|
16
|
+
LOADING: string;
|
|
17
|
+
CROSSORIGIN: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
comments: GiscusComments;
|
|
22
|
+
resolvedLocale: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const { comments, resolvedLocale } = Astro.props;
|
|
26
|
+
const normalizedCommentTerm = comments.TERM.trim();
|
|
27
|
+
const normalizedCommentNumber = comments.NUMBER.trim();
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<script
|
|
31
|
+
is:inline
|
|
32
|
+
src="https://giscus.app/client.js"
|
|
33
|
+
data-repo={comments.REPO}
|
|
34
|
+
data-repo-id={comments.REPO_ID}
|
|
35
|
+
data-category={comments.CATEGORY}
|
|
36
|
+
data-category-id={comments.CATEGORY_ID}
|
|
37
|
+
data-mapping={comments.MAPPING}
|
|
38
|
+
data-term={comments.MAPPING === 'specific' ? normalizedCommentTerm : undefined}
|
|
39
|
+
data-number={comments.MAPPING === 'number' ? normalizedCommentNumber : undefined}
|
|
40
|
+
data-strict={comments.STRICT}
|
|
41
|
+
data-reactions-enabled={comments.REACTIONS_ENABLED}
|
|
42
|
+
data-emit-metadata={comments.EMIT_METADATA}
|
|
43
|
+
data-input-position={comments.INPUT_POSITION}
|
|
44
|
+
data-theme={comments.THEME}
|
|
45
|
+
data-lang={comments.LANG || resolvedLocale}
|
|
46
|
+
data-loading={comments.LOADING}
|
|
47
|
+
crossorigin={comments.CROSSORIGIN}
|
|
48
|
+
async></script>
|
package/src/content-schema.ts
CHANGED
|
@@ -2,33 +2,58 @@ import { defineCollection } from 'astro:content';
|
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
3
|
import { z } from 'astro/zod';
|
|
4
4
|
|
|
5
|
+
const ABSOLUTE_URL_SCHEME_REGEX = /^[a-z][a-z\d+.-]*:/i;
|
|
6
|
+
|
|
7
|
+
function normalizeSourceLink(value: string): string {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return ABSOLUTE_URL_SCHEME_REGEX.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isValidSourceLink(value: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sourceLinkSchema = z
|
|
22
|
+
.string()
|
|
23
|
+
.trim()
|
|
24
|
+
.min(1)
|
|
25
|
+
.transform(normalizeSourceLink)
|
|
26
|
+
.refine(isValidSourceLink, {
|
|
27
|
+
message: 'sourceLinks entries must be valid HTTP(S) URLs or bare domains.',
|
|
28
|
+
});
|
|
29
|
+
|
|
5
30
|
const blog = defineCollection({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
|
32
|
+
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
|
|
33
|
+
// Type-check frontmatter using a schema
|
|
34
|
+
schema: ({ image }) =>
|
|
35
|
+
z.object({
|
|
36
|
+
title: z.string(),
|
|
37
|
+
subtitle: z.string().optional(),
|
|
38
|
+
description: z.string(),
|
|
39
|
+
// Transform string to Date object
|
|
40
|
+
pubDate: z.coerce.date(),
|
|
41
|
+
updatedDate: z.coerce.date().optional(),
|
|
42
|
+
heroImage: image().optional(),
|
|
43
|
+
context: z.string().optional(),
|
|
44
|
+
readMinutes: z.number().int().positive().optional(),
|
|
45
|
+
aiModel: z.string().optional(),
|
|
46
|
+
aiMode: z.string().optional(),
|
|
47
|
+
aiState: z.string().optional(),
|
|
48
|
+
aiLatencyMs: z.number().int().nonnegative().optional(),
|
|
49
|
+
aiConfidence: z.number().min(0).max(1).optional(),
|
|
50
|
+
wordCount: z.number().int().nonnegative().optional(),
|
|
51
|
+
tokenCount: z.number().int().nonnegative().optional(),
|
|
52
|
+
author: z.string().optional(),
|
|
53
|
+
tags: z.array(z.string()).optional(),
|
|
54
|
+
canonicalTopic: z.string().optional(),
|
|
55
|
+
sourceLinks: z.array(sourceLinkSchema).optional(),
|
|
56
|
+
}),
|
|
32
57
|
});
|
|
33
58
|
|
|
34
59
|
export const collections = { blog };
|
package/src/i18n/messages.ts
CHANGED
|
@@ -48,6 +48,9 @@ export type Messages = {
|
|
|
48
48
|
metaUpdated: string;
|
|
49
49
|
metaReadMinutes: string;
|
|
50
50
|
systemStatusAria: string;
|
|
51
|
+
systemModelLabel: string;
|
|
52
|
+
systemModeLabel: string;
|
|
53
|
+
systemStateLabel: string;
|
|
51
54
|
promptContextLabel: string;
|
|
52
55
|
latencyLabel: string;
|
|
53
56
|
confidenceLabel: string;
|
|
@@ -116,6 +119,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
|
|
|
116
119
|
metaUpdated: 'updated',
|
|
117
120
|
metaReadMinutes: 'min read',
|
|
118
121
|
systemStatusAria: 'Model status',
|
|
122
|
+
systemModelLabel: 'model',
|
|
123
|
+
systemModeLabel: 'mode',
|
|
124
|
+
systemStateLabel: 'state',
|
|
119
125
|
promptContextLabel: 'Context',
|
|
120
126
|
latencyLabel: 'latency est',
|
|
121
127
|
confidenceLabel: 'confidence',
|
|
@@ -182,6 +188,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
|
|
|
182
188
|
metaUpdated: '更新',
|
|
183
189
|
metaReadMinutes: '分で読了',
|
|
184
190
|
systemStatusAria: 'モデル状態',
|
|
191
|
+
systemModelLabel: 'モデル',
|
|
192
|
+
systemModeLabel: 'モード',
|
|
193
|
+
systemStateLabel: '状態',
|
|
185
194
|
promptContextLabel: 'コンテキスト',
|
|
186
195
|
latencyLabel: '推定レイテンシ',
|
|
187
196
|
confidenceLabel: '信頼度',
|
|
@@ -248,6 +257,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
|
|
|
248
257
|
metaUpdated: '수정',
|
|
249
258
|
metaReadMinutes: '분 읽기',
|
|
250
259
|
systemStatusAria: '모델 상태',
|
|
260
|
+
systemModelLabel: '모델',
|
|
261
|
+
systemModeLabel: '모드',
|
|
262
|
+
systemStateLabel: '상태',
|
|
251
263
|
promptContextLabel: '컨텍스트',
|
|
252
264
|
latencyLabel: '지연 추정',
|
|
253
265
|
confidenceLabel: '신뢰도',
|
|
@@ -315,6 +327,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
|
|
|
315
327
|
metaUpdated: 'actualizado',
|
|
316
328
|
metaReadMinutes: 'min de lectura',
|
|
317
329
|
systemStatusAria: 'Estado del modelo',
|
|
330
|
+
systemModelLabel: 'modelo',
|
|
331
|
+
systemModeLabel: 'modo',
|
|
332
|
+
systemStateLabel: 'estado',
|
|
318
333
|
promptContextLabel: 'Contexto',
|
|
319
334
|
latencyLabel: 'latencia est',
|
|
320
335
|
confidenceLabel: 'confianza',
|
|
@@ -381,6 +396,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
|
|
|
381
396
|
metaUpdated: '更新',
|
|
382
397
|
metaReadMinutes: '分钟阅读',
|
|
383
398
|
systemStatusAria: '模型状态',
|
|
399
|
+
systemModelLabel: '模型',
|
|
400
|
+
systemModeLabel: '模式',
|
|
401
|
+
systemStateLabel: '状态',
|
|
384
402
|
promptContextLabel: '语境',
|
|
385
403
|
latencyLabel: '延迟估计',
|
|
386
404
|
confidenceLabel: '置信度',
|
|
@@ -4,6 +4,7 @@ import type { CollectionEntry } from 'astro:content';
|
|
|
4
4
|
import themeRedqueen1 from '../assets/theme/red-queen/theme-redqueen1.webp';
|
|
5
5
|
import themeRedqueen2 from '../assets/theme/red-queen/theme-redqueen2.gif';
|
|
6
6
|
import FormattedDate from '../components/FormattedDate.astro';
|
|
7
|
+
import Giscus from '../components/Giscus.astro';
|
|
7
8
|
import AiShell from './shells/AiShell.astro';
|
|
8
9
|
import blogPostCssUrl from '../styles/blog-post.css?url';
|
|
9
10
|
import { SITE_AUTHOR } from '@anglefeint/site-config/site';
|
|
@@ -58,11 +59,16 @@ const hasStats = aiModel || wordCount !== undefined || tokenCount !== undefined;
|
|
|
58
59
|
const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : undefined;
|
|
59
60
|
const enableRedQueen = THEME.EFFECTS.ENABLE_RED_QUEEN;
|
|
60
61
|
const comments = THEME.COMMENTS;
|
|
62
|
+
const normalizedCommentTerm = comments.TERM.trim();
|
|
63
|
+
const normalizedCommentNumber = comments.NUMBER.trim();
|
|
64
|
+
const hasValidCommentNumber =
|
|
65
|
+
/^[1-9]\d*$/.test(normalizedCommentNumber) &&
|
|
66
|
+
Number.isSafeInteger(Number(normalizedCommentNumber));
|
|
61
67
|
const hasMappingParam =
|
|
62
68
|
comments.MAPPING === 'specific'
|
|
63
|
-
? Boolean(
|
|
69
|
+
? Boolean(normalizedCommentTerm)
|
|
64
70
|
: comments.MAPPING === 'number'
|
|
65
|
-
?
|
|
71
|
+
? hasValidCommentNumber
|
|
66
72
|
: true;
|
|
67
73
|
const hasCommentsConfig = Boolean(
|
|
68
74
|
comments.ENABLED &&
|
|
@@ -211,9 +217,21 @@ const hasCommentsConfig = Boolean(
|
|
|
211
217
|
{
|
|
212
218
|
hasSystemMeta && (
|
|
213
219
|
<div class="ai-system-row" aria-label={messages.blog.systemStatusAria}>
|
|
214
|
-
{aiModel &&
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
{aiModel && (
|
|
221
|
+
<span class="ai-system-chip">
|
|
222
|
+
{messages.blog.systemModelLabel}: {aiModel}
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
{aiMode && (
|
|
226
|
+
<span class="ai-system-chip">
|
|
227
|
+
{messages.blog.systemModeLabel}: {aiMode}
|
|
228
|
+
</span>
|
|
229
|
+
)}
|
|
230
|
+
{aiState && (
|
|
231
|
+
<span class="ai-system-chip">
|
|
232
|
+
{messages.blog.systemStateLabel}: {aiState}
|
|
233
|
+
</span>
|
|
234
|
+
)}
|
|
217
235
|
</div>
|
|
218
236
|
)
|
|
219
237
|
}
|
|
@@ -318,25 +336,7 @@ const hasCommentsConfig = Boolean(
|
|
|
318
336
|
{
|
|
319
337
|
hasCommentsConfig && (
|
|
320
338
|
<section class="prose ai-comments" aria-label={messages.blog.comments}>
|
|
321
|
-
<
|
|
322
|
-
src="https://giscus.app/client.js"
|
|
323
|
-
data-repo={comments.REPO}
|
|
324
|
-
data-repo-id={comments.REPO_ID}
|
|
325
|
-
data-category={comments.CATEGORY}
|
|
326
|
-
data-category-id={comments.CATEGORY_ID}
|
|
327
|
-
data-mapping={comments.MAPPING}
|
|
328
|
-
data-term={comments.MAPPING === 'specific' ? comments.TERM : undefined}
|
|
329
|
-
data-number={comments.MAPPING === 'number' ? comments.NUMBER : undefined}
|
|
330
|
-
data-strict={comments.STRICT}
|
|
331
|
-
data-reactions-enabled={comments.REACTIONS_ENABLED}
|
|
332
|
-
data-emit-metadata={comments.EMIT_METADATA}
|
|
333
|
-
data-input-position={comments.INPUT_POSITION}
|
|
334
|
-
data-theme={comments.THEME}
|
|
335
|
-
data-lang={comments.LANG || resolvedLocale}
|
|
336
|
-
data-loading={comments.LOADING}
|
|
337
|
-
crossorigin={comments.CROSSORIGIN}
|
|
338
|
-
async
|
|
339
|
-
/>
|
|
339
|
+
<Giscus comments={comments} resolvedLocale={resolvedLocale} />
|
|
340
340
|
</section>
|
|
341
341
|
)
|
|
342
342
|
}
|
|
@@ -2,13 +2,18 @@ import { createDecryptorController } from './modal-decryptor.js';
|
|
|
2
2
|
import { mountHelpKeyboard } from './modal-keyboard.js';
|
|
3
3
|
import { renderProgressModal } from './modal-progress.js';
|
|
4
4
|
|
|
5
|
+
let cleanupAboutModals = null;
|
|
6
|
+
|
|
5
7
|
export function initAboutModals(runtimeConfig, prefersReducedMotion) {
|
|
8
|
+
if (cleanupAboutModals) cleanupAboutModals();
|
|
9
|
+
|
|
6
10
|
const modalOverlay = document.getElementById('hacker-modal');
|
|
7
11
|
const modalBody = document.getElementById('hacker-modal-body');
|
|
8
12
|
const modalTitle = document.querySelector('.hacker-modal-title');
|
|
9
13
|
if (!modalOverlay || !modalBody || !modalTitle) return;
|
|
10
14
|
|
|
11
|
-
const decryptorKeysLabel =
|
|
15
|
+
const decryptorKeysLabel =
|
|
16
|
+
typeof runtimeConfig.decryptorKeysLabel === 'string' ? runtimeConfig.decryptorKeysLabel : '';
|
|
12
17
|
const decryptor = createDecryptorController(
|
|
13
18
|
modalOverlay,
|
|
14
19
|
prefersReducedMotion,
|
|
@@ -17,26 +22,8 @@ export function initAboutModals(runtimeConfig, prefersReducedMotion) {
|
|
|
17
22
|
let cleanupKeyboard = null;
|
|
18
23
|
|
|
19
24
|
const scriptsTpl = document.getElementById('hacker-scripts-folders-tpl');
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
title: 'Downloading...',
|
|
23
|
-
body: '<div class="hacker-modal-download"><div class="modal-subtitle">Critical Data</div><div class="hacker-modal-progress" id="dl-progress"></div></div>',
|
|
24
|
-
type: 'progress',
|
|
25
|
-
},
|
|
26
|
-
ai: {
|
|
27
|
-
title: 'AI',
|
|
28
|
-
body: '<pre>~ $ ai --status --verbose\n\nmodel: runtime-default\nmode: standard\ncontext window: 32k\nlatency: 100-250ms\nsafety: enabled\n\n>> system online\n>> ready</pre>',
|
|
29
|
-
type: 'plain',
|
|
30
|
-
},
|
|
31
|
-
decryptor: {
|
|
32
|
-
title: 'Password Decryptor',
|
|
33
|
-
body: '<pre class="hacker-decryptor-pre">Calculating Hashes\n\n<span id="dec-keys">[00:00:01] 0 keys tested</span>\n\nCurrent passphrase: <span id="dec-pass">********</span>\n\nMaster key\n<span id="dec-master1"></span>\n<span id="dec-master2"></span>\n\nTransient key\n<span id="dec-trans1"></span>\n<span id="dec-trans2"></span>\n<span id="dec-trans3"></span>\n<span id="dec-trans4"></span></pre>',
|
|
34
|
-
type: 'decryptor',
|
|
35
|
-
},
|
|
36
|
-
help: { title: 'Help', body: '', type: 'keyboard' },
|
|
37
|
-
'all-scripts': { title: '/root/bash/scripts', body: '', type: 'scripts' },
|
|
38
|
-
};
|
|
39
|
-
const modalContent = runtimeConfig.modalContent || fallbackModalContent;
|
|
25
|
+
const modalContent = runtimeConfig.modalContent;
|
|
26
|
+
if (!modalContent || typeof modalContent !== 'object') return;
|
|
40
27
|
|
|
41
28
|
const closeModal = () => {
|
|
42
29
|
decryptor.stop();
|
|
@@ -99,22 +86,43 @@ export function initAboutModals(runtimeConfig, prefersReducedMotion) {
|
|
|
99
86
|
modalOverlay.setAttribute('aria-hidden', 'false');
|
|
100
87
|
};
|
|
101
88
|
|
|
102
|
-
document.querySelectorAll('.hacker-folder[data-modal]')
|
|
103
|
-
|
|
89
|
+
const folderButtons = Array.from(document.querySelectorAll('.hacker-folder[data-modal]'));
|
|
90
|
+
const buttonHandlers = folderButtons.map((button) => {
|
|
91
|
+
const onClick = () => {
|
|
104
92
|
const id = button.getAttribute('data-modal');
|
|
105
93
|
if (!id) return;
|
|
106
94
|
openModal(modalContent[id]);
|
|
107
|
-
}
|
|
95
|
+
};
|
|
96
|
+
button.addEventListener('click', onClick);
|
|
97
|
+
return [button, onClick];
|
|
108
98
|
});
|
|
109
99
|
|
|
110
100
|
const closeButton = document.querySelector('.hacker-modal-close');
|
|
111
101
|
if (closeButton) closeButton.addEventListener('click', closeModal);
|
|
112
102
|
|
|
113
|
-
|
|
103
|
+
const onOverlayClick = (event) => {
|
|
114
104
|
if (event.target === modalOverlay) closeModal();
|
|
115
|
-
}
|
|
105
|
+
};
|
|
106
|
+
modalOverlay.addEventListener('click', onOverlayClick);
|
|
116
107
|
|
|
117
|
-
|
|
108
|
+
const onDocumentKeydown = (event) => {
|
|
118
109
|
if (event.key === 'Escape' && modalOverlay.classList.contains('open')) closeModal();
|
|
119
|
-
}
|
|
110
|
+
};
|
|
111
|
+
document.addEventListener('keydown', onDocumentKeydown);
|
|
112
|
+
|
|
113
|
+
cleanupAboutModals = () => {
|
|
114
|
+
decryptor.stop();
|
|
115
|
+
if (cleanupKeyboard) {
|
|
116
|
+
cleanupKeyboard();
|
|
117
|
+
cleanupKeyboard = null;
|
|
118
|
+
}
|
|
119
|
+
if (closeButton) closeButton.removeEventListener('click', closeModal);
|
|
120
|
+
modalOverlay.removeEventListener('click', onOverlayClick);
|
|
121
|
+
document.removeEventListener('keydown', onDocumentKeydown);
|
|
122
|
+
buttonHandlers.forEach(([button, handler]) => {
|
|
123
|
+
button.removeEventListener('click', handler);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return cleanupAboutModals;
|
|
120
128
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export function initPostInteractions(prefersReducedMotion) {
|
|
2
|
-
|
|
3
|
-
if (glow) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
const glow = document.querySelector('.ai-mouse-glow');
|
|
3
|
+
if (glow && window.__anglefeintPostGlowBound__ !== true) {
|
|
4
|
+
window.__anglefeintPostGlowBound__ = true;
|
|
5
|
+
let raf = 0;
|
|
6
|
+
let x = 0;
|
|
7
|
+
let y = 0;
|
|
8
|
+
document.addEventListener('mousemove', function (e) {
|
|
8
9
|
x = e.clientX;
|
|
9
10
|
y = e.clientY;
|
|
10
11
|
if (!raf) {
|
|
11
|
-
raf = requestAnimationFrame(function() {
|
|
12
|
+
raf = requestAnimationFrame(function () {
|
|
12
13
|
glow.style.setProperty('--mouse-x', x + 'px');
|
|
13
14
|
glow.style.setProperty('--mouse-y', y + 'px');
|
|
14
15
|
raf = 0;
|
|
@@ -17,42 +18,56 @@ export function initPostInteractions(prefersReducedMotion) {
|
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
document.querySelectorAll('.ai-prose-body a[href]').forEach(function(a) {
|
|
21
|
+
document.querySelectorAll('.ai-prose-body a[href]').forEach(function (a) {
|
|
22
|
+
if (a.dataset.aiLinkPreviewBound === 'true') return;
|
|
21
23
|
var href = a.getAttribute('href') || '';
|
|
22
24
|
if (!href || href.startsWith('#')) return;
|
|
25
|
+
a.dataset.aiLinkPreviewBound = 'true';
|
|
23
26
|
a.classList.add('ai-link-preview');
|
|
24
27
|
try {
|
|
25
|
-
a.setAttribute(
|
|
28
|
+
a.setAttribute(
|
|
29
|
+
'data-preview',
|
|
30
|
+
href.startsWith('http') ? new URL(href, location.origin).hostname : href
|
|
31
|
+
);
|
|
26
32
|
} catch (_err) {
|
|
27
33
|
a.setAttribute('data-preview', href);
|
|
28
34
|
}
|
|
29
35
|
});
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
const paras = document.querySelectorAll(
|
|
38
|
+
'.ai-prose-body p, .ai-prose-body h2, .ai-prose-body h3, .ai-prose-body pre, .ai-prose-body blockquote, .ai-prose-body ul, .ai-prose-body ol'
|
|
39
|
+
);
|
|
32
40
|
if (window.IntersectionObserver) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
entry.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const io = new IntersectionObserver(
|
|
42
|
+
function (entries) {
|
|
43
|
+
entries.forEach(function (entry) {
|
|
44
|
+
if (entry.isIntersecting) {
|
|
45
|
+
entry.target.classList.add('ai-para-visible');
|
|
46
|
+
io.unobserve(entry.target);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
{ rootMargin: '0px 0px -60px 0px', threshold: 0.1 }
|
|
51
|
+
);
|
|
41
52
|
|
|
42
|
-
paras.forEach(function(p) {
|
|
53
|
+
paras.forEach(function (p) {
|
|
54
|
+
if (p.dataset.aiRevealObserved === 'true') return;
|
|
55
|
+
p.dataset.aiRevealObserved = 'true';
|
|
43
56
|
io.observe(p);
|
|
44
57
|
});
|
|
45
58
|
} else {
|
|
46
|
-
paras.forEach(function(p) {
|
|
59
|
+
paras.forEach(function (p) {
|
|
47
60
|
p.classList.add('ai-para-visible');
|
|
48
61
|
});
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
const regen = document.querySelector('.ai-regenerate');
|
|
65
|
+
const article = document.querySelector('.ai-article');
|
|
66
|
+
const scan = document.querySelector('.ai-load-scan');
|
|
54
67
|
if (regen && article) {
|
|
55
|
-
regen.
|
|
68
|
+
if (regen.dataset.aiRegenerateBound === 'true') return;
|
|
69
|
+
regen.dataset.aiRegenerateBound = 'true';
|
|
70
|
+
regen.addEventListener('click', function () {
|
|
56
71
|
regen.disabled = true;
|
|
57
72
|
regen.classList.add('ai-regenerating');
|
|
58
73
|
article.classList.add('ai-regenerate-flash');
|
|
@@ -63,11 +78,14 @@ export function initPostInteractions(prefersReducedMotion) {
|
|
|
63
78
|
scan.style.top = '0';
|
|
64
79
|
scan.style.opacity = '1';
|
|
65
80
|
}
|
|
66
|
-
setTimeout(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
setTimeout(
|
|
82
|
+
function () {
|
|
83
|
+
article.classList.remove('ai-regenerate-flash');
|
|
84
|
+
regen.classList.remove('ai-regenerating');
|
|
85
|
+
regen.disabled = false;
|
|
86
|
+
},
|
|
87
|
+
prefersReducedMotion ? 120 : 1200
|
|
88
|
+
);
|
|
71
89
|
});
|
|
72
90
|
}
|
|
73
91
|
}
|