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:
@@ -0,0 +1,294 @@
|
||||
# Vite Integration (Recommended)
|
||||
|
||||
> **Strongly recommended** for building theme projects. Compared to plain Thymeleaf, this approach provides a modern frontend development experience (TypeScript, Sass, PostCSS, TailwindCSS) plus lightweight HTML component reuse (`include`/`slot`) — significantly reducing the verbosity of Thymeleaf's native fragment system.
|
||||
|
||||
---
|
||||
|
||||
## vite-plugin-halo-theme
|
||||
|
||||
- GitHub: https://github.com/halo-sigs/vite-plugin-halo-theme
|
||||
- NPM: `@halo-dev/vite-plugin-halo-theme`
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **HTML template reuse**: supports `<include>`, `<slot>` (default and named), and `<props>` syntax, preprocessed at Vite build time — fully independent of Thymeleaf.
|
||||
2. **Automatic multi-page entry scanning**: automatically discovers all `.html` files under `src/` (except `src/partials/`) as Vite build entries, with output going to `templates/`.
|
||||
3. **Static asset handling**: CSS/JS in `src/` is bundled by Vite into `templates/assets/`; files in `public/` are copied directly to `templates/assets/` without processing.
|
||||
|
||||
---
|
||||
|
||||
## Directory Convention
|
||||
|
||||
```
|
||||
my-theme/
|
||||
├── src/
|
||||
│ ├── css/
|
||||
│ │ └── main.css # CSS source
|
||||
│ ├── js/
|
||||
│ │ └── main.ts # TypeScript source
|
||||
│ ├── partials/ # Not treated as page entries; referenced via include
|
||||
│ │ ├── layout.html # Shared layout
|
||||
│ │ └── post-card.html # Reusable post card component
|
||||
│ ├── index.html # Home page entry → templates/index.html
|
||||
│ ├── post.html # → templates/post.html
|
||||
│ ├── page.html
|
||||
│ ├── archives.html
|
||||
│ ├── tags.html
|
||||
│ ├── tag.html
|
||||
│ ├── categories.html
|
||||
│ └── category.html
|
||||
├── public/ # Copied directly to templates/assets/ (no Vite processing)
|
||||
├── templates/ # Build output directory (add to .gitignore)
|
||||
├── vite.config.ts
|
||||
├── package.json
|
||||
├── .gitignore
|
||||
├── theme.yaml
|
||||
└── settings.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@halo-dev/vite-plugin-halo-theme": "latest",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### vite.config.ts
|
||||
|
||||
```ts
|
||||
import { defineConfig } from "vite";
|
||||
import { haloThemePlugin } from "@halo-dev/vite-plugin-halo-theme";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [haloThemePlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
### .gitignore
|
||||
|
||||
```gitignore
|
||||
node_modules/
|
||||
templates/
|
||||
!templates/.gitkeep
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Syntax (build-time only — independent of Thymeleaf)
|
||||
|
||||
> **Important**: `include`/`slot` syntax is processed at **Vite build time**; Thymeleaf syntax is processed at **server runtime**. The two systems are fully isolated and can be mixed freely.
|
||||
|
||||
### `<include>` — Import a partial
|
||||
|
||||
```html
|
||||
<include src="layout.html">
|
||||
<main>Page content</main>
|
||||
</include>
|
||||
```
|
||||
|
||||
### Named Slots
|
||||
|
||||
Declare slots in a partial (`src/partials/layout.html`):
|
||||
|
||||
```html
|
||||
<head>
|
||||
<slot name="head">
|
||||
<title th:text="${site.title}">Default title</title>
|
||||
</slot>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
```
|
||||
|
||||
Use in a page:
|
||||
|
||||
```html
|
||||
<include src="layout.html">
|
||||
<template name="head">
|
||||
<title th:text="${post.spec.title}">Post title</title>
|
||||
</template>
|
||||
<article th:utext="${post.content.content}"></article>
|
||||
</include>
|
||||
```
|
||||
|
||||
### Include Path Resolution
|
||||
|
||||
| Syntax | Resolves to |
|
||||
| ------------------- | ----------------------------------- |
|
||||
| `foo.html` | `src/partials/foo.html` (preferred) |
|
||||
| `partials/foo.html` | `src/partials/foo.html` |
|
||||
| `./foo.html` | Relative to current file |
|
||||
| `/foo.html` | Relative to `src/` root |
|
||||
|
||||
### Static Asset Paths
|
||||
|
||||
**All HTML files — both `src/*.html` and `src/partials/*.html` — resolve static asset references relative to `src/`**, regardless of the file's actual location.
|
||||
|
||||
```html
|
||||
<!-- src/partials/layout.html: referencing src/css/main.css -->
|
||||
<link rel="stylesheet" href="./css/main.css" />
|
||||
<!-- ✅ correct (relative to src/) -->
|
||||
<link rel="stylesheet" href="../css/main.css" />
|
||||
<!-- ❌ wrong (relative to file location) -->
|
||||
|
||||
<!-- Same rule in src/index.html -->
|
||||
<script type="module" src="./js/main.ts"></script>
|
||||
<!-- ✅ correct -->
|
||||
```
|
||||
|
||||
Always write asset paths as if the file is in `src/`, even when it is inside `src/partials/`.
|
||||
|
||||
---
|
||||
|
||||
## Full Example: Shared Layout
|
||||
|
||||
### `src/partials/layout.html`
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<slot name="head">
|
||||
<title th:text="${site.title}">Site title</title>
|
||||
</slot>
|
||||
<link rel="stylesheet" href="./css/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a th:href="@{/}" th:text="${site.title}"></a>
|
||||
<nav th:with="menu = ${menuFinder.getPrimary()}">
|
||||
<ul>
|
||||
<li th:each="item : ${menu.menuItems}">
|
||||
<a
|
||||
th:href="${item.status.href}"
|
||||
th:text="${item.status.displayName}"
|
||||
th:target="${item.spec.target?.value}"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<halo:footer />
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### `src/index.html`
|
||||
|
||||
```html
|
||||
<include src="layout.html">
|
||||
<template name="head">
|
||||
<title th:text="${site.title}">Home</title>
|
||||
</template>
|
||||
|
||||
<section>
|
||||
<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>
|
||||
<p th:text="${post.status.excerpt}"></p>
|
||||
</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>
|
||||
</section>
|
||||
</include>
|
||||
```
|
||||
|
||||
### `src/post.html`
|
||||
|
||||
```html
|
||||
<include src="layout.html">
|
||||
<template name="head">
|
||||
<title th:text="${post.spec.title}">Post title</title>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Prev / next navigation -->
|
||||
<div 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
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<div th:if="${haloCommentEnabled}">
|
||||
<halo:comment group="content.halo.run" kind="Post" th:attr="name=${post.metadata.name}" />
|
||||
</div>
|
||||
</article>
|
||||
</include>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TailwindCSS Integration
|
||||
|
||||
```bash
|
||||
pnpm add -D tailwindcss @tailwindcss/vite
|
||||
```
|
||||
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { haloThemePlugin } from "@halo-dev/vite-plugin-halo-theme";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), haloThemePlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
```css
|
||||
/* src/css/main.css */
|
||||
@import "tailwindcss";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ready-to-use Templates
|
||||
|
||||
The skill's `assets/` directory provides two ready-to-use templates:
|
||||
|
||||
- `assets/theme-minimal/` — Zero-build-tool minimal theme; good for quick prototyping or plain HTML/CSS themes
|
||||
- `assets/theme-vite/` — Full Vite project with vite-plugin-halo-theme; recommended as the starting point for new themes
|
||||
|
||||
Usage: copy the directory into `themes/` in your Halo working directory, ensure the folder name matches `metadata.name` in `theme.yaml`, then install and activate in Console.
|
||||
Reference in New Issue
Block a user