feat: 初始化 Halo 暗色模式插件

- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-06 21:03:00 +08:00
commit edbf78f236
115 changed files with 14745 additions and 0 deletions
@@ -0,0 +1,25 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = null, content = ~{::content})}"
>
<th:block th:fragment="content">
<h1>Archives</h1>
<th:block th:each="archive : ${archives.items}">
<h2 th:text="${archive.year}"></h2>
<th:block th:each="month : ${archive.months}">
<h3 th:text="|${archive.year}-${month.month}|"></h3>
<ul>
<li th:each="post : ${month.posts}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
</th:block>
</th:block>
<nav th:if="${archives.hasPrevious() || archives.hasNext()}">
<a th:if="${archives.hasPrevious()}" th:href="@{${archives.prevUrl}}">Previous</a>
<span th:text="|${archives.page} / ${archives.totalPages}|"></span>
<a th:if="${archives.hasNext()}" th:href="@{${archives.nextUrl}}">Next</a>
</nav>
</th:block>
</html>
@@ -0,0 +1,34 @@
body {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
font-family: system-ui, sans-serif;
}
a {
color: #0070f3;
}
header {
display: flex;
gap: 1rem;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
header a {
text-decoration: none;
color: inherit;
}
nav {
display: flex;
gap: 1rem;
}
footer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #eee;
color: #666;
font-size: 0.875rem;
}
article img {
max-width: 100%;
}
@@ -0,0 +1,26 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = ~{::head}, content = ~{::content})}"
>
<th:block th:fragment="head">
<title th:text="|${author.spec.displayName} - ${site.title}|"></title>
</th:block>
<th:block th:fragment="content">
<h1 th:text="${author.spec.displayName}"></h1>
<p th:if="${not #strings.isEmpty(author.spec.bio)}" th:text="${author.spec.bio}"></p>
<ul>
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
</li>
</ul>
<nav th:if="${posts.hasPrevious() || posts.hasNext()}">
<a th:if="${posts.hasPrevious()}" th:href="@{${posts.prevUrl}}">Previous</a>
<span th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a th:if="${posts.hasNext()}" th:href="@{${posts.nextUrl}}">Next</a>
</nav>
</th:block>
</html>
@@ -0,0 +1,17 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = null, content = ~{::content})}"
>
<th:block th:fragment="content">
<h1>Categories</h1>
<ul>
<li th:each="cat : ${categories.items}">
<a
th:href="@{${cat.status.permalink}}"
th:text="|${cat.spec.displayName} (${cat.postCount})|"
></a>
</li>
</ul>
</th:block>
</html>
@@ -0,0 +1,20 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = null, content = ~{::content})}"
>
<th:block th:fragment="content">
<h1 th:text="|Category: ${category.spec.displayName}|"></h1>
<ul>
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
</li>
</ul>
<nav th:if="${posts.hasPrevious() || posts.hasNext()}">
<a th:if="${posts.hasPrevious()}" th:href="@{${posts.prevUrl}}">Previous</a>
<span th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a th:if="${posts.hasNext()}" th:href="@{${posts.nextUrl}}">Next</a>
</nav>
</th:block>
</html>
@@ -0,0 +1,19 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = null, content = ~{::content})}"
>
<th:block th:fragment="content">
<ul>
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
</li>
</ul>
<nav th:if="${posts.hasPrevious() || posts.hasNext()}">
<a th:if="${posts.hasPrevious()}" th:href="@{${posts.prevUrl}}">Previous</a>
<span th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a th:if="${posts.hasNext()}" th:href="@{${posts.nextUrl}}">Next</a>
</nav>
</th:block>
</html>
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en" xmlns:th="https://www.thymeleaf.org" th:fragment="html (head, content)">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title th:text="${site.title}">Site Title</title>
<link rel="stylesheet" th:href="@{/assets/css/style.css?v={v}(v=${theme.spec.version})}" />
<th:block th:if="${head != null}">
<th:block th:replace="${head}" />
</th:block>
</head>
<body>
<header>
<a th:href="@{/}" th:text="${site.title}"></a>
<nav th:with="menu = ${menuFinder.getPrimary()}">
<a
th:each="item : ${menu.menuItems}"
th:href="@{${item.status.href}}"
th:text="${item.status.displayName}"
th:target="${item.spec.target?.value}"
></a>
</nav>
</header>
<main>
<th:block th:replace="${content}" />
</main>
<footer>
<p th:text="${theme.config.basic.custom_footer ?: site.title}"></p>
<halo:footer />
</footer>
</body>
</html>
@@ -0,0 +1,23 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = ~{::head}, content = ~{::content})}"
>
<th:block th:fragment="head">
<title th:text="|${singlePage.spec.title} - ${site.title}|"></title>
</th:block>
<th:block th:fragment="content">
<article>
<h1 th:text="${singlePage.spec.title}"></h1>
<div th:utext="${singlePage.content.content}"></div>
</article>
<div th:if="${haloCommentEnabled}">
<halo:comment
group="content.halo.run"
kind="SinglePage"
th:attr="name=${singlePage.metadata.name}"
/>
</div>
</th:block>
</html>
@@ -0,0 +1,42 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = ~{::head}, content = ~{::content})}"
>
<th:block th:fragment="head">
<title th:text="|${post.spec.title} - ${site.title}|"></title>
</th:block>
<th:block th:fragment="content">
<article>
<h1 th:text="${post.spec.title}"></h1>
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
<div th:utext="${post.content.content}"></div>
<div th:if="${not #lists.isEmpty(post.tags)}">
<a
th:each="tag : ${post.tags}"
th:href="@{${tag.status.permalink}}"
th:text="${tag.spec.displayName}"
></a>
</div>
</article>
<nav th:with="cursor = ${postFinder.cursor(post.metadata.name)}">
<a
th:if="${cursor.hasPrevious()}"
th:href="@{${cursor.previous.status.permalink}}"
th:text="${cursor.previous.spec.title}"
>Previous</a
>
<a
th:if="${cursor.hasNext()}"
th:href="@{${cursor.next.status.permalink}}"
th:text="${cursor.next.spec.title}"
>Next</a
>
</nav>
<div th:if="${haloCommentEnabled}">
<halo:comment group="content.halo.run" kind="Post" th:attr="name=${post.metadata.name}" />
</div>
</th:block>
</html>
@@ -0,0 +1,20 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = null, content = ~{::content})}"
>
<th:block th:fragment="content">
<h1 th:text="|Tag: ${tag.spec.displayName}|"></h1>
<ul>
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
</li>
</ul>
<nav th:if="${posts.hasPrevious() || posts.hasNext()}">
<a th:if="${posts.hasPrevious()}" th:href="@{${posts.prevUrl}}">Previous</a>
<span th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a th:if="${posts.hasNext()}" th:href="@{${posts.nextUrl}}">Next</a>
</nav>
</th:block>
</html>
@@ -0,0 +1,14 @@
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head = null, content = ~{::content})}"
>
<th:block th:fragment="content">
<h1>Tags</h1>
<ul>
<li th:each="tag : ${tags.items}">
<a th:href="@{${tag.status.permalink}}" th:text="${tag.spec.displayName}"></a>
</li>
</ul>
</th:block>
</html>