@anglefeint/astro-theme 0.1.36 → 0.1.38

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 CHANGED
@@ -54,7 +54,7 @@ This package reads site-specific config from alias imports:
54
54
 
55
55
  In the starter/site project, map these aliases to `src/config/*` and `src/i18n/*` in both Vite and TS config.
56
56
 
57
- Giscus comments are configured from site-side `theme.comments` (enabled + repo/category IDs). If required fields are not set, comments are not rendered.
57
+ Giscus comments are configured from site-side `theme.comments` (core IDs + behavior fields like `mapping`, `inputPosition`, `theme`, and `lang`). If required core fields are not set, comments are not rendered. When `mapping="specific"` set `term`; when `mapping="number"` set `number`.
58
58
 
59
59
  ## CLI
60
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anglefeint/astro-theme",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "description": "Anglefeint core theme package for Astro",
6
6
  "keywords": [
@@ -6,242 +6,248 @@ import HeaderLink from '../HeaderLink.astro';
6
6
  import LangSwitcher from './LangSwitcher.astro';
7
7
  import SocialMenu from './SocialMenu.astro';
8
8
  import {
9
- DEFAULT_LOCALE,
10
- LOCALE_LABELS,
11
- type Locale,
12
- SUPPORTED_LOCALES,
13
- alternatePathForLocale,
14
- isLocale,
15
- stripLocaleFromPath,
9
+ DEFAULT_LOCALE,
10
+ LOCALE_LABELS,
11
+ type Locale,
12
+ SUPPORTED_LOCALES,
13
+ alternatePathForLocale,
14
+ isLocale,
15
+ stripLocaleFromPath,
16
16
  } from '@anglefeint/site-i18n/config';
17
17
 
18
18
  interface Props {
19
- locale?: Locale;
20
- homeHref?: string;
21
- /** Optional per-locale overrides (used for existence-aware language fallback on some routes). */
22
- localeHrefs?: Partial<Record<Locale, string>>;
23
- /** Show Blade Runner scanlines overlay on ai-page (header/footer only) */
24
- scanlines?: boolean;
25
- labels?: {
26
- home: string;
27
- blog: string;
28
- about: string;
29
- status: string;
30
- language: string;
31
- };
19
+ locale?: Locale;
20
+ homeHref?: string;
21
+ /** Optional per-locale overrides (used for existence-aware language fallback on some routes). */
22
+ localeHrefs?: Partial<Record<Locale, string>>;
23
+ /** Show Blade Runner scanlines overlay on ai-page (header/footer only) */
24
+ scanlines?: boolean;
25
+ labels?: {
26
+ home: string;
27
+ blog: string;
28
+ about: string;
29
+ status: string;
30
+ statusAria: string;
31
+ language: string;
32
+ };
32
33
  }
33
34
 
34
35
  const props = Astro.props as Props;
35
36
  const locale: Locale = props.locale && isLocale(props.locale) ? props.locale : DEFAULT_LOCALE;
36
37
  const labels = {
37
- home: props.labels?.home ?? 'Home',
38
- blog: props.labels?.blog ?? 'Blog',
39
- about: props.labels?.about ?? 'About',
40
- status: props.labels?.status ?? 'system: online',
41
- language: props.labels?.language ?? 'Language',
38
+ home: props.labels?.home ?? 'Home',
39
+ blog: props.labels?.blog ?? 'Blog',
40
+ about: props.labels?.about ?? 'About',
41
+ status: props.labels?.status ?? 'system: online',
42
+ statusAria: props.labels?.statusAria ?? 'System status',
43
+ language: props.labels?.language ?? 'Language',
42
44
  };
43
45
  const showAbout = THEME.ENABLE_ABOUT_PAGE;
44
46
  const currentSubpath = stripLocaleFromPath(Astro.url.pathname, locale);
45
47
  const homeHref = props.homeHref ?? alternatePathForLocale(locale, '/');
46
48
  const buildLocaleHref = (targetLocale: Locale) => {
47
- // Language switcher: preserve current route when switching locales.
48
- // Only fall back for About when the feature is disabled.
49
- const sectionSubpath =
50
- currentSubpath.startsWith('/about') && !showAbout ? '/' : currentSubpath || '/';
51
- return alternatePathForLocale(targetLocale, sectionSubpath);
49
+ // Language switcher: preserve current route when switching locales.
50
+ // Only fall back for About when the feature is disabled.
51
+ const sectionSubpath =
52
+ currentSubpath.startsWith('/about') && !showAbout ? '/' : currentSubpath || '/';
53
+ return alternatePathForLocale(targetLocale, sectionSubpath);
52
54
  };
53
55
  const localeOptions = SUPPORTED_LOCALES.map((targetLocale) => ({
54
- locale: targetLocale,
55
- label: LOCALE_LABELS[targetLocale],
56
- href: props.localeHrefs?.[targetLocale] ?? buildLocaleHref(targetLocale),
56
+ locale: targetLocale,
57
+ label: LOCALE_LABELS[targetLocale],
58
+ href: props.localeHrefs?.[targetLocale] ?? buildLocaleHref(targetLocale),
57
59
  }));
58
60
  ---
59
61
 
60
62
  <header>
61
- <nav>
62
- <div class="nav-left">
63
- <h2><a href={homeHref}>{SITE_TITLE}</a></h2>
64
- </div>
65
- <div class="internal-links">
66
- <HeaderLink href={homeHref}>{labels.home}</HeaderLink>
67
- <HeaderLink href={alternatePathForLocale(locale, '/blog/')}>{labels.blog}</HeaderLink>
68
- {showAbout && <HeaderLink href={alternatePathForLocale(locale, '/about/')}>{labels.about}</HeaderLink>}
69
- </div>
70
- <div class="nav-right">
71
- <div class="social-links">
72
- <LangSwitcher label={labels.language} currentLocale={locale} options={localeOptions} />
73
- <div class="nav-status" aria-label="System status">
74
- <span class="nav-status-dot" aria-hidden="true"></span>
75
- <span class="nav-status-text">{labels.status}</span>
76
- </div>
77
- <SocialMenu links={SOCIAL_LINKS} />
78
- </div>
79
- </div>
80
- </nav>
81
- {props.scanlines && <div class="ai-scanlines" aria-hidden="true"></div>}
63
+ <nav>
64
+ <div class="nav-left">
65
+ <h2><a href={homeHref}>{SITE_TITLE}</a></h2>
66
+ </div>
67
+ <div class="internal-links">
68
+ <HeaderLink href={homeHref}>{labels.home}</HeaderLink>
69
+ <HeaderLink href={alternatePathForLocale(locale, '/blog/')}>{labels.blog}</HeaderLink>
70
+ {
71
+ showAbout && (
72
+ <HeaderLink href={alternatePathForLocale(locale, '/about/')}>{labels.about}</HeaderLink>
73
+ )
74
+ }
75
+ </div>
76
+ <div class="nav-right">
77
+ <div class="social-links">
78
+ <LangSwitcher label={labels.language} currentLocale={locale} options={localeOptions} />
79
+ <div class="nav-status" aria-label={labels.statusAria}>
80
+ <span class="nav-status-dot" aria-hidden="true"></span>
81
+ <span class="nav-status-text">{labels.status}</span>
82
+ </div>
83
+ <SocialMenu links={SOCIAL_LINKS} />
84
+ </div>
85
+ </div>
86
+ </nav>
87
+ {props.scanlines && <div class="ai-scanlines" aria-hidden="true" />}
82
88
  </header>
83
89
  <style>
84
- header {
85
- margin: 0;
86
- padding: 0 1em;
87
- background: var(--chrome-bg, var(--bg));
88
- border-bottom: 1px solid var(--chrome-border, rgb(var(--border)));
89
- }
90
- h2 {
91
- margin: 0;
92
- font-size: 1em;
93
- }
90
+ header {
91
+ margin: 0;
92
+ padding: 0 1em;
93
+ background: var(--chrome-bg, var(--bg));
94
+ border-bottom: 1px solid var(--chrome-border, rgb(var(--border)));
95
+ }
96
+ h2 {
97
+ margin: 0;
98
+ font-size: 1em;
99
+ }
94
100
 
95
- h2 a,
96
- h2 a.active {
97
- text-decoration: none;
98
- color: var(--chrome-link, rgb(var(--text)));
99
- border-bottom: none;
100
- }
101
- nav {
102
- display: grid;
103
- grid-template-columns: 1fr auto 1fr;
104
- align-items: center;
105
- gap: 0.6rem;
106
- min-height: 56px;
107
- width: 100%;
108
- }
109
- .nav-left {
110
- justify-self: start;
111
- }
112
- .internal-links {
113
- justify-self: center;
114
- display: flex;
115
- align-items: center;
116
- gap: 0.1rem;
117
- }
118
- .nav-right {
119
- justify-self: end;
120
- display: flex;
121
- align-items: center;
122
- gap: 0.35rem;
123
- min-width: max-content;
124
- }
125
- .internal-links :global(a) {
126
- padding: 1em 0.5em;
127
- color: var(--chrome-link, rgb(var(--text)));
128
- border-bottom: 4px solid transparent;
129
- text-decoration: none;
130
- }
131
- .internal-links :global(a.active) {
132
- text-decoration: none;
133
- border-bottom-color: var(--chrome-active, var(--accent));
134
- }
135
- .social-links a {
136
- color: var(--chrome-link, rgb(var(--text)));
137
- align-items: center;
138
- justify-content: center;
139
- padding: 0.6rem 0.35rem;
140
- border-bottom: none;
141
- }
142
- .social-links a:hover {
143
- color: var(--chrome-link-hover, var(--chrome-link, rgb(var(--text))));
144
- }
145
- .social-links,
146
- .social-links a {
147
- display: flex;
148
- }
149
- .social-links {
150
- align-items: center;
151
- gap: 0.4rem;
152
- position: relative;
153
- flex-wrap: nowrap;
154
- --lang-switcher-border: rgba(132, 214, 255, 0.2);
155
- --lang-switcher-bg: rgba(6, 16, 30, 0.35);
156
- --lang-label-color: rgba(190, 226, 248, 0.78);
157
- --lang-select-arrow: rgba(204, 236, 252, 0.82);
158
- --lang-select-bg: rgba(9, 22, 40, 0.68);
159
- --lang-select-border: rgba(132, 214, 255, 0.2);
160
- --lang-select-text: rgba(204, 236, 252, 0.86);
161
- --lang-select-focus-border: rgba(132, 214, 255, 0.5);
162
- --lang-select-focus-ring: rgba(98, 180, 228, 0.18);
163
- --lang-select-option-text: #ccecfb;
164
- --lang-select-option-bg: #0b1c32;
165
- }
166
- .nav-status {
167
- display: none;
168
- align-items: center;
169
- gap: 0.45rem;
170
- padding: 0.24rem 0.5rem;
171
- border-radius: 999px;
172
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
173
- font-size: 0.62rem;
174
- letter-spacing: 0.12em;
175
- text-transform: uppercase;
176
- color: rgba(186, 232, 252, 0.86);
177
- background: rgba(6, 16, 30, 0.52);
178
- border: 1px solid rgba(132, 214, 255, 0.22);
179
- box-shadow:
180
- 0 0 0 1px rgba(132, 214, 255, 0.08),
181
- 0 0 16px rgba(90, 180, 255, 0.16);
182
- white-space: nowrap;
183
- position: absolute;
184
- right: calc(100% + 0.5rem);
185
- top: 50%;
186
- transform: translateY(-50%);
187
- pointer-events: none;
188
- }
189
- .nav-status-dot {
190
- width: 0.44rem;
191
- height: 0.44rem;
192
- border-radius: 50%;
193
- background: rgba(150, 226, 255, 0.96);
194
- box-shadow: 0 0 10px rgba(122, 210, 255, 0.72);
195
- animation: nav-status-pulse 1.8s steps(1, end) infinite;
196
- }
197
- @keyframes nav-status-pulse {
198
- 0%,
199
- 78%,
200
- 100% {
201
- opacity: 1;
202
- transform: scale(1);
203
- }
204
- 82% {
205
- opacity: 0.2;
206
- transform: scale(0.7);
207
- }
208
- 86% {
209
- opacity: 1;
210
- transform: scale(1.05);
211
- }
212
- 90% {
213
- opacity: 0.28;
214
- transform: scale(0.78);
215
- }
216
- }
217
- body.ai-page .nav-status {
218
- display: inline-flex;
219
- }
220
- body.about-page .nav-status {
221
- display: none;
222
- }
223
- @media (max-width: 720px) {
224
- nav {
225
- grid-template-columns: 1fr;
226
- gap: 0;
227
- }
228
- .nav-left {
229
- display: none;
230
- }
231
- .internal-links {
232
- justify-self: start;
233
- }
234
- .nav-right {
235
- justify-self: end;
236
- }
237
- .nav-status {
238
- display: none !important;
239
- }
240
- .social-links {
241
- display: none;
242
- }
243
- .lang-switcher {
244
- margin-right: 0;
245
- }
246
- }
101
+ h2 a,
102
+ h2 a.active {
103
+ text-decoration: none;
104
+ color: var(--chrome-link, rgb(var(--text)));
105
+ border-bottom: none;
106
+ }
107
+ nav {
108
+ display: grid;
109
+ grid-template-columns: 1fr auto 1fr;
110
+ align-items: center;
111
+ gap: 0.6rem;
112
+ min-height: 56px;
113
+ width: 100%;
114
+ }
115
+ .nav-left {
116
+ justify-self: start;
117
+ }
118
+ .internal-links {
119
+ justify-self: center;
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 0.1rem;
123
+ }
124
+ .nav-right {
125
+ justify-self: end;
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 0.35rem;
129
+ min-width: max-content;
130
+ }
131
+ .internal-links :global(a) {
132
+ padding: 1em 0.5em;
133
+ color: var(--chrome-link, rgb(var(--text)));
134
+ border-bottom: 4px solid transparent;
135
+ text-decoration: none;
136
+ }
137
+ .internal-links :global(a.active) {
138
+ text-decoration: none;
139
+ border-bottom-color: var(--chrome-active, var(--accent));
140
+ }
141
+ .social-links a {
142
+ color: var(--chrome-link, rgb(var(--text)));
143
+ align-items: center;
144
+ justify-content: center;
145
+ padding: 0.6rem 0.35rem;
146
+ border-bottom: none;
147
+ }
148
+ .social-links a:hover {
149
+ color: var(--chrome-link-hover, var(--chrome-link, rgb(var(--text))));
150
+ }
151
+ .social-links,
152
+ .social-links a {
153
+ display: flex;
154
+ }
155
+ .social-links {
156
+ align-items: center;
157
+ gap: 0.4rem;
158
+ position: relative;
159
+ flex-wrap: nowrap;
160
+ --lang-switcher-border: rgba(132, 214, 255, 0.2);
161
+ --lang-switcher-bg: rgba(6, 16, 30, 0.35);
162
+ --lang-label-color: rgba(190, 226, 248, 0.78);
163
+ --lang-select-arrow: rgba(204, 236, 252, 0.82);
164
+ --lang-select-bg: rgba(9, 22, 40, 0.68);
165
+ --lang-select-border: rgba(132, 214, 255, 0.2);
166
+ --lang-select-text: rgba(204, 236, 252, 0.86);
167
+ --lang-select-focus-border: rgba(132, 214, 255, 0.5);
168
+ --lang-select-focus-ring: rgba(98, 180, 228, 0.18);
169
+ --lang-select-option-text: #ccecfb;
170
+ --lang-select-option-bg: #0b1c32;
171
+ }
172
+ .nav-status {
173
+ display: none;
174
+ align-items: center;
175
+ gap: 0.45rem;
176
+ padding: 0.24rem 0.5rem;
177
+ border-radius: 999px;
178
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
179
+ font-size: 0.62rem;
180
+ letter-spacing: 0.12em;
181
+ text-transform: uppercase;
182
+ color: rgba(186, 232, 252, 0.86);
183
+ background: rgba(6, 16, 30, 0.52);
184
+ border: 1px solid rgba(132, 214, 255, 0.22);
185
+ box-shadow:
186
+ 0 0 0 1px rgba(132, 214, 255, 0.08),
187
+ 0 0 16px rgba(90, 180, 255, 0.16);
188
+ white-space: nowrap;
189
+ position: absolute;
190
+ right: calc(100% + 0.5rem);
191
+ top: 50%;
192
+ transform: translateY(-50%);
193
+ pointer-events: none;
194
+ }
195
+ .nav-status-dot {
196
+ width: 0.44rem;
197
+ height: 0.44rem;
198
+ border-radius: 50%;
199
+ background: rgba(150, 226, 255, 0.96);
200
+ box-shadow: 0 0 10px rgba(122, 210, 255, 0.72);
201
+ animation: nav-status-pulse 1.8s steps(1, end) infinite;
202
+ }
203
+ @keyframes nav-status-pulse {
204
+ 0%,
205
+ 78%,
206
+ 100% {
207
+ opacity: 1;
208
+ transform: scale(1);
209
+ }
210
+ 82% {
211
+ opacity: 0.2;
212
+ transform: scale(0.7);
213
+ }
214
+ 86% {
215
+ opacity: 1;
216
+ transform: scale(1.05);
217
+ }
218
+ 90% {
219
+ opacity: 0.28;
220
+ transform: scale(0.78);
221
+ }
222
+ }
223
+ body.ai-page .nav-status {
224
+ display: inline-flex;
225
+ }
226
+ body.about-page .nav-status {
227
+ display: none;
228
+ }
229
+ @media (max-width: 720px) {
230
+ nav {
231
+ grid-template-columns: 1fr;
232
+ gap: 0;
233
+ }
234
+ .nav-left {
235
+ display: none;
236
+ }
237
+ .internal-links {
238
+ justify-self: start;
239
+ }
240
+ .nav-right {
241
+ justify-self: end;
242
+ }
243
+ .nav-status {
244
+ display: none !important;
245
+ }
246
+ .social-links {
247
+ display: none;
248
+ }
249
+ .lang-switcher {
250
+ margin-right: 0;
251
+ }
252
+ }
247
253
  </style>
@@ -3,46 +3,46 @@ import { SOCIAL_LINKS, type SocialLink } from '@anglefeint/site-config/social';
3
3
  import Icon from './Icon.astro';
4
4
 
5
5
  interface Props {
6
- links?: SocialLink[];
7
- iconSize?: number;
8
- showPlaceholdersWhenEmpty?: boolean;
6
+ links?: SocialLink[];
7
+ iconSize?: number;
8
+ showPlaceholdersWhenEmpty?: boolean;
9
9
  }
10
10
 
11
11
  const {
12
- links = SOCIAL_LINKS,
13
- iconSize = 32,
14
- showPlaceholdersWhenEmpty = true,
12
+ links = SOCIAL_LINKS,
13
+ iconSize = 32,
14
+ showPlaceholdersWhenEmpty = true,
15
15
  } = Astro.props as Props;
16
16
  const placeholderLinks: SocialLink[] = [
17
- { href: '', label: 'Mastodon', icon: 'mastodon' },
18
- { href: '', label: 'Twitter', icon: 'twitter' },
19
- { href: '', label: 'GitHub', icon: 'github' },
17
+ { href: '', label: 'Mastodon', icon: 'mastodon' },
18
+ { href: '', label: 'Twitter', icon: 'twitter' },
19
+ { href: '', label: 'GitHub', icon: 'github' },
20
20
  ];
21
21
  const resolvedLinks = links.length > 0 || !showPlaceholdersWhenEmpty ? links : placeholderLinks;
22
22
  ---
23
23
 
24
24
  {
25
- resolvedLinks.map((link) => (
26
- links.length > 0 ? (
27
- <a href={link.href} target="_blank" rel="noopener noreferrer">
28
- <span class="sr-only">{link.label}</span>
29
- {link.icon ? <Icon name={link.icon} size={iconSize} /> : <span>{link.label}</span>}
30
- </a>
31
- ) : (
32
- <span class="social-placeholder" aria-hidden="true" title="Configure social links in src/site.config.ts">
33
- {link.icon ? <Icon name={link.icon} size={iconSize} /> : <span>{link.label}</span>}
34
- </span>
35
- )
36
- ))
25
+ resolvedLinks.map((link) =>
26
+ links.length > 0 ? (
27
+ <a href={link.href} target="_blank" rel="noopener noreferrer">
28
+ <span class="sr-only">{link.label}</span>
29
+ {link.icon ? <Icon name={link.icon} size={iconSize} /> : <span>{link.label}</span>}
30
+ </a>
31
+ ) : (
32
+ <span class="social-placeholder" aria-hidden="true">
33
+ {link.icon ? <Icon name={link.icon} size={iconSize} /> : <span>{link.label}</span>}
34
+ </span>
35
+ )
36
+ )
37
37
  }
38
38
 
39
39
  <style>
40
- .social-placeholder {
41
- display: inline-flex;
42
- align-items: center;
43
- justify-content: center;
44
- opacity: 0.45;
45
- filter: saturate(0.7);
46
- cursor: default;
47
- }
40
+ .social-placeholder {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ opacity: 0.45;
45
+ filter: saturate(0.7);
46
+ cursor: default;
47
+ }
48
48
  </style>
@@ -7,79 +7,80 @@ import { getMessages } from '@anglefeint/site-i18n/messages';
7
7
  import type { ImageMetadata } from 'astro';
8
8
 
9
9
  interface Props {
10
- locale?: Locale;
11
- title: string;
12
- description: string;
13
- image?: ImageMetadata;
14
- pageType?: 'website' | 'article';
15
- publishedTime?: Date;
16
- modifiedTime?: Date;
17
- author?: string;
18
- tags?: string[];
19
- schema?: Record<string, unknown> | Record<string, unknown>[];
20
- noindex?: boolean;
21
- bodyClass: string;
22
- mainClass?: string;
23
- scanlines?: boolean;
24
- localeHrefs?: Partial<Record<Locale, string>>;
10
+ locale?: Locale;
11
+ title: string;
12
+ description: string;
13
+ image?: ImageMetadata;
14
+ pageType?: 'website' | 'article';
15
+ publishedTime?: Date;
16
+ modifiedTime?: Date;
17
+ author?: string;
18
+ tags?: string[];
19
+ schema?: Record<string, unknown> | Record<string, unknown>[];
20
+ noindex?: boolean;
21
+ bodyClass: string;
22
+ mainClass?: string;
23
+ scanlines?: boolean;
24
+ localeHrefs?: Partial<Record<Locale, string>>;
25
25
  }
26
26
 
27
27
  const rawLocale = Astro.props.locale ?? DEFAULT_LOCALE;
28
28
  const locale: Locale = isLocale(rawLocale) ? rawLocale : DEFAULT_LOCALE;
29
29
  const {
30
- title,
31
- description,
32
- image,
33
- pageType,
34
- publishedTime,
35
- modifiedTime,
36
- author,
37
- tags,
38
- schema,
39
- noindex,
40
- bodyClass,
41
- mainClass = 'page-main',
42
- scanlines = false,
43
- localeHrefs,
30
+ title,
31
+ description,
32
+ image,
33
+ pageType,
34
+ publishedTime,
35
+ modifiedTime,
36
+ author,
37
+ tags,
38
+ schema,
39
+ noindex,
40
+ bodyClass,
41
+ mainClass = 'page-main',
42
+ scanlines = false,
43
+ localeHrefs,
44
44
  } = Astro.props as Props;
45
45
  const messages = getMessages(locale);
46
46
  ---
47
47
 
48
48
  <!doctype html>
49
49
  <html lang={locale}>
50
- <head>
51
- <BaseHead
52
- title={title}
53
- description={description}
54
- image={image}
55
- pageType={pageType}
56
- publishedTime={publishedTime}
57
- modifiedTime={modifiedTime}
58
- author={author}
59
- tags={tags}
60
- schema={schema}
61
- noindex={noindex}
62
- />
63
- <slot name="head" />
64
- </head>
65
- <body class={bodyClass}>
66
- <slot name="body-start" />
67
- <CommonHeader
68
- locale={locale}
69
- localeHrefs={localeHrefs}
70
- scanlines={scanlines}
71
- labels={{
72
- home: messages.nav.home,
73
- blog: messages.nav.blog,
74
- about: messages.nav.about,
75
- status: messages.nav.status,
76
- language: messages.langLabel,
77
- }}
78
- />
79
- <main class={mainClass}>
80
- <slot />
81
- </main>
82
- <CommonFooter scanlines={scanlines} />
83
- <slot name="body-end" />
84
- </body>
50
+ <head>
51
+ <BaseHead
52
+ title={title}
53
+ description={description}
54
+ image={image}
55
+ pageType={pageType}
56
+ publishedTime={publishedTime}
57
+ modifiedTime={modifiedTime}
58
+ author={author}
59
+ tags={tags}
60
+ schema={schema}
61
+ noindex={noindex}
62
+ />
63
+ <slot name="head" />
64
+ </head>
65
+ <body class={bodyClass}>
66
+ <slot name="body-start" />
67
+ <CommonHeader
68
+ locale={locale}
69
+ localeHrefs={localeHrefs}
70
+ scanlines={scanlines}
71
+ labels={{
72
+ home: messages.nav.home,
73
+ blog: messages.nav.blog,
74
+ about: messages.nav.about,
75
+ status: messages.nav.status,
76
+ statusAria: messages.nav.statusAria,
77
+ language: messages.langLabel,
78
+ }}
79
+ />
80
+ <main class={mainClass}>
81
+ <slot />
82
+ </main>
83
+ <CommonFooter scanlines={scanlines} />
84
+ <slot name="body-end" />
85
+ </body>
85
86
  </html>
@@ -34,7 +34,16 @@ export const THEME = {
34
34
  REPO_ID: '',
35
35
  CATEGORY: '',
36
36
  CATEGORY_ID: '',
37
+ MAPPING: 'pathname',
38
+ TERM: '',
39
+ NUMBER: '',
40
+ STRICT: '0',
41
+ REACTIONS_ENABLED: '1',
42
+ EMIT_METADATA: '0',
43
+ INPUT_POSITION: 'bottom',
37
44
  THEME: 'dark',
38
45
  LANG: 'en',
46
+ LOADING: 'lazy',
47
+ CROSSORIGIN: 'anonymous',
39
48
  },
40
49
  } as const;
@@ -9,6 +9,7 @@ export type Messages = {
9
9
  blog: string;
10
10
  about: string;
11
11
  status: string;
12
+ statusAria: string;
12
13
  };
13
14
  home: {
14
15
  hero: string;
@@ -41,10 +42,24 @@ export type Messages = {
41
42
  related: string;
42
43
  comments: string;
43
44
  responseOutput: string;
45
+ rqBadge: string;
46
+ rqReplayAria: string;
47
+ metaPublished: string;
48
+ metaUpdated: string;
49
+ metaReadMinutes: string;
50
+ systemStatusAria: string;
51
+ promptContextLabel: string;
52
+ latencyLabel: string;
53
+ confidenceLabel: string;
54
+ statsWords: string;
55
+ statsTokens: string;
44
56
  heroMonitor: string;
45
57
  heroSignalSync: string;
46
58
  heroModelOnline: string;
47
59
  regenerate: string;
60
+ relatedAria: string;
61
+ backToBlogAria: string;
62
+ paginationAria: string;
48
63
  toastP10: string;
49
64
  toastP30: string;
50
65
  toastP60: string;
@@ -57,7 +72,13 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
57
72
  siteTitle: 'Angle Feint',
58
73
  siteDescription: 'Cinematic web interfaces and AI-era engineering essays.',
59
74
  langLabel: 'Language',
60
- nav: { home: 'Home', blog: 'Blog', about: 'About', status: 'system: online' },
75
+ nav: {
76
+ home: 'Home',
77
+ blog: 'Blog',
78
+ about: 'About',
79
+ status: 'system: online',
80
+ statusAria: 'System status',
81
+ },
61
82
  home: {
62
83
  hero: 'Write a short introduction for your site and what readers can expect from your posts.',
63
84
  latest: 'Latest Posts',
@@ -89,10 +110,24 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
89
110
  related: 'Related',
90
111
  comments: 'Comments',
91
112
  responseOutput: 'Output',
113
+ rqBadge: 'monitor feed',
114
+ rqReplayAria: 'Replay monitor feed',
115
+ metaPublished: 'published',
116
+ metaUpdated: 'updated',
117
+ metaReadMinutes: 'min read',
118
+ systemStatusAria: 'Model status',
119
+ promptContextLabel: 'Context',
120
+ latencyLabel: 'latency est',
121
+ confidenceLabel: 'confidence',
122
+ statsWords: 'words',
123
+ statsTokens: 'tokens',
92
124
  heroMonitor: 'neural monitor',
93
125
  heroSignalSync: 'signal sync active',
94
126
  heroModelOnline: 'model online',
95
127
  regenerate: 'Regenerate',
128
+ relatedAria: 'Related posts',
129
+ backToBlogAria: 'Back to blog',
130
+ paginationAria: 'Pagination',
96
131
  toastP10: 'context parsed 10%',
97
132
  toastP30: 'context parsed 30%',
98
133
  toastP60: 'inference stable 60%',
@@ -103,7 +138,13 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
103
138
  siteTitle: 'Angle Feint',
104
139
  siteDescription: '映画的なWebインターフェースとAI時代のエンジニアリング考察。',
105
140
  langLabel: '言語',
106
- nav: { home: 'ホーム', blog: 'ブログ', about: 'プロフィール', status: 'system: online' },
141
+ nav: {
142
+ home: 'ホーム',
143
+ blog: 'ブログ',
144
+ about: 'プロフィール',
145
+ status: 'system: online',
146
+ statusAria: 'システム状態',
147
+ },
107
148
  home: {
108
149
  hero: 'このサイトの紹介文と、読者がどんな記事を期待できるかを書いてください。',
109
150
  latest: '最新記事',
@@ -135,10 +176,24 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
135
176
  related: '関連記事',
136
177
  comments: 'コメント',
137
178
  responseOutput: '出力',
179
+ rqBadge: 'モニターフィード',
180
+ rqReplayAria: 'モニターフィードを再生',
181
+ metaPublished: '公開',
182
+ metaUpdated: '更新',
183
+ metaReadMinutes: '分で読了',
184
+ systemStatusAria: 'モデル状態',
185
+ promptContextLabel: 'コンテキスト',
186
+ latencyLabel: '推定レイテンシ',
187
+ confidenceLabel: '信頼度',
188
+ statsWords: '語',
189
+ statsTokens: 'トークン',
138
190
  heroMonitor: 'ニューラルモニター',
139
191
  heroSignalSync: 'シグナル同期中',
140
192
  heroModelOnline: 'モデルオンライン',
141
193
  regenerate: '再生成',
194
+ relatedAria: '関連記事',
195
+ backToBlogAria: 'ブログへ戻る',
196
+ paginationAria: 'ページネーション',
142
197
  toastP10: '文脈解析 10%',
143
198
  toastP30: '文脈解析 30%',
144
199
  toastP60: '推論安定 60%',
@@ -149,7 +204,13 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
149
204
  siteTitle: 'Angle Feint',
150
205
  siteDescription: '시네마틱 웹 인터페이스와 AI 시대 엔지니어링 에세이.',
151
206
  langLabel: '언어',
152
- nav: { home: '홈', blog: '블로그', about: '소개', status: 'system: online' },
207
+ nav: {
208
+ home: '홈',
209
+ blog: '블로그',
210
+ about: '소개',
211
+ status: 'system: online',
212
+ statusAria: '시스템 상태',
213
+ },
153
214
  home: {
154
215
  hero: '사이트 소개와 방문자가 어떤 글을 기대할 수 있는지 간단히 작성하세요.',
155
216
  latest: '최신 글',
@@ -181,10 +242,24 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
181
242
  related: '관련 글',
182
243
  comments: '댓글',
183
244
  responseOutput: '출력',
245
+ rqBadge: '모니터 피드',
246
+ rqReplayAria: '모니터 피드 다시 재생',
247
+ metaPublished: '게시',
248
+ metaUpdated: '수정',
249
+ metaReadMinutes: '분 읽기',
250
+ systemStatusAria: '모델 상태',
251
+ promptContextLabel: '컨텍스트',
252
+ latencyLabel: '지연 추정',
253
+ confidenceLabel: '신뢰도',
254
+ statsWords: '단어',
255
+ statsTokens: '토큰',
184
256
  heroMonitor: '뉴럴 모니터',
185
257
  heroSignalSync: '신호 동기화 활성',
186
258
  heroModelOnline: '모델 온라인',
187
259
  regenerate: '재생성',
260
+ relatedAria: '관련 글',
261
+ backToBlogAria: '블로그로 돌아가기',
262
+ paginationAria: '페이지네이션',
188
263
  toastP10: '컨텍스트 파싱 10%',
189
264
  toastP30: '컨텍스트 파싱 30%',
190
265
  toastP60: '추론 안정화 60%',
@@ -195,7 +270,13 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
195
270
  siteTitle: 'Angle Feint',
196
271
  siteDescription: 'Interfaces web cinematográficas y ensayos de ingeniería en la era de IA.',
197
272
  langLabel: 'Idioma',
198
- nav: { home: 'Inicio', blog: 'Blog', about: 'Sobre mí', status: 'system: online' },
273
+ nav: {
274
+ home: 'Inicio',
275
+ blog: 'Blog',
276
+ about: 'Sobre mí',
277
+ status: 'system: online',
278
+ statusAria: 'Estado del sistema',
279
+ },
199
280
  home: {
200
281
  hero: 'Escribe una breve presentación del sitio y qué tipo de contenido encontrarán tus lectores.',
201
282
  latest: 'Últimas publicaciones',
@@ -228,10 +309,24 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
228
309
  related: 'Relacionados',
229
310
  comments: 'Comentarios',
230
311
  responseOutput: 'Salida',
312
+ rqBadge: 'monitor de señal',
313
+ rqReplayAria: 'Reproducir monitor de señal',
314
+ metaPublished: 'publicado',
315
+ metaUpdated: 'actualizado',
316
+ metaReadMinutes: 'min de lectura',
317
+ systemStatusAria: 'Estado del modelo',
318
+ promptContextLabel: 'Contexto',
319
+ latencyLabel: 'latencia est',
320
+ confidenceLabel: 'confianza',
321
+ statsWords: 'palabras',
322
+ statsTokens: 'tokens',
231
323
  heroMonitor: 'monitor neural',
232
324
  heroSignalSync: 'sincronización de señal activa',
233
325
  heroModelOnline: 'modelo en línea',
234
326
  regenerate: 'Regenerar',
327
+ relatedAria: 'Publicaciones relacionadas',
328
+ backToBlogAria: 'Volver al blog',
329
+ paginationAria: 'Paginación',
235
330
  toastP10: 'contexto analizado 10%',
236
331
  toastP30: 'contexto analizado 30%',
237
332
  toastP60: 'inferencia estable 60%',
@@ -242,7 +337,13 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
242
337
  siteTitle: 'Angle Feint',
243
338
  siteDescription: '电影感网页界面与 AI 时代工程实践文章。',
244
339
  langLabel: '语言',
245
- nav: { home: '首页', blog: '博客', about: '关于', status: 'system: online' },
340
+ nav: {
341
+ home: '首页',
342
+ blog: '博客',
343
+ about: '关于',
344
+ status: 'system: online',
345
+ statusAria: '系统状态',
346
+ },
246
347
  home: {
247
348
  hero: '在这里写一段站点简介,并告诉读者你将发布什么类型的内容。',
248
349
  latest: '最新文章',
@@ -274,10 +375,24 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
274
375
  related: '相关文章',
275
376
  comments: '评论',
276
377
  responseOutput: '输出',
378
+ rqBadge: '监视器信号',
379
+ rqReplayAria: '重放监视器信号',
380
+ metaPublished: '发布',
381
+ metaUpdated: '更新',
382
+ metaReadMinutes: '分钟阅读',
383
+ systemStatusAria: '模型状态',
384
+ promptContextLabel: '语境',
385
+ latencyLabel: '延迟估计',
386
+ confidenceLabel: '置信度',
387
+ statsWords: '词',
388
+ statsTokens: '令牌',
277
389
  heroMonitor: '神经监视器',
278
390
  heroSignalSync: '信号同步中',
279
391
  heroModelOnline: '模型在线',
280
392
  regenerate: '重新生成',
393
+ relatedAria: '相关文章',
394
+ backToBlogAria: '返回博客',
395
+ paginationAria: '分页导航',
281
396
  toastP10: '语境解析 10%',
282
397
  toastP30: '语境解析 30%',
283
398
  toastP60: '推理稳定 60%',
@@ -58,8 +58,19 @@ const hasStats = aiModel || wordCount !== undefined || tokenCount !== undefined;
58
58
  const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : undefined;
59
59
  const enableRedQueen = THEME.EFFECTS.ENABLE_RED_QUEEN;
60
60
  const comments = THEME.COMMENTS;
61
+ const hasMappingParam =
62
+ comments.MAPPING === 'specific'
63
+ ? Boolean(comments.TERM)
64
+ : comments.MAPPING === 'number'
65
+ ? Boolean(comments.NUMBER)
66
+ : true;
61
67
  const hasCommentsConfig = Boolean(
62
- comments.ENABLED && comments.REPO && comments.REPO_ID && comments.CATEGORY && comments.CATEGORY_ID
68
+ comments.ENABLED &&
69
+ comments.REPO &&
70
+ comments.REPO_ID &&
71
+ comments.CATEGORY &&
72
+ comments.CATEGORY_ID &&
73
+ hasMappingParam
63
74
  );
64
75
  ---
65
76
 
@@ -105,13 +116,13 @@ const hasCommentsConfig = Boolean(
105
116
  data-rq-src2={themeRedqueen2.src}
106
117
  />
107
118
  <div class="rq-tv-badge">
108
- monitor feed
119
+ {messages.blog.rqBadge}
109
120
  <span class="rq-tv-dot" />
110
121
  </div>
111
122
  <button
112
123
  type="button"
113
124
  class="rq-tv-toggle"
114
- aria-label="Replay monitor feed"
125
+ aria-label={messages.blog.rqReplayAria}
115
126
  aria-expanded="false"
116
127
  >
117
128
 
@@ -178,13 +189,28 @@ const hasCommentsConfig = Boolean(
178
189
  <div class="prose">
179
190
  <div class="title ai-title">
180
191
  <div class="ai-meta-terminal">
181
- $ published {fmt(pubDate)}
182
- {updatedDate && <> | updated {fmt(updatedDate)}</>}
183
- {readMinutes !== undefined && <> | ~{readMinutes} min read</>}
192
+ $ {messages.blog.metaPublished}
193
+ {fmt(pubDate)}
194
+ {
195
+ updatedDate && (
196
+ <>
197
+ {' '}
198
+ | {messages.blog.metaUpdated} {fmt(updatedDate)}
199
+ </>
200
+ )
201
+ }
202
+ {
203
+ readMinutes !== undefined && (
204
+ <>
205
+ {' '}
206
+ | ~{readMinutes} {messages.blog.metaReadMinutes}
207
+ </>
208
+ )
209
+ }
184
210
  </div>
185
211
  {
186
212
  hasSystemMeta && (
187
- <div class="ai-system-row" aria-label="Model status">
213
+ <div class="ai-system-row" aria-label={messages.blog.systemStatusAria}>
188
214
  {aiModel && <span class="ai-system-chip">model: {aiModel}</span>}
189
215
  {aiMode && <span class="ai-system-chip">mode: {aiMode}</span>}
190
216
  {aiState && <span class="ai-system-chip">state: {aiState}</span>}
@@ -194,7 +220,8 @@ const hasCommentsConfig = Boolean(
194
220
  {
195
221
  contextText && (
196
222
  <div class="ai-prompt-line">
197
- Context: <span class="ai-prompt-topic">{contextText}</span>
223
+ {messages.blog.promptContextLabel}: <span class="ai-prompt-topic">{contextText}</span>{' '}
224
+
198
225
  </div>
199
226
  )
200
227
  }
@@ -212,12 +239,12 @@ const hasCommentsConfig = Boolean(
212
239
  <div class="ai-response-meta">
213
240
  {aiLatencyMs !== undefined && (
214
241
  <span>
215
- latency est <strong>{aiLatencyMs}</strong> ms
242
+ {messages.blog.latencyLabel} <strong>{aiLatencyMs}</strong> ms
216
243
  </span>
217
244
  )}
218
245
  {confidenceText !== undefined && (
219
246
  <span>
220
- confidence <strong>{confidenceText}</strong>
247
+ {messages.blog.confidenceLabel} <strong>{confidenceText}</strong>
221
248
  </span>
222
249
  )}
223
250
  </div>
@@ -234,9 +261,17 @@ const hasCommentsConfig = Boolean(
234
261
  <div class="ai-stats-corner">
235
262
  {aiModel && <span class="ai-model-id">{aiModel}</span>}
236
263
  {aiModel && (wordCount !== undefined || tokenCount !== undefined) && ' · '}
237
- {wordCount !== undefined && <span>{compact(wordCount)} words</span>}
264
+ {wordCount !== undefined && (
265
+ <span>
266
+ {compact(wordCount)} {messages.blog.statsWords}
267
+ </span>
268
+ )}
238
269
  {wordCount !== undefined && tokenCount !== undefined && ' · '}
239
- {tokenCount !== undefined && <span>{compact(tokenCount)} tokens</span>}
270
+ {tokenCount !== undefined && (
271
+ <span>
272
+ {compact(tokenCount)} {messages.blog.statsTokens}
273
+ </span>
274
+ )}
240
275
  </div>
241
276
  )
242
277
  }
@@ -247,7 +282,7 @@ const hasCommentsConfig = Boolean(
247
282
  </article>
248
283
  {
249
284
  related.length > 0 && (
250
- <section class="ai-related" aria-label="Related posts">
285
+ <section class="ai-related" aria-label={messages.blog.relatedAria}>
251
286
  <h2 class="ai-related-title">{messages.blog.related}</h2>
252
287
  <div class="ai-related-grid">
253
288
  {related.map((p) => (
@@ -274,7 +309,7 @@ const hasCommentsConfig = Boolean(
274
309
  </section>
275
310
  )
276
311
  }
277
- <nav class="ai-back-to-blog" aria-label="Back to blog">
312
+ <nav class="ai-back-to-blog" aria-label={messages.blog.backToBlogAria}>
278
313
  <a href={localePath(resolvedLocale, '/blog/')}>
279
314
  <span class="ai-back-prompt">$</span>
280
315
  <span class="ai-back-text">← {messages.blog.backToBlog}</span>
@@ -289,15 +324,17 @@ const hasCommentsConfig = Boolean(
289
324
  data-repo-id={comments.REPO_ID}
290
325
  data-category={comments.CATEGORY}
291
326
  data-category-id={comments.CATEGORY_ID}
292
- data-mapping="pathname"
293
- data-strict="0"
294
- data-reactions-enabled="1"
295
- data-emit-metadata="0"
296
- data-input-position="bottom"
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}
297
334
  data-theme={comments.THEME}
298
335
  data-lang={comments.LANG || resolvedLocale}
299
- data-loading="lazy"
300
- crossorigin="anonymous"
336
+ data-loading={comments.LOADING}
337
+ crossorigin={comments.CROSSORIGIN}
301
338
  async
302
339
  />
303
340
  </section>
@@ -25,7 +25,7 @@ export function initAboutModals(runtimeConfig, prefersReducedMotion) {
25
25
  },
26
26
  ai: {
27
27
  title: 'AI',
28
- body: '<pre>~ $ ai --status --verbose\n\nmodel: anglefeint-core\nmode: reasoning + builder\ncontext window: 128k\ntools: codex / cursor / claude-code\nlatency: 120-220ms\nsafety: guardrails enabled\n\n&gt;&gt; system online\n&gt;&gt; ready for execution</pre>',
28
+ body: '<pre>~ $ ai --status --verbose\n\nmodel: runtime-default\nmode: standard\ncontext window: 32k\nlatency: 100-250ms\nsafety: enabled\n\n&gt;&gt; system online\n&gt;&gt; ready</pre>',
29
29
  type: 'plain',
30
30
  },
31
31
  decryptor: {