@anglefeint/astro-theme 0.1.17 → 0.1.19

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
@@ -59,4 +59,24 @@ In the starter/site project, map these aliases to `src/config/*` and `src/i18n/*
59
59
  - `anglefeint-new-post`
60
60
  - `anglefeint-new-page`
61
61
 
62
- Starter projects can invoke these directly (or wrap them in npm scripts).
62
+ Examples:
63
+
64
+ ```bash
65
+ # create one post slug in all default locales
66
+ anglefeint-new-post my-first-post
67
+
68
+ # create post only for selected locales
69
+ anglefeint-new-post my-first-post --locales en,ja
70
+
71
+ # or via environment variable
72
+ ANGLEFEINT_LOCALES=en,ja anglefeint-new-post my-first-post
73
+
74
+ # create a custom page with theme variant
75
+ anglefeint-new-page projects --theme base
76
+ anglefeint-new-page projects --theme ai
77
+ anglefeint-new-page projects --theme cyber
78
+ anglefeint-new-page projects --theme hacker
79
+ anglefeint-new-page projects --theme matrix
80
+ ```
81
+
82
+ Starter projects can invoke these directly (or wrap them in npm scripts). For most users, `#starter` is the recommended installation path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anglefeint/astro-theme",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "description": "Anglefeint core theme package for Astro",
6
6
  "keywords": [
@@ -26,6 +26,7 @@
26
26
  "src/scripts",
27
27
  "src/i18n",
28
28
  "src/styles",
29
+ "src/utils",
29
30
  "src/assets/theme",
30
31
  "src/cli-new-post.mjs",
31
32
  "src/cli-new-page.mjs"
@@ -42,6 +43,8 @@
42
43
  "./i18n/*": "./src/i18n/*",
43
44
  "./styles/*": "./src/styles/*",
44
45
  "./assets/*": "./src/assets/*",
46
+ "./utils/merge": "./src/utils/merge.ts",
47
+ "./utils/*": "./src/utils/*",
45
48
  "./content-schema": "./src/content-schema.ts",
46
49
  "./consts": "./src/consts.ts"
47
50
  },
@@ -5,7 +5,9 @@ import { SUPPORTED_LOCALES } from './i18n/locales.mjs';
5
5
  import {
6
6
  buildNewPostTemplate,
7
7
  loadDefaultCovers,
8
+ parseNewPostArgs,
8
9
  pickDefaultCoverBySlug,
10
+ resolveLocales,
9
11
  usageNewPost,
10
12
  validatePostSlug,
11
13
  } from './scaffold/new-post.mjs';
@@ -23,7 +25,7 @@ async function exists(filePath) {
23
25
  }
24
26
 
25
27
  async function main() {
26
- const slug = process.argv[2];
28
+ const { slug, locales: cliLocales } = parseNewPostArgs(process.argv);
27
29
  if (!slug) {
28
30
  console.error(usageNewPost());
29
31
  process.exit(1);
@@ -36,10 +38,15 @@ async function main() {
36
38
 
37
39
  const pubDate = new Date().toISOString().slice(0, 10);
38
40
  const defaultCovers = await loadDefaultCovers(DEFAULT_COVERS_ROOT);
41
+ const locales = resolveLocales({
42
+ cliLocales,
43
+ envLocales: process.env.ANGLEFEINT_LOCALES ?? '',
44
+ defaultLocales: SUPPORTED_LOCALES,
45
+ });
39
46
  const created = [];
40
47
  const skipped = [];
41
48
 
42
- for (const locale of SUPPORTED_LOCALES) {
49
+ for (const locale of locales) {
43
50
  const localeDir = path.join(CONTENT_ROOT, locale);
44
51
  const filePath = path.join(localeDir, `${slug}.md`);
45
52
  await mkdir(localeDir, { recursive: true });
@@ -8,4 +8,8 @@ export const THEME = {
8
8
  HOME_LATEST_COUNT: 3,
9
9
  /** Whether to enable the About page (disable to hide from nav/routes if needed) */
10
10
  ENABLE_ABOUT_PAGE: true,
11
+ /** Optional visual effects switches */
12
+ EFFECTS: {
13
+ ENABLE_RED_QUEEN: true,
14
+ },
11
15
  } as const;
@@ -7,6 +7,7 @@ import FormattedDate from '../components/FormattedDate.astro';
7
7
  import AiShell from './shells/AiShell.astro';
8
8
  import blogPostCssUrl from '../styles/blog-post.css?url';
9
9
  import { SITE_AUTHOR } from '@anglefeint/site-config/site';
10
+ import { THEME } from '@anglefeint/site-config/theme';
10
11
  import { DEFAULT_LOCALE, type Locale, isLocale, localePath, blogIdToSlugAnyLocale } from '@anglefeint/site-i18n/config';
11
12
  import { getMessages } from '@anglefeint/site-i18n/messages';
12
13
 
@@ -49,6 +50,7 @@ const hasSystemMeta = Boolean(aiModel || aiMode || aiState);
49
50
  const hasResponseMeta = aiLatencyMs !== undefined || aiConfidence !== undefined;
50
51
  const hasStats = aiModel || wordCount !== undefined || tokenCount !== undefined;
51
52
  const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : undefined;
53
+ const enableRedQueen = THEME.EFFECTS.ENABLE_RED_QUEEN;
52
54
  ---
53
55
 
54
56
  <AiShell
@@ -79,11 +81,13 @@ const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : un
79
81
  </div>
80
82
  <canvas class="ai-network-canvas" aria-hidden="true"></canvas>
81
83
  </div>
82
- <aside class="rq-tv rq-tv-collapsed">
83
- <div class="rq-tv-stage" data-rq-src={themeRedqueen1.src} data-rq-src2={themeRedqueen2.src}></div>
84
- <div class="rq-tv-badge">monitor feed<span class="rq-tv-dot"></span></div>
85
- <button type="button" class="rq-tv-toggle" aria-label="Replay monitor feed" aria-expanded="false">▶</button>
86
- </aside>
84
+ {enableRedQueen && (
85
+ <aside class="rq-tv rq-tv-collapsed">
86
+ <div class="rq-tv-stage" data-rq-src={themeRedqueen1.src} data-rq-src2={themeRedqueen2.src}></div>
87
+ <div class="rq-tv-badge">monitor feed<span class="rq-tv-dot"></span></div>
88
+ <button type="button" class="rq-tv-toggle" aria-label="Replay monitor feed" aria-expanded="false">▶</button>
89
+ </aside>
90
+ )}
87
91
  <div class="ai-read-progress" aria-hidden="true"></div>
88
92
  <button type="button" class="ai-back-to-top" aria-label="Back to top" title="Back to top">↑</button>
89
93
  <div class="ai-stage-toast" aria-live="polite" aria-atomic="true"></div>
@@ -17,6 +17,8 @@ export function initBlogpostEffects() {
17
17
  initReadProgressAndBackToTop(prefersReducedMotion);
18
18
  initNetworkCanvas(prefersReducedMotion);
19
19
  initHeroCanvas(prefersReducedMotion);
20
- initRedQueenTv(prefersReducedMotion);
20
+ if (document.querySelector('.rq-tv-stage')) {
21
+ initRedQueenTv(prefersReducedMotion);
22
+ }
21
23
  initPostInteractions(prefersReducedMotion);
22
24
  }
@@ -0,0 +1,57 @@
1
+ /* About background layer: terminal backdrop and scan/flicker ambience. */
2
+ /* ── terminal background ── */
3
+ .hacker-bg {
4
+ position: fixed;
5
+ inset: 0;
6
+ z-index: 0;
7
+ overflow: hidden;
8
+ pointer-events: none;
9
+ }
10
+ .hacker-bg-canvas {
11
+ position: absolute;
12
+ inset: 0;
13
+ width: 100%;
14
+ height: 100%;
15
+ }
16
+ .hacker-scanlines {
17
+ position: absolute;
18
+ inset: 0;
19
+ /* 浅色扫描线:在深色背景上可见,全屏 CRT 感 */
20
+ background: repeating-linear-gradient(
21
+ 180deg,
22
+ transparent 0px,
23
+ transparent 2px,
24
+ rgba(255, 255, 255, 0.03) 2px,
25
+ rgba(255, 255, 255, 0.03) 3px
26
+ );
27
+ pointer-events: none;
28
+ }
29
+ .hacker-vignette {
30
+ position: absolute;
31
+ inset: 0;
32
+ background: radial-gradient(
33
+ ellipse at center,
34
+ transparent 50%,
35
+ rgba(0, 0, 0, 0.5) 80%,
36
+ rgba(0, 0, 0, 0.85) 100%
37
+ );
38
+ pointer-events: none;
39
+ }
40
+ .hacker-flicker {
41
+ position: absolute;
42
+ inset: 0;
43
+ background: transparent;
44
+ animation: hacker-flicker-anim 0.08s infinite;
45
+ pointer-events: none;
46
+ }
47
+ @keyframes hacker-flicker-anim {
48
+ 0% { background: rgba(255, 255, 255, 0.012); }
49
+ 50% { background: transparent; }
50
+ 100% { background: rgba(255, 255, 255, 0.005); }
51
+ }
52
+ .hacker-glow {
53
+ position: absolute;
54
+ inset: 0;
55
+ box-shadow: inset 0 0 120px rgba(255, 255, 255, 0.03);
56
+ pointer-events: none;
57
+ }
@@ -0,0 +1,58 @@
1
+ /* About base layer: hacker-page chrome variables and shell baselines. */
2
+ body.hacker-page {
3
+ background: #000 !important;
4
+ background-image: none !important;
5
+ color: rgba(255, 255, 255, 0.88);
6
+ min-height: 100vh;
7
+ scrollbar-width: thin;
8
+ scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
9
+ /* header: Anonymous 黑白灰 + 黄点缀 */
10
+ --chrome-bg: rgba(0, 0, 0, 0.42);
11
+ --chrome-border: rgba(255, 255, 255, 0.2);
12
+ --chrome-link: rgba(255, 255, 255, 0.9);
13
+ --chrome-link-hover: rgba(0, 255, 100, 0.95);
14
+ --chrome-active: rgba(255, 255, 255, 0.5);
15
+ --chrome-text-muted: rgba(255, 255, 255, 0.5);
16
+ }
17
+ body.hacker-page::-webkit-scrollbar { width: 6px; }
18
+ body.hacker-page::-webkit-scrollbar-track { background: transparent; }
19
+ body.hacker-page::-webkit-scrollbar-thumb {
20
+ background: rgba(255, 255, 255, 0.22);
21
+ border-radius: 3px;
22
+ }
23
+ /* fixed header + content offset (mirrors ai-page in global.css) */
24
+ body.hacker-page header {
25
+ position: fixed;
26
+ top: 0;
27
+ left: 0;
28
+ right: 0;
29
+ z-index: 100;
30
+ backdrop-filter: blur(3px);
31
+ -webkit-backdrop-filter: blur(3px);
32
+ }
33
+ body.hacker-page .hacker-content {
34
+ padding-top: calc(3em + 56px);
35
+ }
36
+ body.hacker-page .hacker-content,
37
+ body.hacker-page footer {
38
+ position: relative;
39
+ z-index: 10;
40
+ }
41
+ /* footer: Anonymous 灰白 */
42
+ body.hacker-page footer {
43
+ --chrome-bg: rgba(0, 0, 0, 0.85);
44
+ --chrome-border: rgba(255, 255, 255, 0.15);
45
+ border-top-color: rgba(255, 255, 255, 0.15) !important;
46
+ color: rgba(255, 255, 255, 0.55);
47
+ }
48
+ body.hacker-page footer a {
49
+ color: rgba(255, 255, 255, 0.7) !important;
50
+ }
51
+ body.hacker-page footer a:hover {
52
+ color: rgba(0, 255, 100, 0.9) !important;
53
+ }
54
+ @media (max-width: 720px) {
55
+ body.hacker-page .hacker-content {
56
+ padding-top: calc(1em + 56px);
57
+ }
58
+ }
@@ -0,0 +1,112 @@
1
+ /* About keyboard layer: virtual keyboard and helper interaction visuals. */
2
+ /* Help: 虚拟键盘 */
3
+ .hacker-vkeyboard {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 4px;
7
+ font-size: 0.7rem;
8
+ }
9
+ .hacker-vkeyboard-row {
10
+ display: flex;
11
+ justify-content: center;
12
+ gap: 3px;
13
+ }
14
+ .hacker-vkey {
15
+ min-width: 24px;
16
+ height: 28px;
17
+ padding: 0 6px;
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ background: rgba(255, 255, 255, 0.08);
22
+ border: 1px solid rgba(255, 255, 255, 0.2);
23
+ border-radius: 4px;
24
+ color: rgba(255, 255, 255, 0.9);
25
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
26
+ cursor: pointer;
27
+ transition: all 0.1s ease;
28
+ user-select: none;
29
+ }
30
+ .hacker-vkey:hover {
31
+ background: rgba(255, 255, 255, 0.15);
32
+ border-color: rgba(255, 255, 255, 0.35);
33
+ }
34
+ .hacker-vkey.highlight {
35
+ background: rgba(0, 255, 100, 0.35);
36
+ border-color: rgba(0, 255, 100, 0.7);
37
+ box-shadow: 0 0 12px rgba(0, 255, 100, 0.3);
38
+ }
39
+ .hacker-vkey.wide { min-width: 36px; }
40
+ .hacker-vkey.backspace { min-width: 72px; }
41
+ .hacker-vkey.space { min-width: 120px; }
42
+ .hacker-vkey.acc { background: rgba(255, 140, 0, 0.25); border-color: rgba(255, 140, 0, 0.5); }
43
+ .hacker-vkey.acc:hover { background: rgba(255, 140, 0, 0.35); }
44
+ .hacker-vkeyboard-wrap {
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ gap: 2rem;
48
+ align-items: stretch;
49
+ }
50
+ .hacker-vkeyboard-main {
51
+ flex: 1;
52
+ min-width: 0;
53
+ }
54
+ .hacker-vkeyboard-side {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 6px;
58
+ flex-shrink: 0;
59
+ min-width: 140px;
60
+ }
61
+ .hacker-vkeyboard-side-block {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 4px;
65
+ margin-bottom: 4px;
66
+ }
67
+ .hacker-vkeyboard-side-row {
68
+ display: flex;
69
+ gap: 3px;
70
+ }
71
+ .hacker-vkeyboard-side-block .hacker-vkey {
72
+ flex: 1;
73
+ min-width: 0;
74
+ }
75
+ .hacker-vkey.nav-home { background: rgba(66, 133, 244, 0.3); border-color: rgba(66, 133, 244, 0.5); }
76
+ .hacker-vkey.nav-home:hover { background: rgba(66, 133, 244, 0.4); }
77
+ .hacker-vkey.nav-end { background: rgba(234, 67, 53, 0.25); border-color: rgba(234, 67, 53, 0.5); }
78
+ .hacker-vkey.nav-end:hover { background: rgba(234, 67, 53, 0.35); }
79
+ .hacker-vkeyboard-arrows-wrap {
80
+ display: flex;
81
+ flex-direction: column;
82
+ align-items: center;
83
+ margin-top: 6px;
84
+ }
85
+ .hacker-vkeyboard-arrows {
86
+ display: grid;
87
+ grid-template: ". u ." 1fr "l d r" 1fr / 1fr 1fr 1fr;
88
+ gap: 2px;
89
+ height: 56px;
90
+ }
91
+ .hacker-vkeyboard-arrows .arr-u { grid-area: u; }
92
+ .hacker-vkeyboard-arrows .arr-l { grid-area: l; }
93
+ .hacker-vkeyboard-arrows .arr-r { grid-area: r; }
94
+ .hacker-vkeyboard-arrows .arr-d { grid-area: d; }
95
+ .hacker-vkeyboard-arrows .hacker-vkey { width: 50px; height: 22px; font-size: 0.65rem; padding: 0; }
96
+ .hacker-vkeyboard-stats {
97
+ width: 100%;
98
+ margin-top: 0.2rem;
99
+ font-size: 0.75em;
100
+ color: rgba(255, 255, 255, 0.6);
101
+ }
102
+ .hacker-vkeyboard-stats-label {
103
+ font-size: 0.65rem;
104
+ color: rgba(255, 255, 255, 0.5);
105
+ letter-spacing: 0.1em;
106
+ }
107
+ .hacker-modal-body.hacker-modal-keyboard {
108
+ overflow: visible;
109
+ }
110
+ @media (max-width: 900px) {
111
+ .hacker-sidebar { display: none; }
112
+ }
@@ -0,0 +1,159 @@
1
+ /* About modal layer: dialog shells and script explorer surfaces. */
2
+ /* ── 弹窗 (Anonymous 窗口) ── */
3
+ .hacker-modal-overlay {
4
+ position: fixed;
5
+ inset: 0;
6
+ z-index: 200;
7
+ background: rgba(0, 0, 0, 0.7);
8
+ backdrop-filter: blur(4px);
9
+ display: none;
10
+ align-items: center;
11
+ justify-content: center;
12
+ padding: 2rem;
13
+ }
14
+ .hacker-modal-overlay.open {
15
+ display: flex;
16
+ }
17
+ .hacker-modal {
18
+ width: min(420px, 100%);
19
+ max-height: 80vh;
20
+ }
21
+ .hacker-modal.hacker-modal-wide {
22
+ width: min(900px, calc(100vw - 2rem));
23
+ overflow: visible;
24
+ }
25
+ .hacker-modal-header {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 0.5rem;
29
+ padding: 0.5rem 0.9rem;
30
+ background: #fff;
31
+ border-bottom: 1px solid rgba(0, 0, 0, 0.15);
32
+ }
33
+ .hacker-modal-dots {
34
+ display: flex;
35
+ gap: 6px;
36
+ }
37
+ .hacker-modal-dots span {
38
+ width: 10px;
39
+ height: 10px;
40
+ border-radius: 50%;
41
+ }
42
+ .hacker-modal-dots span:nth-child(1) { background: #ff5f57; }
43
+ .hacker-modal-dots span:nth-child(2) { background: #febc2e; }
44
+ .hacker-modal-dots span:nth-child(3) { background: #28c840; }
45
+ .hacker-modal-title {
46
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
47
+ font-size: 0.75em;
48
+ color: #000;
49
+ font-weight: 600;
50
+ letter-spacing: 0.1em;
51
+ text-transform: uppercase;
52
+ }
53
+ .hacker-modal-close {
54
+ margin-left: auto;
55
+ width: 66px;
56
+ height: 66px;
57
+ border: none;
58
+ background: rgba(0, 0, 0, 0.06);
59
+ color: #000;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ cursor: pointer;
64
+ opacity: 0.9;
65
+ border-radius: 10px;
66
+ }
67
+ .hacker-modal-close .hacker-modal-close-icon {
68
+ flex-shrink: 0;
69
+ }
70
+ .hacker-modal-close:hover {
71
+ opacity: 1;
72
+ background: rgba(0, 0, 0, 0.14);
73
+ }
74
+ .hacker-modal-body {
75
+ padding: 1rem 0.9rem 1.2rem;
76
+ color: rgba(255, 255, 255, 0.88);
77
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
78
+ font-size: 0.82rem;
79
+ line-height: 1.6;
80
+ max-height: 60vh;
81
+ overflow-y: auto;
82
+ }
83
+ .hacker-modal-body pre {
84
+ margin: 0;
85
+ white-space: pre-wrap;
86
+ word-break: break-word;
87
+ }
88
+ /* DL Data: Downloading 进度条 (Anonymous 风格) */
89
+ .hacker-modal-body.hacker-modal-download .modal-subtitle {
90
+ font-size: 0.9em;
91
+ color: rgba(255, 255, 255, 0.7);
92
+ margin-bottom: 1rem;
93
+ }
94
+ .hacker-modal-progress {
95
+ display: flex;
96
+ gap: 2px;
97
+ flex-wrap: wrap;
98
+ margin-top: 0.5rem;
99
+ }
100
+ .hacker-modal-progress span {
101
+ width: 10px;
102
+ height: 12px;
103
+ background: rgba(255, 255, 255, 0.2);
104
+ transition: background 0.12s ease;
105
+ }
106
+ .hacker-modal-progress span.filled {
107
+ background: rgba(255, 255, 255, 0.6);
108
+ }
109
+ /* All Scripts: 文件夹网格 */
110
+ .hacker-modal-scripts {
111
+ padding: 0.5rem 0;
112
+ }
113
+ .hacker-modal-scripts-path {
114
+ font-size: 0.9em;
115
+ color: rgba(255, 255, 255, 0.9);
116
+ letter-spacing: 0.05em;
117
+ margin-bottom: 1rem;
118
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
119
+ padding-bottom: 0.5rem;
120
+ }
121
+ .hacker-modal-scripts-grid {
122
+ display: grid;
123
+ grid-template-columns: repeat(5, 1fr);
124
+ gap: 1rem;
125
+ }
126
+ .hacker-script-folder {
127
+ display: flex;
128
+ flex-direction: column;
129
+ align-items: center;
130
+ gap: 0.5rem;
131
+ padding: 0.6rem 0.4rem;
132
+ background: rgba(255, 255, 255, 0.05);
133
+ border: 1px solid rgba(255, 255, 255, 0.12);
134
+ border-radius: 6px;
135
+ color: rgba(255, 255, 255, 0.9);
136
+ text-decoration: none;
137
+ transition: all 0.15s ease;
138
+ }
139
+ .hacker-script-folder:hover {
140
+ background: rgba(255, 255, 255, 0.12);
141
+ border-color: rgba(0, 255, 100, 0.4);
142
+ box-shadow: 0 0 12px rgba(0, 255, 100, 0.15);
143
+ }
144
+ .hacker-script-folder-icon {
145
+ width: 32px;
146
+ height: 28px;
147
+ color: rgba(255, 255, 255, 0.7);
148
+ }
149
+ .hacker-script-folder-label {
150
+ font-size: 0.72rem;
151
+ text-align: center;
152
+ word-break: break-word;
153
+ line-height: 1.3;
154
+ }
155
+ @media (max-width: 600px) {
156
+ .hacker-modal-scripts-grid {
157
+ grid-template-columns: repeat(3, 1fr);
158
+ }
159
+ }