1 Commits

Author SHA1 Message Date
351c398553 Merge pull request 'dev: 合并近期新增博客与资源调整' (#10) from dev into main
Reviewed-on: #10
2025-10-09 13:47:22 +08:00
66 changed files with 612 additions and 3518 deletions

5
.gitignore vendored
View File

@@ -1,6 +1,5 @@
# build output
dist/
dist.zip
# generated types
.astro/
@@ -25,6 +24,4 @@ pnpm-debug.log*
.idea/
teaser.pptx
~$teaser.pptx
.claude
~$teaser.pptx

View File

@@ -5,22 +5,16 @@
## dev.2ha.me
### frok by [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)
[![Code License]](LICENSE.md)
[![Code License]](LICENSE)
English | [简体中文](README_zh-Hans.md) | [繁體中文](README_zh-Hant.md)
[dev.2ha.me](https://dev.2ha.me) is a project I worked on while learning [Next.js](https://nextjs.org/). I discovered [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) from the Examples in the [tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) project. Both [dev.2ha.me](https://dev.2ha.me) and [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) use the same architecture, such as [Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), and [shadcn/ui](https://ui.shadcn.com/). On this basis, I modified some APIs to display data from domestic Chinese platforms. The main framework is consistent with the 3.0 version of [enscribe.dev](https://github.com/jktrn/enscribe.dev.git).
[dev.2ha.me](https://dev.2ha.me) 是我在学习[Next.js](https://nextjs.org/)时练手的项目,我从[tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)项目的Examples发现了[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)[dev.2ha.me](https://dev.2ha.me)和[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)使用了相同的架构,如[Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), 和 [shadcn/ui](https://ui.shadcn.com/)。在此基础修改了一些API展示为中国国内的平台数据, 主体框架与[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)的3.0版本一致。
</div>
---
> [!WARNING]
This project does not use i18n.
## 技术栈
## Technology Stack
These are the technology stacks originally used in [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)
这些是原[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)使用到的技术栈
| Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- |
@@ -33,10 +27,9 @@ These are the technology stacks originally used in [enscribe.dev](https://github
| Deployment | [Vercel](https://vercel.com) |
> [!NOTE]
[dev.2ha.me](https://dev.2ha.me) is based on the [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) project. If you want to refer to other sample code repositories or use the components within them, you can check out the Examples in [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) or the [tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) project. I have added support for NetEase Cloud Music and GiteaCalendar on top of [enscribe.dev](https://github.com/jktrn/enscribe.dev.git), replaced the original background image of [enscribe.dev](https://github.com/jktrn/enscribe.dev.git), and modified it to my own preferred style.
> [dev.2ha.me](https://dev.2ha.me) 是基于 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) 这个项目的。如果您想参考其他样例的代码库或者使用其中的组件,可以查看[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)或者[tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)项目的Examples;
> 我在[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)基础上添加了网易云音乐GiteaCalendar的支持替换了原[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)背景图片,修改为我自己喜欢的风格。
| Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- |
@@ -44,34 +37,31 @@ These are the technology stacks originally used in [enscribe.dev](https://github
| API | [githubCalendar](https://github.com/luckykeeper/giteaCalendar) |
---
## 许可
## License
### 原始模板
Original Template
摘自许可证中的 “原始模板许可证 ”部分:
Extract from the "Original Template License" section of the license:
> 本网站基于 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git),而 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) 是基于 [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)的, [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)则衍生自松散的 MIT 许可项目 [trevortylerlee/astro-micro](https://github.com/trevortylerlee/astro-micro)
This website is based on [enscribe.dev](https://github.com/jktrn/enscribe.dev.git), which is based on [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite), and [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite) is derived from the loosely MIT-licensed project [trevortylerlee/astro-micro](https://github.com/trevortylerlee/astro-micro).
[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)的许可可以查看enscribe.dev的[Licence](https://github.com/jktrn/enscribe.dev/blob/main/LICENSE.md)文件
### 站点代码
The license for [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) can be viewed in the [License](https://github.com/jktrn/enscribe.dev/blob/main/LICENSE) file of enscribe.dev.
[![Code License]](LICENSE.md)
> 本许可证特别适用于对 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)(基于[jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)) 模板所做的自定义修改。它并不延伸至原始模板代码,原始模板代码仍使用其原始 MIT 许可。
### Site Code
就本许可而言,“代码”是指网站的软件组件、配置、布局、样式、功能、脚本和其他功能元素,但不包括此存储库中包含的内容呈现脚本(例如 MDX、Markdown、SVG、等媒体资源文件
此存储库中的所有此类代码均根据 Apache License 2.0 获得许可:
[dev.2ha.me](https://dev.2ha.me) © 2024 Jimleerx
本文件遵循 Apache 许可证 2.0 版(简称“许可证”);您不得在未遵守该许可证的情况下使用本文件。您可以访问以下网址获取许可证副本:
http://www.apache.org/licenses/LICENSE-2.0
除非适用法律另有规定或双方以书面形式达成一致,否则根据本许可证分发的软件均按“原样”分发,不附带任何明示或暗示的保证或条件。请参阅许可证,了解本许可证下特定语言的权限和限制规定。
[![Code License]](LICENSE)
> This license specifically applies to custom modifications made to the [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) (based on [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)) template. It does not extend to the original template code, which remains under its original MIT license.
For the purposes of this license, "Code" refers to the software components, configuration, layout, style, functionality, scripts, and other functional elements of the website, but does not include content presentation scripts contained in this repository (such as MDX, Markdown, SVG, and other media resource files).
All such Code in this repository is licensed under the Apache License 2.0: [dev.2ha.me](https://dev.2ha.me) © 2024 iluobei
This document is subject to the Apache License, Version 2.0 (the "License"); you may not use this document except in compliance with the License. You can obtain a copy of the License at the following URL: http://www.apache.org/licenses/LICENSE-2.0
Unless otherwise required by applicable law or agreed in writing by both parties, the software distributed under this license is distributed "as is", without any express or implied warranties or conditions. Please refer to the license for specific permissions and limitations under this license in the applicable language.
### Non-code content
All external resources used are listed on the [dev.2ha.me](https://dev.2ha.me) About page. These resources are sourced from the internet. If used for commercial purposes, permission from the original author is required.
Any content not defined in the above code, including but not limited to MDX blog posts, SVG graphics, personal materials, other images, any written content in any format, and any accompanying documents, are licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0). This means you can use and share this content, but you must provide appropriate attribution, may not use it for commercial purposes, and may not distribute modified versions. For more details, please refer to CC BY-NC-ND 4.0.
Extracted from the [Website Code License](LICENSE#website-code-license) section.
### 非代码内容
使用的外部资源已在[dev.2ha.me](https://dev.2ha.me) 关于页面列出,资源均来自网络,如果是商业使用需得到原作者的授权。
任何未在上述代码中定义的内容,包括但不限于 MDX 博客文章、SVG 图形、个人素材、其他图片、任何格式的书面内容以及任何随附文档,均受知识共享署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0) 许可。这意味着您可以使用和分享这些内容,但必须提供适当的署名,不得将其用于商业用途,也不得分发其修改版本。更多详情,请参阅 CC BY-NC-ND 4.0 。
摘自 [Website Code License](LICENSE.md#website-code-license) 部分:
[Code License]: https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge&logo=github&logoColor=fff

View File

@@ -1,70 +0,0 @@
![Showcase Card](/public/static/showcase-card.png)
<div align="center">
## dev.2ha.me
### frok by [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)
[![Code License]](LICENSE)
[English](README.md) | 简体中文 | [繁體中文](README_zh-Hant.md)
[dev.2ha.me](https://dev.2ha.me) 是我在学习[Next.js](https://nextjs.org/)时练手的项目,我从[tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)项目的Examples发现了[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)[dev.2ha.me](https://dev.2ha.me)和[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)使用了相同的架构,如[Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), 和 [shadcn/ui](https://ui.shadcn.com/)。在此基础修改了一些API展示为中国国内的平台数据, 主体框架与[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)的3.0版本一致。
</div>
---
## 技术栈
这些是原[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)使用到的技术栈
| Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Framework | [Astro](https://astro.build/) |
| Styling | [Tailwind](https://tailwindcss.com) |
| Components | [shadcn/ui](https://ui.shadcn.com/) |
| Content | [MDX](https://mdxjs.com/) |
| Syntax Highlighting | [Shiki](https://github.com/shikijs/shiki) + [rehype-pretty-code](https://rehype-pretty.pages.dev/) |
| Graphics | [Figma](https://www.figma.com/) |
| Deployment | [Vercel](https://vercel.com) |
> [!NOTE]
> [dev.2ha.me](https://dev.2ha.me) 是基于 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) 这个项目的。如果您想参考其他样例的代码库或者使用其中的组件,可以查看[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)或者[tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)项目的Examples;
> 我在[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)基础上添加了网易云音乐GiteaCalendar的支持替换了原[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)背景图片,修改为我自己喜欢的风格。
| Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Components | [MagicUI](magicui.design), [reactbits](https://www.reactbits.dev/), [startlight](https://starlight.astro.build/), [CultUi](https://www.cult-ui.com/) |
| API | [githubCalendar](https://github.com/luckykeeper/giteaCalendar) |
---
## 许可
### 原始模板
摘自许可证中的 “原始模板许可证 ”部分:
> 本网站基于 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git),而 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) 是基于 [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)的, [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)则衍生自松散的 MIT 许可项目 [trevortylerlee/astro-micro](https://github.com/trevortylerlee/astro-micro)
[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)的许可可以查看enscribe.dev的[Licence](https://github.com/jktrn/enscribe.dev/blob/main/LICENSE)文件
### 站点代码
[![Code License]](LICENSE)
> 本许可证特别适用于对 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)(基于[jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)) 模板所做的自定义修改。它并不延伸至原始模板代码,原始模板代码仍使用其原始 MIT 许可。
就本许可而言,“代码”是指网站的软件组件、配置、布局、样式、功能、脚本和其他功能元素,但不包括此存储库中包含的内容呈现脚本(例如 MDX、Markdown、SVG、等媒体资源文件
此存储库中的所有此类代码均根据 Apache License 2.0 获得许可:
[dev.2ha.me](https://dev.2ha.me) © 2024 iluobei
本文件遵循 Apache 许可证 2.0 版(简称“许可证”);您不得在未遵守该许可证的情况下使用本文件。您可以访问以下网址获取许可证副本:
http://www.apache.org/licenses/LICENSE-2.0
除非适用法律另有规定或双方以书面形式达成一致,否则根据本许可证分发的软件均按“原样”分发,不附带任何明示或暗示的保证或条件。请参阅许可证,了解本许可证下特定语言的权限和限制规定。
### 非代码内容
使用的外部资源已在[dev.2ha.me](https://dev.2ha.me) 关于页面列出,资源均来自网络,如果是商业使用需得到原作者的授权。
任何未在上述代码中定义的内容,包括但不限于 MDX 博客文章、SVG 图形、个人素材、其他图片、任何格式的书面内容以及任何随附文档,均受知识共享署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0) 许可。这意味着您可以使用和分享这些内容,但必须提供适当的署名,不得将其用于商业用途,也不得分发其修改版本。更多详情,请参阅 CC BY-NC-ND 4.0 。
摘自 [Website Code License](LICENSE#website-code-license) 部分
[Code License]: https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge&logo=github&logoColor=fff

View File

@@ -1,119 +0,0 @@
![Showcase Card](/public/static/showcase-card.png)
<div align="center">
## dev.2ha.me
### frok by [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)
[![Code License]](LICENSE)
[English](README.md) | [簡體中文](README_zh-Hans.md) | 繁體中文
[dev.2ha.me](https://dev.2ha.me) 是我在學習[Next.js](https://nextjs.org/)時練手的項目,我從[tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)項目的Examples發現了[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)[dev.2ha.me](https://dev.2ha.me)和[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)使用了相同的架構,如[Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), 和 [shadcn/ui](https://ui.shadcn.com/)。在此基礎修改了一些API展示為中國國內的平臺數據, 主體框架與[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)的3.0版本一致。
</div>
---
## 技術棧
這些是原[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)使用到的技術棧
| Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Framework | [Astro](https://astro.build/) |
| Styling | [Tailwind](https://tailwindcss.com) |
| Components | [shadcn/ui](https://ui.shadcn.com/) |
| Content | [MDX](https://mdxjs.com/) |
| Syntax Highlighting | [Shiki](https://github.com/shikijs/shiki) + [rehype-pretty-code](https://rehype-pretty.pages.dev/) |
| Graphics | [Figma](https://www.figma.com/) |
| Deployment | [Vercel](https://vercel.com) |
> [!NOTE]
> [dev.2ha.me](https://dev.2ha.me) 是基於 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) 這個項目的。如果您想參考其他樣例的代碼庫或者使用其中的組件,可以查看[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)或者[tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)項目的Examples;
> 我在[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)基礎上添加了網易雲音樂GiteaCalendar的支持替換了原[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)背景圖片,修改為我自己喜歡的風格。
| Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Components | [MagicUI](magicui.design), [reactbits](https://www.reactbits.dev/), [startlight](https://starlight.astro.build/), [CultUi](https://www.cult-ui.com/) |
| API | [githubCalendar](https://github.com/luckykeeper/giteaCalendar) |
---
## 許可
### 原始模板
摘自許可證中的 「原始模板許可證 」部分:
> 本網站基於 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git),而 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git) 是基於 [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)的, [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)則衍生自松散的 MIT 許可項目 [trevortylerlee/astro-micro](https://github.com/trevortylerlee/astro-micro)
[enscribe.dev](https://github.com/jktrn/enscribe.dev.git)的許可可以查看enscribe.dev的[Licence](https://github.com/jktrn/enscribe.dev/blob/main/LICENSE)文件
### 站點代碼
[![Code License]](LICENSE)
> 本許可證特別適用於對 [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)(基於[jktrn/astro-erudite](https://github.com/jktrn/astro-erudite)) 模板所做的自定義修改。它並不延伸至原始模板代碼,原始模板代碼仍使用其原始 MIT 許可。
就本許可而言,「代碼」是指網站的軟件組件、配置、布局、樣式、功能、腳本和其他功能元素,但不包括此存儲庫中包含的內容呈現腳本(例如 MDX、Markdown、SVG、等媒體資源文件
此存儲庫中的所有此類代碼均根據 Apache License 2.0 獲得許可:
[dev.2ha.me](https://dev.2ha.me) © 2024 iluobei
本文件遵循 Apache 許可證 2.0 版(簡稱「許可證」);您不得在未遵守該許可證的情況下使用本文件。您可以訪問以下網址獲取許可證副本:
http://www.apache.org/licenses/LICENSE-2.0
除非適用法律另有規定或雙方以書面形式達成一致,否則根據本許可證分發的軟件均按「原樣」分發,不附帶任何明示或暗示的保證或條件。請參閱許可證,了解本許可證下特定語言的權限和限製規定。
### 非代碼內容
使用的外部資源已在[dev.2ha.me](https://dev.2ha.me) 關於頁面列出,資源均來自網絡,如果是商業使用需得到原作者的授權。
任何未在上述代碼中定義的內容,包括但不限於 MDX 博客文章、SVG 圖形、個人素材、其他圖片、任何格式的書面內容以及任何隨附文檔,均受知識共享署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0) 許可。這意味著您可以使用和分享這些內容,但必須提供適當的署名,不得將其用於商業用途,也不得分發其修改版本。更多詳情,請參閱 CC BY-NC-ND 4.0 。
摘自 [Website Code License](LICENSE#website-code-license) 部分
[Code License]: https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge&logo=github&logoColor=fff

BIN
dist.zip Normal file

Binary file not shown.

1008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,7 @@
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}",
"postinstall": "patch-package"
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
@@ -61,7 +60,6 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"patch-package": "^8.0.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.13.0",
"prettier-plugin-astro-organize-imports": "^0.4.11",

View File

@@ -1,14 +0,0 @@
diff --git a/node_modules/@shikijs/transformers/dist/index.mjs b/node_modules/@shikijs/transformers/dist/index.mjs
index db4db63..c7f3ea2 100644
--- a/node_modules/@shikijs/transformers/dist/index.mjs
+++ b/node_modules/@shikijs/transformers/dist/index.mjs
@@ -410,6 +410,9 @@ function transformerRenderWhitespace(options = {}) {
return token;
if (position === "trailing" && index !== last)
return token;
+ if (token.children.length === 0) {
+ return token;
+ }
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 959 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 KiB

View File

@@ -37,7 +37,7 @@ const socialLinks: SocialLink[] = [
---
<div
class="border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="overflow-hidden rounded-xl border p-4 transition-colors duration-300 ease-in-out has-[a:hover]:bg-secondary/50"
>
<div class="flex flex-wrap gap-4">
<Link
@@ -69,7 +69,7 @@ const socialLinks: SocialLink[] = [
<p class="text-sm text-muted-foreground">{bio}</p>
<p class="text-sm text-muted-foreground flex items-center">
这是一个开源的站点,你可以点击
<Link href="https://github.com/iluobei/dev.2ha.me" target="_blank">
<Link href="https://code.2ha.me/dev.2ha.me/dev.2ha.me" target="_blank">
<svg
class="size-4 ml-1 mr-1"
style="filter: invert(100%);"

View File

@@ -1,192 +0,0 @@
---
import { Badge as BadgeComponent } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
interface Props {
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
className?: string
children?: any
text?: string
showIcon?: boolean
}
const {
variant = 'secondary',
className = '',
children,
text,
showIcon = true,
} = Astro.props
const categoryMappings = [
{
keywords: ['crypto'],
style: {
color:
'bg-yellow-50 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-200',
icon: 'lucide:key',
},
},
{
keywords: ['web'],
style: {
color: 'bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-200',
icon: 'lucide:globe',
},
},
{
keywords: ['reverse', 'rev'],
style: {
color:
'bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-200',
icon: 'lucide:rotate-ccw',
},
},
{
keywords: ['pwn', 'binary exploitation'],
style: {
color: 'bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-200',
icon: 'lucide:zap',
},
},
{
keywords: ['misc'],
style: {
color:
'bg-stone-50 text-stone-700 dark:bg-stone-950/30 dark:text-stone-200',
icon: 'lucide:puzzle',
},
},
{
keywords: ['forensic'],
style: {
color:
'bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-200',
icon: 'lucide:search',
},
},
{
keywords: ['osint'],
style: {
color:
'bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-200',
icon: 'lucide:eye',
},
},
{
keywords: ['blockchain'],
style: {
color: 'bg-teal-50 text-teal-700 dark:bg-teal-950/30 dark:text-teal-200',
icon: 'lucide:link',
},
},
{
keywords: ['ppc', 'programming'],
style: {
color:
'bg-indigo-50 text-indigo-700 dark:bg-indigo-950/30 dark:text-indigo-200',
icon: 'lucide:code',
},
},
{
keywords: ['commercial'],
style: {
color: 'text-foreground bg-foreground/10',
icon: 'lucide:building-2',
},
},
{
keywords: ['personal'],
style: {
// color: 'bg-sky-50 text-sky-700 dark:bg-sky-950/30 dark:text-sky-200',
icon: 'lucide:user',
},
},
{
keywords: ['open-source'],
style: {
// color: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-200',
icon: 'lucide:git-branch',
},
},
{
keywords: ['freelance'],
style: {
// color: 'bg-teal-50 text-teal-700 dark:bg-teal-950/30 dark:text-teal-200',
icon: 'lucide:briefcase',
},
},
{
keywords: ['team'],
style: {
// color: 'bg-violet-50 text-violet-700 dark:bg-violet-950/30 dark:text-violet-200',
icon: 'lucide:users',
},
},
{
keywords: ['contract'],
style: {
// color: 'bg-rose-50 text-rose-700 dark:bg-rose-950/30 dark:text-rose-200',
icon: 'lucide:file-text',
},
},
{
keywords: ['astro'],
style: {
// color: 'bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-200',
icon: 'lucide:rocket',
},
},
{
keywords: ['shopify'],
style: {
// color: 'bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-200',
icon: 'lucide:shopping-bag',
},
},
{
keywords: ['html'],
style: {
// color: 'bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-200',
icon: 'lucide:code-2',
},
},
{
keywords: ['figma'],
style: {
// color: 'bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-200',
icon: 'lucide:palette',
},
},
]
const getCategoryStyle = (content: string) => {
const lowerContent = content.toLowerCase()
const match = categoryMappings.find((category) =>
category.keywords.some((keyword) => lowerContent.includes(keyword)),
)
return match?.style || null
}
const content = text || (typeof children === 'string' ? children : '')
const categoryStyle = getCategoryStyle(content)
---
<BadgeComponent
variant={categoryStyle ? 'secondary' : variant}
className={cn(categoryStyle?.color, className)}
client:load
>
{
showIcon && (
<Icon
name={categoryStyle ? categoryStyle.icon : 'lucide:tag'}
class="size-3"
/>
)
}
<slot>{text}</slot>
</BadgeComponent>

View File

@@ -24,14 +24,13 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === entry.data.title)
const totalBody = [entry.body!, ...subposts.map((p) => p.body!)]
.map(stripCodeBlocks)
.join('')
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
const readTime = readingTime(wordCount)
const readTime = readingTime(totalBody)
const authors = await parseAuthors(entry.data.authors ?? [])
---
<div
class="not-prose border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
>
<Link
href={`/${entry.collection}/${entry.id}`}

View File

@@ -63,15 +63,15 @@ import Link from './Link.astro'
// ]
const SHORTS_CUTS_CLASS_NAMES: string[] = [
'z-10 max-w max-h mt-[2.6em] inline-block sm:mt-[2.35em]',
'z-10 max-w max-h ml-[0.1em] mt-[-4.7em] inline-block sm:ml-[0em] sm:mt-[-3.4em]',
'z-10 max-w max-h ml-[-0.2em] mt-[3.0em] inline-block sm:ml-[0em] sm:mt-[2.5em]',
'z-10 max-w max-h mt-[-5.2em] inline-block sm:mt-[-3em]',
'z-10 max-w max-h ml-[0.1em] mt-[-3.7em] inline-block sm:ml-[0em] sm:mt-[-3.4em]',
'z-10 max-w max-h ml-[-0.1em] mt-[2.5em] inline-block sm:ml-[0em] sm:mt-[2.5em]',
'z-10 max-w max-h mt-[-4em] inline-block sm:mt-[-3em]',
'z-10 max-w max-h mt-[0.1em] inline-block sm:mt-[0.6em]',
'z-10 max-w max-h ml-[0.15em] mt-[-7.5em] inline-block sm:ml-[0em] sm:mt-[-4.8em]',
'z-10 max-w max-h ml-[-0.15em] mt-[0.2em] inline-block sm:mt-[0.9em] sm:ml-[0em]',
'z-10 max-w max-h mt-[-7.5em] inline-block sm:mt-[-4.8em]',
'z-10 max-w max-h mt-[1.88em] inline-block sm:mt-[1em]',
'z-10 max-w max-h ml-[0.15em] mt-[-5.8em] inline-block sm:ml-[0em] sm:mt-[-4em]'
'z-10 max-w max-h ml-[0.1em] mt-[-6.5em] inline-block sm:ml-[0em] sm:mt-[-5.3em]',
'z-10 max-w max-h mt-[0.2em] inline-block sm:mt-[0.6em]',
'z-10 max-w max-h mt-[-6.5em] inline-block sm:mt-[-5.3em]',
'z-10 max-w max-h mt-[1.49em] inline-block sm:mt-[1em]',
'z-10 max-w max-h mt-[-5.2em] inline-block sm:mt-[-4.3em]'
]
---
@@ -87,23 +87,14 @@ const SHORTS_CUTS_CLASS_NAMES: string[] = [
target="_blank"
>
<div
class="bottom-0 right-0 w-fit items-end rounded-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_30%,hsl(var(--border)))] bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:scale-110 hover:border-[color-mix(in_srgb,hsl(var(--primary))_50%,hsl(var(--border)))]"
class="bottom-0 right-0 w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary"
>
{item.icon.startsWith('mdi:') ? (
<Icon
style="color: rgb(233, 211, 182);"
name={item.icon}
class="z-[1] size-1/2 size-8 text-primary sm:size-8"
aria-hidden="true"
/>
) : (
<img
src={item.icon}
alt={item.title}
class="z-[1] size-1/2 size-8 sm:size-8"
aria-hidden="true"
/>
)}
<Icon
style="color: rgb(233, 211, 182);"
name={item.icon}
class="z-[1] size-1/2 size-8 text-primary sm:size-8"
aria-hidden="true"
/>
</div>
</Link>
</div>

View File

@@ -1,406 +0,0 @@
---
import { DEV_LINKS } from '@/consts'
import { Icon } from 'astro-icon/components'
// ------------- Hex math helpers (ported from react-hexgrid) -------------
type Point = { x: number; y: number }
type HexCoord = { q: number; r: number; s: number }
type Orientation = {
f0: number
f1: number
f2: number
f3: number
b0: number
b1: number
b2: number
b3: number
startAngle: number
}
type LayoutDimension = {
size: Point
spacing: number
origin: Point
orientation: Orientation
}
const SQRT3 = Math.sqrt(3)
const ORIENTATION_FLAT: Orientation = {
f0: 3 / 2,
f1: 0,
f2: SQRT3 / 2,
f3: SQRT3,
b0: 2 / 3,
b1: 0,
b2: -1 / 3,
b3: SQRT3 / 3,
startAngle: 0,
}
const BASE_HEX_SIZE = 104
const HEX_SIZE = 168
const BORDER_WIDTH = 24
const layout: LayoutDimension = {
size: { x: HEX_SIZE, y: HEX_SIZE },
spacing: 1,
origin: { x: 0, y: 0 },
orientation: ORIENTATION_FLAT,
}
const GRID_WIDTH = 5
const GRID_HEIGHT = 5
function calculatePolygonPoints(size: number, flat = false) {
const angleOffset = flat ? 0 : Math.PI / 6
const corners: Point[] = []
for (let i = 0; i < 6; i++) {
const angle = (2 * Math.PI * i) / 6 + angleOffset
corners.push({
x: size * Math.cos(angle),
y: size * Math.sin(angle),
})
}
return corners.map((corner) => `${corner.x.toFixed(2)},${corner.y.toFixed(2)}`).join(' ')
}
function coordKey(hex: HexCoord) {
return `${hex.q},${hex.r},${hex.s}`
}
function hexToPixel(hex: HexCoord, layout: LayoutDimension): Point {
const { orientation: M, size, spacing, origin } = layout
let x = (M.f0 * hex.q + M.f1 * hex.r) * size.x
let y = (M.f2 * hex.q + M.f3 * hex.r) * size.y
x *= spacing
y *= spacing
return { x: x + origin.x, y: y + origin.y }
}
function rectangle(mapWidth: number, mapHeight: number): HexCoord[] {
const cells: HexCoord[] = []
for (let r = 0; r < mapHeight; r++) {
const offset = Math.floor(r / 2)
for (let q = -offset; q < mapWidth - offset; q++) {
cells.push({ q, r, s: -q - r })
}
}
return cells
}
function hexagon(mapRadius: number): HexCoord[] {
const cells: HexCoord[] = []
for (let q = -mapRadius; q <= mapRadius; q++) {
const r1 = Math.max(-mapRadius, -q - mapRadius)
const r2 = Math.min(mapRadius, -q + mapRadius)
for (let r = r1; r <= r2; r++) {
cells.push({ q, r, s: -q - r })
}
}
return cells
}
// 根据cells的下标与对应值控制显示在哪些格子里
const ACTIVE_COORDS: HexCoord[] = [
// { q: -1, r: 0, s: 1 },
// { q: 0, r: 0, s: 0 },
// { q: 1, r: 0, s: -1 },
{ q: 2, r: 0, s: -2 },
{ q: 3, r: 0, s: -3 },
{ q: 0, r: 1, s: -1 },
{ q: 1, r: 1, s: -2 },
{ q: 2, r: 1, s: -3 },
{ q: 0, r: 2, s: -2 },
{ q: 1, r: 2, s: -3 },
{ q: 2, r: 2, s: -4 },
{ q: 0, r: 3, s: -3 },
{ q: 1, r: 3, s: -4 },
{ q: 2, r: 3, s: -5 },
]
const gridCoords = rectangle(GRID_WIDTH, GRID_HEIGHT)
const gridKeyMap = new Map(gridCoords.map((hex) => [coordKey(hex), hex]))
const preferredCoords = ACTIVE_COORDS.map((hex) => gridKeyMap.get(coordKey(hex))).filter(
Boolean,
) as HexCoord[]
const preferredKeySet = new Set(preferredCoords.map((hex) => coordKey(hex)))
const remainingGridCoords = gridCoords.filter((hex) => !preferredKeySet.has(coordKey(hex)))
let orderedActiveSlots = [...preferredCoords, ...remainingGridCoords]
if (DEV_LINKS.length > orderedActiveSlots.length) {
const usedKeys = new Set(orderedActiveSlots.map((hex) => coordKey(hex)))
let radius = Math.max(GRID_WIDTH, GRID_HEIGHT)
while (orderedActiveSlots.length < DEV_LINKS.length) {
radius += 1
for (const hex of hexagon(radius)) {
const key = coordKey(hex)
if (usedKeys.has(key)) continue
orderedActiveSlots.push(hex)
usedKeys.add(key)
if (orderedActiveSlots.length === DEV_LINKS.length) break
}
}
}
const assignedCoords = orderedActiveSlots.slice(0, DEV_LINKS.length)
const coordToLink = new Map<string, (typeof DEV_LINKS)[number]>()
assignedCoords.forEach((hex, index) => {
const link = DEV_LINKS[index]
if (!hex || !link) return
coordToLink.set(coordKey(hex), link)
})
const renderCoordMap = new Map<string, HexCoord>()
gridCoords.forEach((hex) => renderCoordMap.set(coordKey(hex), hex))
assignedCoords.forEach((hex) => {
if (!hex) return
renderCoordMap.set(coordKey(hex), hex)
})
const renderCoords = Array.from(renderCoordMap.values())
const hexagonPoints = calculatePolygonPoints(layout.size.x, true)
// const circleRadius = layout.size.x * 0.84
const circleRadius = 80
const baseCells = renderCoords.map((hex) => ({
hex,
link: coordToLink.get(coordKey(hex)) ?? null,
center: hexToPixel(hex, layout),
}))
const xs = baseCells.map((cell) => cell.center.x)
const ys = baseCells.map((cell) => cell.center.y)
const padding = layout.size.x * 0.45
const minX = Math.min(...xs) - padding
const maxX = Math.max(...xs) + padding
const minY = Math.min(...ys) - padding
const maxY = Math.max(...ys) + padding
const sizeScale = HEX_SIZE / BASE_HEX_SIZE
const rawWidth = maxX - minX
const rawHeight = maxY - minY
const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
const viewWidth = rawWidth / sizeScale
const viewHeight = rawHeight / sizeScale
const viewMinX = centerX - viewWidth / 2
const viewMinY = centerY - viewHeight / 2
const viewBox = `${viewMinX} ${viewMinY} ${viewWidth} ${viewHeight}`
const iconClipId = 'hexIconClip'
const cells = baseCells.map((cell) => ({
...cell,
showContent: Boolean(cell.link),
}))
---
<svg
class="hex-grid hex-grid--polygons"
width="100%"
height="100%"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid slice"
>
<g class="hex-grid__group">
{cells.map(({ link, center }, index) => {
const isGhost = !link
return (
<polygon
class={`hexagon-bg ${isGhost ? 'hexagon-bg--ghost' : ''}`}
points={hexagonPoints}
fill="transparent"
stroke="#252525"
stroke-width={BORDER_WIDTH}
transform={`translate(${center.x}, ${center.y})`}
data-index={index}
/>
)
})}
</g>
</svg>
<svg
class="hex-grid hex-grid--icons"
width="100%"
height="100%"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid slice"
>
<defs>
<clipPath id={iconClipId} clipPathUnits="userSpaceOnUse">
<circle cx="0" cy="0" r={circleRadius} />
</clipPath>
</defs>
<g class="hex-grid__group">
{cells.map(({ link, center, showContent }, index) => {
if (!link || !showContent) return null
return (
<g
class="hexagon-wrapper"
transform={`translate(${center.x}, ${center.y})`}
data-index={index}
>
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
>
<title>{link.title}</title>
<g class="icon-group">
<circle
r={circleRadius}
fill="hsl(var(--secondary))"
fill-opacity="0.45"
stroke="hsl(var(--border))"
stroke-width={Math.max(4, BORDER_WIDTH * 0.32)}
class="icon-circle z-10"
/>
{link.icon.startsWith('mdi:') ? (
<foreignObject
x={-(layout.size.x * 0.7)}
y={-(layout.size.y * 0.7)}
width={layout.size.x * 1.4}
height={layout.size.y * 1.4}
clip-path={`url(#${iconClipId})`}
class="icon-foreign"
>
<div class="icon-container">
<Icon
name={link.icon}
class="hexagon-icon"
style="color: rgb(233, 211, 182);"
aria-hidden="true"
/>
</div>
</foreignObject>
) : (
<image
href={link.icon}
x={-layout.size.x * 0.3}
y={-layout.size.y * 0.3}
width={layout.size.x * 0.6}
height={layout.size.y * 0.6}
clip-path={`url(#${iconClipId})`}
class="hexagon-image"
/>
)}
</g>
</a>
</g>
)
})}
</g>
</svg>
<style>
.hex-grid {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.hex-grid--polygons {
z-index: 1;
}
.hex-grid--icons {
z-index: 20;
}
.hex-grid__group {
transform-origin: center;
transform: translateX(-114px) translateY(60px) scale(1.08);
}
.hexagon-wrapper {
cursor: pointer;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.hexagon-wrapper a {
outline: none;
-webkit-tap-highlight-color: transparent;
pointer-events: auto;
cursor: pointer;
}
.hexagon-wrapper a title {
pointer-events: none;
}
.hexagon-bg,
.icon-circle,
.icon-group {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.icon-circle {
stroke-width: 4px;
transform-origin: center;
transform-box: fill-box;
}
.icon-group {
transform-origin: center;
transform-box: fill-box;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.45));
}
.hexagon-wrapper:hover .icon-circle {
fill-opacity: 0.85;
stroke: hsl(var(--primary));
stroke-width: 4px;
transform: scale(1.08);
}
.hexagon-wrapper:hover .icon-group {
transform: rotate(12deg);
transform-origin: center;
}
.hexagon-bg--ghost {
stroke-dasharray: none;
}
.icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 9999px;
}
.hexagon-icon {
width: 100% !important;
height: 100% !important;
max-width: 88px;
max-height: 88px;
}
.hexagon-image {
object-fit: contain;
}
.icon-foreign {
overflow: visible;
}
:global(.hexagon-icon) {
color: rgb(233, 211, 182) !important;
}
</style>

View File

@@ -4,30 +4,27 @@ import Link from '@/components/Link.astro'
import MobileMenu from '@/components/ui/mobile-menu'
import { NAV_LINKS } from '@/consts'
import { Image } from 'astro:assets'
import { Icon } from 'astro-icon/components'
import logo from '../../public/static/logo.webp'
---
<header
class="sticky top-0 z-[99] bg-background/50 backdrop-blur-md"
class="sticky top-0 z-20 bg-background/50 backdrop-blur-md"
transition:persist
>
<Container>
<div class="flex flex-wrap items-center justify-between gap-4 py-4 ">
<Link href="/" class="logo-link">
<Image src={logo} alt="Logo" class="size-8 -scale-x-100 border-2 border-[color:rgba(241,140,110,0.4)] shadow-[4px_4px_0_rgba(0,0,0,0.2)] transition-all duration-300" />
<Link href="/">
<Image src={logo} alt="Logo" class="size-8 -scale-x-100" />
</Link>
<div class="flex items-center gap-2 sm:gap-4">
<nav class="hidden items-center gap-2 text-base sm:flex sm:gap-3">
<nav class="hidden items-center gap-4 text-base sm:flex sm:gap-6">
{
NAV_LINKS.map((item) => (
<Link
href={item.href}
class="pixel-button inline-flex items-center justify-center gap-2 px-4 py-2 h-9 text-sm font-semibold uppercase tracking-wider bg-background/75 text-foreground border-[color:rgba(137,110,96,0.45)] hover:bg-primary/20 hover:text-primary hover:border-[color:rgba(217,119,87,0.65)] dark:bg-input/30 dark:border-[color:rgba(255,255,255,0.18)] dark:hover:bg-primary/25 dark:hover:text-primary dark:hover:border-[color:rgba(241,140,110,0.75)] transition-all"
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
>
<Icon name={item.icon} class="size-[18px] shrink-0" />
<span>{item.label}</span>
{item.label}
</Link>
))
}
@@ -38,31 +35,3 @@ import logo from '../../public/static/logo.webp'
</div>
</Container>
</header>
<style>
.logo-link:hover img {
border-color: hsl(var(--primary));
box-shadow:
0 0 12px rgba(241, 140, 110, 0.6),
0 0 24px rgba(241, 140, 110, 0.4),
0 0 36px rgba(241, 140, 110, 0.2),
4px 4px 0 rgba(0, 0, 0, 0.2);
transform: scale(1.05);
}
nav :global(.pixel-button:hover) {
box-shadow:
0 0 12px rgba(241, 140, 110, 0.5),
0 0 24px rgba(241, 140, 110, 0.3),
0 0 36px rgba(241, 140, 110, 0.15),
4px 4px 0 rgba(0, 0, 0, 0.2) !important;
}
.dark nav :global(.pixel-button:hover) {
box-shadow:
0 0 12px rgba(241, 140, 110, 0.5),
0 0 24px rgba(241, 140, 110, 0.3),
0 0 36px rgba(241, 140, 110, 0.15),
4px 4px 0 rgba(0, 0, 0, 0.4) !important;
}
</style>

View File

@@ -1,36 +0,0 @@
---
import { SITE } from '@/consts'
interface Props {
title?: string
description?: string
noindex?: boolean
}
const {
title = SITE.TITLE,
description = SITE.DESCRIPTION,
noindex = false,
} = Astro.props
const image = new URL('/static/twitter-card.png', Astro.site)
---
<title>{`${title} | ${SITE.TITLE}`}</title>
<meta name="description" content={description} />
<link rel="canonical" href={SITE.SITEURL} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<meta property="og:title" content={`${title} | ${SITE.TITLE}`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:alt" content={title} />
<meta property="og:type" content="website" />
<meta property="og:locale" content={SITE.locale} />
<meta property="og:site_name" content={SITE.TITLE} />
<meta property="og:url" content={Astro.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:card" content="summary_large_image" />

View File

@@ -1,13 +1,10 @@
---
import Badge from '@/components/Badge.astro'
import Link from '@/components/Link.astro'
import { Separator } from '@/components/ui/separator'
import { extractDomain, formatMonthYear } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
import { Badge } from '@/components/ui/badge'
import { Image } from 'astro:assets'
import type { CollectionEntry } from 'astro:content'
interface Props {
type Props = {
project: CollectionEntry<'projects'>
}
@@ -15,63 +12,30 @@ const { project } = Astro.props
---
<div
class="border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50"
>
<Link
href={project.data.link}
class="flex flex-col gap-4 sm:flex-row"
target="_blank"
rel="noopener noreferrer"
>
{
project.data.image && (
<div class="max-w-[225px] sm:flex-shrink-0">
<Image
src={project.data.image}
alt={project.data.name}
width={1200}
height={630}
class="object-cover"
/>
</div>
)
}
<div class="grow">
<h3 class="mb-1 text-lg font-semibold">
{project.data.name}
</h3>
<p class="text-muted-foreground mb-2 text-sm">
<Link href={project.data.link} class="block">
<Image
src={project.data.image}
alt={project.data.name}
width={400}
height={200}
class="w-full object-cover"
/>
<div class="p-4">
<h3 class="mb-2 text-lg font-semibold">{project.data.name}</h3>
<p class="mb-4 text-sm text-muted-foreground">
{project.data.description}
</p>
{
project.data.startDate && (
<div class="text-muted-foreground/70 mb-2 flex flex-wrap items-center gap-x-2 text-xs">
<span class="flex items-center gap-x-1.5">
<Icon name="lucide:calendar" class="size-3" />
<span>
{formatMonthYear(project.data.startDate)}
{project.data.endDate
? ` → ${formatMonthYear(project.data.endDate)}`
: ' → Present'}
</span>
</span>
<Separator orientation="vertical" className="h-4!" />
<span class="flex items-center gap-x-1">
<Icon name="lucide:external-link" class="size-3" />
<span>{extractDomain(project.data.link)}</span>
</span>
</div>
)
}
{
project.data.tags && (
<div class="flex flex-wrap gap-2">
{project.data.tags.map((tag: string) => (
<Badge text={tag} />
))}
</div>
)
}
<div class="flex flex-wrap gap-2">
{
project.data.tags.map((tag) => (
<Badge variant="secondary" showHash={false}>
{tag}
</Badge>
))
}
</div>
</div>
</Link>
</div>

View File

@@ -22,7 +22,6 @@ const iconMap = {
Gitea: 'mdi:git',
Maven: 'mavenrepo',
DevIntro: 'lucide:info',
HubProxy: 'lucide:rocket',
}
const getSocialLink = ({ href, label }: SocialLink) => ({

View File

@@ -1,46 +1,47 @@
import AvatarComponent from '@/components/ui/avatar'
const AuthorPresence = () => {
return (
<div className="relative overflow-hidden sm:aspect-square select-none" style={{ cursor: 'default' }}>
<div className="relative overflow-hidden sm:aspect-square">
<div className="grid size-full grid-rows-4">
<div className="bg-secondary/50"></div>
<div className="row-span-3 flex flex-col gap-3 p-3">
<div className="flex justify-between">
<div className="flex justify-between gap-x-1">
<div className="relative">
<AvatarComponent
src="/static/avatar.png"
src="/static/avatar.webp"
alt="Avatar"
fallback="e"
className="-mt-[4.5rem] aspect-square size-24 rounded-full"
/>
</div>
{/* <div className="flex items-center rounded-xl bg-secondary/50 px-2">
<div className="flex items-center rounded-xl bg-secondary/50 px-2">
<img
src="https://contrib.rocks/image?repo=iluobei/dev.2ha.me"
src="/static/images/badges.png"
alt="Discord Badges"
width={104}
height={24}
className="grayscale"
/>
</div> */}
</div>
</div>
<div className="flex flex-col items-start gap-y-1 rounded-xl bg-secondary/50 p-3 select-none">
<span className="text-base leading-none select-none cursor-default">(🥕)</span>
<span className="text-xs leading-none text-muted-foreground select-none cursor-default">
<div className="flex flex-col gap-y-1 rounded-xl bg-secondary/50 p-3">
<span className="text-base leading-none">jimlee</span>
<span className="text-xs leading-none text-muted-foreground">
li@2ha.me
</span>
</div>
<div className="flex grow rounded-xl bg-secondary/50 px-3 py-2 select-none">
<div className="flex size-full flex-col items-center justify-center gap-1 select-none">
<div className="flex grow rounded-xl bg-secondary/50 px-3 py-2">
<div className="flex size-full flex-col items-center justify-center gap-1">
<img
src="/static/images/lieflat.svg"
alt="No Status Image"
width={64}
height={64}
className="h-full rounded-lg select-none"
className="h-full rounded-lg"
/>
<div className="text-[10px] text-muted-foreground select-none cursor-default">
<div className="text-[10px] text-muted-foreground">
</div>
</div>

View File

@@ -55,12 +55,12 @@ const WakatimeGraph = ({ }: Props) => {
useEffect(() => {
setLanguages([
{ name: 'java', hours: 1009, fill: 'hsl(var(--chart-1))' },
{ name: 'javascript', hours: 586, fill: 'hsl(var(--chart-2))' },
{ name: 'typescript', hours: 519, fill: 'hsl(var(--chart-4))' },
{ name: 'react', hours: 442, fill: 'hsl(var(--chart-5))' },
{ name: 'kotlin', hours: 405, fill: 'hsl(var(--chart-3))' },
{ name: 'go', hours: 301, fill: 'hsl(var(--chart-6))' },
{ name: 'python', hours: 177, fill: 'hsl(var(--chart-7))' },
{ name: 'kotlin', hours: 346, fill: 'hsl(var(--chart-2))' },
{ name: 'javascript', hours: 311, fill: 'hsl(var(--chart-3))' },
{ name: 'typescript', hours: 287, fill: 'hsl(var(--chart-4))' },
{ name: 'python', hours: 120, fill: 'hsl(var(--chart-5))' },
{ name: 'react', hours: 85, fill: 'hsl(var(--chart-6))' },
{ name: 'go', hours: 9, fill: 'hsl(var(--chart-7))' },
])
setIsLoading(false)
}, [])

View File

@@ -16,7 +16,7 @@ async function fetchCalendarData(): Promise<ApiResponse> {
const data: ApiResponse | ApiErrorResponse = await response.json()
if (!response.ok) {
throw Error(
`Fetching Gitea(code.2ha.me) contribution data for luobei failed, see https://github.com/luckykeeper/giteaCalendar.`,
`Fetching Gitea(code.2ha.me) contribution data for jimlee failed, see https://github.com/luckykeeper/giteaCalendar.`,
)
}

View File

@@ -62,13 +62,6 @@ const Music163Player = () => {
})
}, [])
// Set audio volume to 50%
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = 0.5
}
}, [displayData])
if (isLoading) {
return (
<div className="relative flex h-full w-full flex-col justify-between rounded-3xl p-6">
@@ -109,7 +102,7 @@ const Music163Player = () => {
alt="Album art"
width={128}
height={128}
className="mb-2 w-[55%] border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))]"
className="mb-2 w-[55%] rounded-xl border border-border"
/>
</a>
<div className="flex min-w-0 flex-1 flex-col justify-end overflow-hidden">
@@ -148,7 +141,7 @@ const Music163Player = () => {
<span className="w-[85%] truncate text-xs text-muted-foreground">
<span className="font-semibold text-secondary-foreground">
<div>
<audio ref={audioRef}>
<audio ref={audioRef} >
<source src={outerurl} type="audio/mp3" />
<source src={backupurl} type="audio/mp3" />
</audio>

View File

@@ -58,7 +58,7 @@ const RandomAnimeBackground = () => {
// }
return (
<video ref={videoRef} width="100" height="100" className="no-repeat relative w-full justify-center object-cover"
<video ref={videoRef} width="100" height="100" className="no-repeat relative w-full justify-center rounded-[1.4em] object-cover"
src={'/static/anime-bg/' + videoBackgrounds[index]}
style={{ display: isLoading ? 'none' : 'block' }}
onEnded={handleVideoEnded}

View File

@@ -24,7 +24,7 @@ export default function GradientText({
// console.log(children)
return (
<div
className={`relative mx-auto ml-2 flex max-w-fit cursor-pointer flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 ${className}`}
className={`relative mx-auto ml-2 md:max-h-[1em] flex max-w-fit cursor-pointer flex-row items-center justify-center overflow-hidden rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 ${className}`}
>
{showBorder && (
<div

View File

@@ -280,7 +280,7 @@ const LetterGlitch = ({
}, [glitchSpeed, smooth])
return (
<div className="relative h-full w-full overflow-hidden bg-black">
<div className="relative h-full w-full overflow-hidden rounded-3xl bg-black">
<canvas ref={canvasRef} className="block h-full w-full" />
{outerVignette && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-full bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]"></div>

View File

@@ -5,25 +5,25 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-2',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] active:[box-shadow:1px_1px_0_rgba(0,0,0,0.16)] active:translate-y-0 dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)] dark:hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.55)] dark:active:[box-shadow:1px_1px_0_rgba(0,0,0,0.35)]',
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)]',
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border-input bg-background [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:bg-accent hover:text-accent-foreground hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)] dark:hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.55)]',
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:bg-secondary/80 hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)]',
ghost: 'hover:bg-accent hover:text-accent-foreground border-transparent',
link: 'text-primary underline-offset-4 hover:underline border-transparent',
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-8',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},

View File

@@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className,
)}
@@ -48,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden border-2 border-[color:rgba(241,140,110,0.22)] bg-popover p-1 text-popover-foreground [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
@@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden border-2 border-[color:rgba(241,140,110,0.22)] bg-popover p-1 text-popover-foreground [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)]',
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
@@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className,
)}
@@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
checked={checked}
@@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}

View File

@@ -7,20 +7,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { NAV_LINKS } from '@/consts'
import { Menu, Home, FileText, FolderGit2, BadgeInfo } from 'lucide-react'
const getIconComponent = (iconName?: string) => {
if (!iconName) return Home
const iconMap: Record<string, any> = {
'lucide:home': Home,
'lucide:file-text': FileText,
'lucide:folder-git-2': FolderGit2,
'lucide:badge-info': BadgeInfo,
}
return iconMap[iconName] || Home
}
import { Menu } from 'lucide-react'
const MobileMenu = () => {
const [isOpen, setIsOpen] = useState(false)
@@ -46,29 +33,25 @@ const MobileMenu = () => {
<Button
variant="outline"
size="icon"
className="sm:hidden pixel-button h-9 w-9 bg-background/75 border-[color:rgba(137,110,96,0.45)] hover:bg-primary/20 hover:text-primary hover:border-[color:rgba(217,119,87,0.65)] dark:bg-input/30 dark:border-[color:rgba(255,255,255,0.18)] dark:hover:bg-primary/25 dark:hover:text-primary dark:hover:border-[color:rgba(241,140,110,0.75)]"
className="sm:hidden"
title="Menu"
>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-background min-w-[140px] w-auto p-0">
{NAV_LINKS.map((item) => {
const Icon = getIconComponent(item.icon)
return (
<DropdownMenuItem key={item.href} asChild className="p-0">
<a
href={item.href}
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 text-base font-semibold uppercase tracking-wider cursor-pointer hover:bg-primary/20 hover:text-primary focus:bg-primary/20 focus:text-primary dark:hover:bg-primary/25 dark:hover:text-primary border-b border-[color:rgba(241,140,110,0.15)] last:border-b-0 transition-colors"
onClick={() => setIsOpen(false)}
>
<Icon className="size-[18px] shrink-0" />
<span>{item.label}</span>
</a>
</DropdownMenuItem>
)
})}
<DropdownMenuContent align="end" className="bg-background">
{NAV_LINKS.map((item) => (
<DropdownMenuItem key={item.href} asChild>
<a
href={item.href}
className="w-full text-lg font-medium capitalize"
onClick={() => setIsOpen(false)}
>
{item.label}
</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)

View File

@@ -5,13 +5,11 @@ export type Site = {
NUM_POSTS_ON_HOMEPAGE: number
POSTS_PER_PAGE: number
SITEURL: string
locale: string
}
export type Link = {
href: string
label: string
icon?: string
}
export type DevLink = {
@@ -28,20 +26,19 @@ export const SITE: Site = {
NUM_POSTS_ON_HOMEPAGE: 2,
POSTS_PER_PAGE: 4,
SITEURL: 'https://dev.2ha.me',
locale: 'zh-CN',
}
export const NAV_LINKS: Link[] = [
{ href: '/', label: '主页', icon: 'lucide:home' },
{ href: '/blog', label: '博客', icon: 'lucide:file-text' },
// { href: '/tags', label: '标签' },
{ href: '/projects', label: '项目', icon: 'lucide:folder-git-2'},
{ href: '/authors', label: '关于', icon: 'lucide:badge-info' },
{ href: '/', label: '主页' },
{ href: '/blog', label: '博客' },
{ href: '/tags', label: '标签' },
// { href: '/authors', label: '作者' },
{ href: '/authors', label: '关于' },
]
export const SOCIAL_LINKS: Link[] = [
{ href: 'https://github.com/iluobei', label: 'GitHub' },
{ href: 'https://1ms.cc', label: 'HubProxy' },
{ href: 'https://github.com/jimleerx', label: 'GitHub' },
{ href: 'https://maven.2ha.me', label: 'Maven' },
{ href: 'https://code.2ha.me', label: 'Gitea' },
{ href: 'li@2ha.me', label: 'Email' },
]
@@ -54,10 +51,10 @@ export const DEV_LINKS: DevLink[] = [
icon: 'mdi:git',
},
{
href: 'https://img.2ha.me',
href: 'https://maven.2ha.me',
label: 'Nexus',
title: '图床',
icon: 'mdi:image-multiple',
title: 'Maven仓库',
icon: 'mdi:chart-doughnut-variant',
},
{
href: 'https://dms.2ha.me',
@@ -69,13 +66,13 @@ export const DEV_LINKS: DevLink[] = [
href: 'https://p.2ha.me',
label: 'Zfile',
title: '网盘',
icon: 'mdi:harddisk',
icon: 'mdi:cloud-arrow-up',
},
{
href: 'https://tz.2ha.me',
label: 'VPS Monitor',
title: '探针',
icon: 'mdi:chart-areaspline',
href: 'https://photo.2ha.me',
label: 'immich',
title: '相册',
icon: 'mdi:camera',
},
{
href: 'https://f.2ha.me',
@@ -83,12 +80,7 @@ export const DEV_LINKS: DevLink[] = [
title: '文件服务器',
icon: 'mdi:file-arrow-up-down-outline',
},
{
href: 'https://status.2ha.me',
label: 'Domain Status',
title: '站点检测',
icon: 'mdi:cloud-check'
},
{ href: 'https://v.2ha.me', label: 'Emby', title: 'Emby', icon: 'mdi:emby' },
{
href: 'https://in.2ha.me',
label: '2ha.me statistics',
@@ -96,16 +88,15 @@ export const DEV_LINKS: DevLink[] = [
icon: 'mdi:sun-azimuth',
},
{
href: 'https://miaomiaowu.net',
label: '妙妙屋',
title: '个人Clash订阅管理工具',
icon: '/static/mmw.svg',
// icon: 'mdi:cat',
href: 'https://mp.2ha.me',
label: 'MoviePilot',
title: '媒体订阅工具',
icon: 'mdi:youtube-creator-studio',
},
{
href: 'https://1ms.cc',
label: 'hubproxy',
title: 'GitHub&DockerHub加速',
href: 'https://g.2ha.me',
label: 'GHProxy',
title: 'GitHub代理',
icon: 'mdi:rocket-launch-outline',
},
]

View File

@@ -11,7 +11,6 @@ const blog = defineCollection({
image: image().optional(),
tags: z.array(z.string()).optional(),
authors: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().optional(),
hidden: z.boolean().optional(),
parentTitle: z.string().optional(),
@@ -67,9 +66,6 @@ const projects = defineCollection({
tags: z.array(z.string()),
image: image(),
link: z.string().url(),
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
order: z.number().optional(),
}),
})

View File

@@ -1,12 +1,12 @@
---
name: '胡萝北(🥕)'
name: 'jimlee'
pronouns: 'he/him'
avatar: 'https://dev.2ha.me/static/avatar_256.webp'
bio: 'd(-_-)b'
website: 'https://dev.2ha.me'
devintro: 'https://code.2ha.me/luobei'
devintro: 'https://code.2ha.me/jimlee'
gitea: 'https://code.2ha.me/dev.2ha.me'
# twitter: 'https://twitter.com/enscry'
github: 'https://github.com/iluobei'
github: 'https://github.com/jimleerx'
mail: 'li@2ha.me'
---

View File

@@ -4,7 +4,7 @@ description: '在debian上根据需要添加nginx模块编译自定义的ngin
date: 2025-04-11
tags: ['nginx', 'debian', 'build']
image: 'assets/nginx.svg'
authors: ['胡萝北(🥕)']
authors: ['jimlee']
---
## 在debian上安装nginx
@@ -15,15 +15,15 @@ debian默认软件库的nginx没有fancy-index模块, fancy-index是一个html
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev
```
## 2. 下载并解压nginx源码
官网查看最新版本(当前20251111为1.29.3)
官网查看最新版本(当前20250111为1.26.3)
https://nginx.org/en/download.html
```shellscript title="shell"
wget https://nginx.org/download/nginx-1.29.3.tar.gz
tar -xf nginx-1.29.3.tar.gz
wget https://nginx.org/download/nginx-1.26.3.tar.gz
tar -xf nginx-1.26.3.tar.gz
```
## 3. 下载并解压nginx-fancyindex模块
```shellscript title="shell"
cd nginx-1.29.3
cd nginx-1.26.3
wget https://github.com/aperezdc/ngx-fancyindex/releases/download/v0.5.2/ngx-fancyindex-0.5.2.tar.xz
tar -xf ngx-fancyindex-0.5.2.tar.xz
```
@@ -34,15 +34,11 @@ mkdir -p /var/cache/nginx
mkdir -p /var/log/nginx
```
NGINX_ROOT_PATH = nginx的安装目录, 需要替换成你想要安装的目录
```
export NGINX_ROOT_PATH=/usr/local/nginx
```
```shellscript
./configure --add-module=./ngx-fancyindex-0.5.2 \
--prefix=${NGINX_ROOT_PATH} \
--user=$(whoami) \
--group=$(id -Gn) \
--user=jimlee \
--group=jimlee \
--sbin-path=${NGINX_ROOT_PATH}/sbin/nginx \
--conf-path=${NGINX_ROOT_PATH}/nginx.conf \
--error-log-path=/var/log/nginx/error.log \

View File

@@ -4,7 +4,7 @@ description: '常用的kotlin, python, shell, regex等代码块合集'
date: 2025-04-10
tags: ['kotlin', 'python', 'regex', 'linux', 'shell', 'javascript', 'nginx']
image: 'assets/commoncodebanner.webp'
authors: ['胡萝北(🥕)']
authors: ['jimlee']
---
## kotlin

View File

@@ -4,7 +4,7 @@ description: '在dsm7.2上破解emby套件, 输入任意激活码即可获取小
date: 2024-12-31
tags: ['emby', 'synology', 'crack']
image: 'assets/emby_banner.png'
authors: ['胡萝北(🥕)']
authors: ['jimlee']
---
import { Icon } from 'astro-icon/components'
import Link from '@/components/Link.astro'

View File

@@ -4,7 +4,7 @@ description: 'debian安装fail2ban保护nginx服务器阻止暴力破解和
date: 2025-10-06
tags: ['debian', 'nginx', 'fail2ban', 'regex', 'shell']
image: 'assets/nginxfail2ban.png'
authors: ['胡萝北(🥕)']
authors: ['jimlee']
---

View File

@@ -4,7 +4,7 @@ description: '1. shikijs重复依赖打包失败; 2. 使用Rehype Pretty Code/Co
date: 2025-05-12
tags: ['typescript', 'astro', 'shiki']
image: 'assets/shikicodecopybutton.png'
authors: ['胡萝北(🥕)']
authors: ['jimlee']
---
import FileTree from '@/components/starlight/FileTree.astro'

View File

@@ -1,9 +0,0 @@
---
---
<h3 class="not-prose text-lg font-medium mb-1">
Some work I&rsquo;ve done <span class="text-muted-foreground">ヽ(o^ ^o)ノ</span>
</h3>
<span class="not-prose text-muted-foreground text-xs">
Last updated: 2025-08-11
</span>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 KiB

View File

@@ -1,8 +0,0 @@
---
name: '妙妙屋(个人clash订阅管理工具)'
description: '妙妙屋是一个功能强大的Clash订阅管理平台帮助您轻松管理订阅、节点和用户。'
tags: ['open-source', 'personal', 'clash', 'substore']
image: 'assets/mmw.png'
link: 'https://miaomiaowu.net'
startDate: '2025-10-10'
---

View File

@@ -14,14 +14,14 @@ const { title, description, image } = Astro.props
---
<!doctype html>
<html lang="zh" class="dark">
<html lang="zh">
<head>
<Head
title={`${title} | ${SITE.TITLE}`}
description={description}
image={image}
/>
<script is:inline async defer src="https://in.2ha.me/script.js" data-website-id="34634aec-34a9-4ef4-9a8f-08ee96699a84"></script>
<script defer src="https://in.2ha.me/script.js" data-website-id="34634aec-34a9-4ef4-9a8f-08ee96699a84"></script>
</head>
<body>
<div

View File

@@ -1,325 +0,0 @@
import { getCollection, render, type CollectionEntry } from 'astro:content'
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
return await getCollection('authors')
}
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllPostsAndSubposts(): Promise<
CollectionEntry<'blog'>[]
> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllTags(): Promise<Map<string, number>> {
const posts = await getAllPosts()
return posts.reduce((acc, post) => {
post.data.tags?.forEach((tag) => {
acc.set(tag, (acc.get(tag) || 0) + 1)
})
return acc
}, new Map<string, number>())
}
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
const projects = await getCollection('projects')
return projects.sort((a, b) => {
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
if (orderA !== orderB) {
return orderA - orderB
}
const dateA = a.data.startDate?.getTime() || 0
const dateB = b.data.startDate?.getTime() || 0
return dateB - dateA
})
}
export async function getAdjacentPosts(currentId: string): Promise<{
newer: CollectionEntry<'blog'> | null
older: CollectionEntry<'blog'> | null
parent: CollectionEntry<'blog'> | null
}> {
const allPosts = await getAllPosts()
if (isSubpost(currentId)) {
const parentId = getParentId(currentId)
const allPosts = await getAllPosts()
const parent = allPosts.find((post) => post.id === parentId) || null
const posts = await getCollection('blog')
const subposts = posts
.filter(
(post) =>
isSubpost(post.id) &&
getParentId(post.id) === parentId &&
!post.data.draft,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
const currentIndex = subposts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent }
}
return {
newer:
currentIndex < subposts.length - 1 ? subposts[currentIndex + 1] : null,
older: currentIndex > 0 ? subposts[currentIndex - 1] : null,
parent,
}
}
const parentPosts = allPosts.filter((post) => !isSubpost(post.id))
const currentIndex = parentPosts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent: null }
}
return {
newer: currentIndex > 0 ? parentPosts[currentIndex - 1] : null,
older:
currentIndex < parentPosts.length - 1
? parentPosts[currentIndex + 1]
: null,
parent: null,
}
}
export async function getPostsByAuthor(
authorId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.authors?.includes(authorId))
}
export async function getPostsByTag(
tag: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.tags?.includes(tag))
}
export async function getRecentPosts(
count: number,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.slice(0, count)
}
export async function getSortedTags(): Promise<
{ tag: string; count: number }[]
> {
const tagCounts = await getAllTags()
return [...tagCounts.entries()]
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => {
const countDiff = b.count - a.count
return countDiff !== 0 ? countDiff : a.tag.localeCompare(b.tag)
})
}
export function getParentId(subpostId: string): string {
return subpostId.split('/')[0]
}
export async function getSubpostsForParent(
parentId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter(
(post) =>
!post.data.draft &&
isSubpost(post.id) &&
getParentId(post.id) === parentId,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
}
export function groupPostsByYear(
posts: CollectionEntry<'blog'>[],
): Record<string, CollectionEntry<'blog'>[]> {
return posts.reduce(
(acc: Record<string, CollectionEntry<'blog'>[]>, post) => {
const year = post.data.date.getFullYear().toString()
;(acc[year] ??= []).push(post)
return acc
},
{},
)
}
export function groupProjectsByYear(
projects: CollectionEntry<'projects'>[],
): Record<string, CollectionEntry<'projects'>[]> {
return projects.reduce(
(acc: Record<string, CollectionEntry<'projects'>[]>, project) => {
// Use startDate for grouping, fallback to current year if no date
const year = project.data.startDate
? project.data.startDate.getFullYear().toString()
: new Date().getFullYear().toString()
;(acc[year] ??= []).push(project)
return acc
},
{},
)
}
export async function hasSubposts(postId: string): Promise<boolean> {
const subposts = await getSubpostsForParent(postId)
return subposts.length > 0
}
export function isSubpost(postId: string): boolean {
return postId.includes('/')
}
export async function getParentPost(
subpostId: string,
): Promise<CollectionEntry<'blog'> | null> {
if (!isSubpost(subpostId)) {
return null
}
const parentId = getParentId(subpostId)
const allPosts = await getAllPosts()
return allPosts.find((post) => post.id === parentId) || null
}
export async function parseAuthors(authorIds: string[] = []) {
if (!authorIds.length) return []
const allAuthors = await getAllAuthors()
const authorMap = new Map(allAuthors.map((author) => [author.id, author]))
return authorIds.map((id) => {
const author = authorMap.get(id)
return {
id,
name: author?.data?.name || id,
avatar: author?.data?.avatar || '/static/logo.png',
isRegistered: !!author,
}
})
}
export async function getPostById(
postId: string,
): Promise<CollectionEntry<'blog'> | null> {
const allPosts = await getAllPostsAndSubposts()
return allPosts.find((post) => post.id === postId) || null
}
export async function getSubpostCount(parentId: string): Promise<number> {
const subposts = await getSubpostsForParent(parentId)
return subposts.length
}
export async function getCombinedReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
let totalWords = calculateWordCountFromHtml(post.body)
if (!isSubpost(postId)) {
const subposts = await getSubpostsForParent(postId)
for (const subpost of subposts) {
totalWords += calculateWordCountFromHtml(subpost.body)
}
}
return readingTime(totalWords)
}
export async function getPostReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
const wordCount = calculateWordCountFromHtml(post.body)
return readingTime(wordCount)
}
export type TOCHeading = {
slug: string
text: string
depth: number
isSubpostTitle?: boolean
}
export type TOCSection = {
type: 'parent' | 'subpost'
title: string
headings: TOCHeading[]
subpostId?: string
}
export async function getTOCSections(postId: string): Promise<TOCSection[]> {
const post = await getPostById(postId)
if (!post) return []
const parentId = isSubpost(postId) ? getParentId(postId) : postId
const parentPost = isSubpost(postId) ? await getPostById(parentId) : post
if (!parentPost) return []
const sections: TOCSection[] = []
const { headings: parentHeadings } = await render(parentPost)
if (parentHeadings.length > 0) {
sections.push({
type: 'parent',
title: 'Overview',
headings: parentHeadings.map((heading) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
})),
})
}
const subposts = await getSubpostsForParent(parentId)
for (const subpost of subposts) {
const { headings: subpostHeadings } = await render(subpost)
if (subpostHeadings.length > 0) {
sections.push({
type: 'subpost',
title: subpost.data.title,
headings: subpostHeadings.map((heading, index) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
isSubpostTitle: index === 0,
})),
subpostId: subpost.id,
})
}
}
return sections
}

View File

@@ -30,6 +30,6 @@ export function transformerNotationSkip(
return false
},
undefined, // remove empty lines
) as ShikiTransformer
)
}

View File

@@ -13,36 +13,13 @@ export function formatDate(date: Date) {
}).format(date)
}
export function formatMonthYear(date: Date) {
return Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
}).format(date)
}
export function calculateWordCountFromHtml(
html: string | null | undefined,
): number {
if (!html) return 0
export function readingTime(html: string) {
const textOnly = html.replace(/<[^>]+>/g, '')
return textOnly.split(/\s+/).filter(Boolean).length
}
export function readingTime(wordCount: number): string {
const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200))
const wordCount = textOnly.split(/\s+/).length
const readingTimeMinutes = (wordCount / 200 + 1).toFixed()
return `${readingTimeMinutes} min read`
}
export function getHeadingMargin(depth: number): string {
const margins: Record<number, string> = {
3: 'ml-4',
4: 'ml-8',
5: 'ml-12',
6: 'ml-16',
}
return margins[depth] || ''
}
export function getElapsedTime(unixTimestamp: number): string {
const createdAt = new Date(unixTimestamp)
const now = new Date()
@@ -56,12 +33,3 @@ export function getElapsedTime(unixTimestamp: number): string {
.toString()
.padStart(2, '0')}:${seconds.toString().padStart(2, '0')} elapsed`
}
export function extractDomain(url: string): string {
try {
const domain = new URL(url).hostname
return domain.startsWith('www.') ? domain.slice(4) : domain
} catch {
return url
}
}

View File

@@ -61,8 +61,7 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === post.data.title)
const totalBody = [post.body!, ...subposts.map((p) => p.body!)]
.map(stripCodeBlocks)
.join('')
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
const readTime = readingTime(wordCount)
const readTime = readingTime(totalBody)
---
<Layout

View File

@@ -57,24 +57,35 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
---
<Layout title="Blog" description="Blog">
<!-- Fixed Sidebar -->
<aside
class="hidden xl:block fixed top-1/2 -translate-y-1/2 w-[240px] px-4 py-3 rounded-sm bg-background/80 backdrop-blur-sm border-2 border-[color:rgba(241,140,110,0.22)] shadow-[4px_4px_0_rgba(0,0,0,0.22)] dark:shadow-[4px_4px_0_rgba(0,0,0,0.65)] z-10"
>
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: 'Blog', href: '/blog', icon: 'lucide:archive' },
{ label: `Page ${page.currentPage}`, icon: 'lucide:folder-open' },
]}
/>
<div
class="sm:none hidden h-full max-h-screen min-w-[280px] max-w-[280px] flex-wrap overflow-auto rounded-sm bg-gray-50 pt-5 shadow-md lg:contents dark:bg-gray-900/70 dark:shadow-gray-800/40"
>
<div
class="absolute left-4 top-[50%] px-4 py-2"
style="transform: translateY(-50%);"
>
<Link
href={`/blog`}
class="block mb-2 font-bold uppercase text-primary hover:text-primary/80 transition-colors"
class="hover:text-primary-500 dark:hover:text-primary-500 font-bold uppercase"
>
All Posts
</Link>
<ul class="space-y-0.5">
<ul>
{
sortedTags.map((t) => {
return (
<li value={t}>
<li value={t} class="my-0">
<Link
href={`/tags/${slug(t)}`}
class="block px-2 py-1.5 text-sm font-medium uppercase text-foreground/60 hover:text-primary hover:bg-primary/10 rounded transition-colors"
class="hover:text-primary-500 dark:hover:text-primary-500 px-3 py-2 text-sm font-medium uppercase text-gray-400 dark:text-gray-300"
aria-label={`View posts tagged ${t}`}
>
{`${t} (${tagCounts[t]})`}
@@ -84,25 +95,22 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
})
}
</ul>
<div class="mt-2 pt-2 border-t border-[color:rgba(241,140,110,0.15)]">
<a
aria-label="View all blog categories"
class="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
href="/tags/"
<span
class="ms-auto inline-flex h-6 items-center text-base sm:text-end"
>
View all →
</a>
<a
aria-label="View all blog categories"
class="sm:hover:text-accent-two font-medium text-accent"
style="color: #e9d3b6; font-size: 12px;"
href="/tags/"
>
View all →
</a>
</span>
</div>
</aside>
</div>
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: 'Blog', href: '/blog', icon: 'lucide:archive' },
{ label: `Page ${page.currentPage}`, icon: 'lucide:folder-open' },
]}
/>
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
{
years.map((year) => (
<section class="flex flex-col gap-y-4">
@@ -117,31 +125,13 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
</section>
))
}
</div>
</div>
<PaginationComponent
<PaginationComponent
currentPage={page.currentPage}
totalPages={page.lastPage}
baseUrl="/blog/"
client:load
/>
/>
</Container>
</Layout>
<style>
ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

View File

@@ -12,8 +12,7 @@ import { getCollection } from 'astro:content'
import GiteaCalendar from '@/components/custom/GiteaCalendar'
import Music163Player from '@/components/custom/Music163Player'
import RandomAnimeBackground from '@/components/custom/RandomAnimeBackgrounds'
// import DevShortCuts from '@/components/DevShortCuts.astro'
import DevShortcutsHexagon from '@/components/DevShortcutsHexagon.astro'
import DevShortCuts from '@/components/DevShortCuts.astro'
const latestPost = await getCollection('blog').then((posts: any[]) =>
posts
@@ -29,11 +28,11 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
<Layout title="主页" description={SITE.DESCRIPTION}>
<section
class="mx-auto grid w-full grid-cols-1 gap-4 px-4 [grid-template-areas:'a'_'d'_'b'_'e'_'g'_'f'_'j'_'i'_'k'] [grid-template-rows:repeat(9,auto)] *:border-2 *:border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] *:bg-secondary/25 *:bg-cover *:bg-center *:bg-no-repeat *:overflow-hidden *:w-full *:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] *:transition-all *:duration-200 sm:max-w-screen-sm sm:grid-cols-2 sm:[grid-template-areas:'a_a'_'b_d'_'e_e'_'j_g'_'f_f'_'i_k'] sm:[grid-template-rows:repeat(6,300px)] sm:*:max-w-[615px] xl:max-w-screen-xl xl:grid-cols-4 xl:[grid-template-areas:'a_a_b_g'_'d_e_e_i'_'k_j_f_f'] xl:[grid-template-rows:repeat(3,300px)] xl:*:max-w-none dark:*:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] xl:[&:hover:has(>.has-overlay:hover)>.first>.overlay]:opacity-100 xl:[&>*:not(.first):hover_.overlay]:opacity-100"
class="mx-auto grid max-w-[375px] grid-cols-2 gap-4 px-4 [grid-template-areas:'a_a'_'a_a'_'b_b'_'b_b'_'e_e'_'h_i'_'h_c'_'k_c'_'d_d'_'d_d'_'g_g'_'g_g'_'f_f'_'j_j'_'j_j'] *:rounded-3xl *:border *:bg-secondary/25 *:bg-cover *:bg-center *:bg-no-repeat sm:max-w-screen-sm sm:[grid-template-areas:'a_a'_'b_d'_'e_e'_'j_g'_'h_i'_'h_c'_'k_c'_'f_f'] xl:max-w-screen-xl xl:grid-cols-4 xl:[grid-template-areas:'a_a_b_c'_'d_e_e_c'_'h_f_f_g'_'h_i_j_k'] xl:[&:hover:has(>.has-overlay:hover)>.first>.overlay]:opacity-100 xl:[&:hover>*:not(.first):hover_.overlay]:opacity-100"
aria-label="Personal information and activity grid"
>
<div
class="first flex flex-row xl:max-h-[298px] xl:min-w-[615px] grow-[0] justify-center aspect-square bg-[url('/static/loading.gif')] border bg-cover bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="first flex flex-row xl:max-h-[298px] grow-[0] justify-center aspect-square rounded-3xl bg-[url('/static/loading.gif')] border bg-cover bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto"
role="img"
aria-label="Introduction"
@@ -43,29 +42,41 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
<div
class="has-overlay relative grid aspect-square grid-cols-4 grid-rows-3 items-center
justify-center [grid-area:b] short-cuts-template hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
justify-center bg-[url('/static/honeycomb.webp')] [grid-area:b] short-cuts-template"
style="grid-template-columns: 2.85fr 2.95fr 2.9fr 1.3fr;grid-template-rows: 4.7fr 3.4fr 2.8fr;"
aria-label="Developer Stack Shortcuts"
>
<img
class="overlay absolute bottom-0 right-0 no-repeat max-w-[28%] object-contain z-[88]"
src="/static/images/shortcuts-bg-mini.png"
class="overlay absolute no-repeat w-full max-w-fit justify-center rounded-3xl object-cover z-9"
src="/static/images/shortcuts-bg.png"
/>
<!-- <DevStackIconsCloud client:load/> -->
<!-- <DevShortCuts /> -->
<DevShortcutsHexagon />
<DevShortCuts />
</div>
<div class="relative overflow-hidden [grid-area:d] aspect-square hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<div class="aspect-[1/2.1] [grid-area:c] bg-[url('/static/images/ump45.png')] xl:aspect-auto bg-color" aria-hidden="true">
<!-- <MagnetLines
client:load
rows={9}
columns={4}
containerSize="100%"
lineColor="#e9d3b6"
lineWidth="0.8vmin"
lineHeight="4vmin"
baseAngle={0}
/> -->
</div>
<div class="relative overflow-hidden [grid-area:d] sm:aspect-square min-h-[300px]">
<AuthorPresence client:load />
</div>
<div
class="has-overlay min-h-[300px] relative flex grid aspect-[6/5] grid-rows-2 items-start overflow-hidden p-1 [grid-area:e] sm:aspect-[2.1/1] sm:items-center xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="has-overlay min-h-[300px] relative flex grid aspect-[6/5] grid-rows-2 items-start overflow-hidden p-1 [grid-area:e] sm:aspect-[2.1/1] sm:items-center xl:aspect-auto"
style="grid-template-rows: 9fr 1fr; "
>
<div
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/lastblogbg-sm.webp')] bg-cover bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/lastblogbg-sm.webp')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
>
</div>
{
@@ -78,7 +89,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
alt={`Featured image for the latest post: ${latestPost.data.title}`}
width={477}
height={251}
class="w-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] sm:w-[82%]"
class="w-full rounded-2xl border border-border sm:w-[82%] "
/>
</Link>
@@ -98,7 +109,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
aria-label={`Read latest blog post: ${latestPost.data.title}`}
title={`Read latest blog post: ${latestPost.data.title}`}
>
<div class="absolute top-0 right-0 m-3 flex w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary z-10">
<div class="absolute top-0 right-0 m-3 flex w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary">
<Icon name="lucide:move-up-right" size={16} />
</div>
</Link>
@@ -108,30 +119,36 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</div>
<div
class="has-overlay xl:min-w-[615px] relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="has-overlay relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto"
>
<div
class="overlay absolute inset-0 z-[1] size-full bg-[url('/static/images/contributions-square.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/contributions.png')] xl:opacity-100"
class="overlay absolute inset-0 z-[1] size-full rounded-3xl bg-[url('/static/images/contributions-square.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/contributions.png')] xl:opacity-100"
>
</div>
<GiteaCalendar client:load />
</div>
<div
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none"
>
<div
class="overlay absolute inset-0 z-0 size-full bg-[url('/static/images/music163.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
class="overlay absolute inset-0 z-0 size-full rounded-3xl bg-[url('/static/images/music163.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
>
</div>
<Music163Player client:load />
</div>
<div
class="has-overlay relative flex aspect-square items-center justify-center [grid-area:i] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
class="aspect-[1/2.1] bg-[url('/static/images/ump9.png')] [grid-area:h] xl:aspect-auto bg-color"
aria-hidden="true"
>
</div>
<div
class="has-overlay relative flex aspect-square items-center justify-center [grid-area:i] "
>
<div
class="overlay absolute inset-0 size-full bg-[url('/static/images/github.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/github.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
>
</div>
<Icon
@@ -153,12 +170,12 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</Link>
</div>
<div class="has-overlay aspect-square [grid-area:j] bg-[url('/static/images/waketime.png')] bg-cover bg-center bg-no-repeat hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<div class="has-overlay aspect-square [grid-area:j] bg-[url('/static/images/waketime.png')] bg-cover bg-center bg-no-repeat ">
<WakatimeGraph omitLanguages={['Markdown', 'JSON']} client:load />
</div>
<!-- 字符滚动 -->
<div class="aspect-square [grid-area:k] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<div class="aspect-square [grid-area:k]">
<LetterGlitch
client:load
glitchColors={['#de17a5', '#5617de', '#e9d3b6']}
@@ -171,21 +188,3 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</div>
</section>
</Layout>
<style>
section > div:hover {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark section > div:hover {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

View File

@@ -1,57 +0,0 @@
---
import Breadcrumbs from '@/components/Breadcrumbs.astro'
import Container from '@/components/Container.astro'
import PageHead from '@/components/PageHead.astro'
import ProjectCard from '@/components/ProjectCard.astro'
import Layout from '@/layouts/Layout.astro'
import { getAllProjects, groupProjectsByYear } from '@/lib/data-utils'
const projects = await getAllProjects()
const projectsByYear = groupProjectsByYear(projects)
const years = Object.keys(projectsByYear).sort(
(a, b) => parseInt(b) - parseInt(a),
)
---
<Layout title="项目" description="dev.2ha.me的项目">
<PageHead slot="head" title="Project" />
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs items={[{ label: 'Project', icon: 'lucide:briefcase' }]} />
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
{
years.map((year) => (
<section class="flex flex-col gap-y-4">
<div class="font-semibold">{year}</div>
<ul class="not-prose flex flex-col gap-4">
{projectsByYear[year].map((project) => (
<li>
<ProjectCard project={project} />
</li>
))}
</ul>
</section>
))
}
</div>
</Container>
</Layout>
<style>
ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

View File

@@ -1,16 +1,12 @@
@import './pixel-components.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'OPlusSans3-Medium';
src: url('/fonts/OPlusSans3-Medium.woff2') format('woff2'),
url('/fonts/OPlusSans3-Medium.woff') format('woff');
font-weight: 500;
font-family: 'haipaiqiangdiaosenxiyuan';
src: url('/fonts/haipaiqiangdiaosenxiyuan.woff');
font-weight: 100 800;
font-style: normal;
font-display: swap;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@@ -36,117 +32,100 @@
@layer base {
:root {
/* MiaoMiaoWu Brand Colors */
--brand-50: #fef5f2;
--brand-100: #fde8e2;
--brand-200: #fbd4c9;
--brand-300: #f7b5a3;
--brand-400: #f18c6e;
--brand-500: #d97757;
--brand-600: #c55438;
--brand-700: #a4432d;
--brand-800: #873829;
--brand-900: #713128;
/* Semantic Colors - Light Mode Default */
--background: 60 20% 99%;
--foreground: 16 62% 15%;
--primary: 15 66% 59%; /* #d97757 橙色 */
--primary-foreground: 33 100% 99%;
--secondary: 22 64% 93%;
--secondary-foreground: 15 60% 27%;
--muted: 30 43% 94%;
--muted-foreground: 15 25% 51%;
--accent: 15 62% 78%; /* #f7b5a3 */
--accent-foreground: 16 73% 11%;
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 34.12deg 53.68% 81.37%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 72% 51%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 15 30% 55% / 0.38;
--ring: 15 66% 59% / 0.6;
--input: 15 30% 55% / 0.45;
--radius: 0; /* 无圆角! */
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 15 66% 59%;
--chart-2: 15 71% 63%;
--chart-3: 45 97% 63%;
--chart-4: 203 92% 70%;
--chart-5: 186 78% 54%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 60 20% 99%;
--card-foreground: 16 68% 13%;
--popover: 60 20% 99%;
--popover-foreground: 16 62% 14%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
.light {
/* Same as :root for light mode */
--background: 60 20% 99%;
--foreground: 16 62% 15%;
--primary: 15 66% 59%;
--primary-foreground: 33 100% 99%;
--secondary: 22 64% 93%;
--secondary-foreground: 15 60% 27%;
--muted: 30 43% 94%;
--muted-foreground: 15 25% 51%;
--accent: 15 62% 78%;
--accent-foreground: 16 73% 11%;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 72% 51%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 15 30% 55% / 0.38;
--ring: 15 66% 59% / 0.6;
--input: 15 30% 55% / 0.45;
--radius: 0;
--border: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 15 66% 59%;
--chart-2: 15 71% 63%;
--chart-3: 45 97% 63%;
--chart-4: 203 92% 70%;
--chart-5: 186 78% 54%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 60 20% 99%;
--card-foreground: 16 68% 13%;
--popover: 60 20% 99%;
--popover-foreground: 16 62% 14%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
}
.dark {
/* Dark Mode - MiaoMiaoWu Style */
--background: 222 29% 8%; /* #10131c 深蓝灰 */
--foreground: 33 83% 97%; /* #f9f4f1 暖白 */
--primary: 15 71% 69%; /* #f18c6e 亮橙色 */
--primary-foreground: 16 65% 13%; /* #30160f 深色 */
--secondary: 225 23% 13%; /* #1d2232 深蓝灰 */
--secondary-foreground: 22 64% 93%;
--muted: 224 24% 12%; /* #1c2131 深蓝 */
--muted-foreground: 15 25% 75%; /* #cfb8af 浅灰棕 */
--accent: 15 71% 69% / 0.22; /* 半透明橙 */
--accent-foreground: 22 100% 93%; /* #ffe5da */
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 70% 68%; /* #f87171 亮红 */
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 100% / 0.08; /* 半透明白 */
--ring: 15 71% 69% / 0.45;
--input: 0 0% 100% / 0.12;
--radius: 0;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 15 71% 69%;
--chart-2: 199 89% 61%;
--chart-3: 271 70% 67%;
--chart-4: 45 98% 54%;
--chart-5: 186 78% 54%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 222 29% 8%;
--card-foreground: 22 75% 94%;
--popover: 222 29% 8%;
--popover-foreground: 33 83% 97%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
*,
@@ -157,7 +136,6 @@
html {
color-scheme: dark;
overflow-y: scroll;
@apply bg-background text-foreground forced-color-adjust-none;
::-webkit-scrollbar-corner {
@@ -326,109 +304,159 @@
0px 4px 4px hsla(0, 0%, 0%, 0.16), 0px 4px 2px hsla(0, 0%, 0%, 0.04);
}
@layer components {
article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words;
@apply prose-p:break-words;
@apply prose-a:!text-primary prose-a:!decoration-primary/50 prose-a:!underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-inherit;
@apply prose-blockquote:!not-italic prose-blockquote:!text-muted-foreground;
@apply prose-pre:!px-0;
@apply prose-img:mx-auto prose-img:rounded-xl prose-img:border;
@apply prose-table:mx-auto prose-table:block prose-table:max-w-fit prose-table:overflow-x-auto prose-td:break-words;
@apply sm:prose-table:mx-0 sm:prose-table:table sm:prose-table:max-w-none;
/* Fixing Katex display */
.katex-display {
@apply overflow-x-auto overflow-y-hidden py-4;
}
/* Fixing Katex fractions */
.frac-line {
@apply border-foreground;
}
/* Removes background from <mark> elements */
mark {
@apply bg-transparent;
}
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span {
color: var(--shiki-dark);
}
.dark & span {
color: var(--shiki-dark);
}
}
/* Inline code */
:not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium before:!content-none after:!content-none;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply break-words rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {
@apply mt-0 rounded-t-none;
}
}
/* Shadcn-like scrollbar */
pre::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
pre::-webkit-scrollbar-track {
@apply bg-transparent;
}
pre::-webkit-scrollbar-thumb {
@apply rounded-full bg-border bg-clip-padding p-px;
}
/* Code block styles */
pre {
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
/* Code block content */
> code {
@apply whitespace-pre-wrap;
counter-reset: line;
/* For code blocks with line numbers */
&[data-line-numbers] {
> [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
}
/* For each line in the code block */
> [data-line] {
@apply px-4;
}
/* Highlighted lines */
[data-highlighted-line] {
@apply bg-foreground/10;
}
/* Highlighted characters */
[data-highlighted-chars] > span {
@apply bg-muted-foreground/40 py-[7px];
}
.tab {
@apply relative;
}
.tab::before {
@apply absolute opacity-30;
content: '⇥';
}
/* Skip lines */
.skip {
@apply my-2 bg-foreground/5 text-center text-foreground;
&::before {
content: '' !important;
}
}
/* Diff lines */
.diff {
&.add {
@apply bg-additive/15;
}
&.remove {
@apply bg-destructive/15;
&::before {
content: '-';
counter-increment: none;
}
}
&.highlight {
@apply bg-foreground/10;
}
}
/* Copy button */
> button:has(> span) {
@apply right-1 top-1 m-0 size-8 rounded-lg bg-secondary p-1 backdrop-blur-none transition-all duration-200;
}
}
}
}
}
}
@layer base {
* {
@apply border-border;
}
/* Prevent text selection cursor on all elements by default */
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-tap-highlight-color: transparent;
cursor: default;
}
/* Allow text selection in content areas */
article,
article *,
.prose,
.prose *,
input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
}
/* Only inputs and textareas should have text cursor */
input,
textarea,
[contenteditable="true"] {
cursor: text;
}
/* Ensure proper cursor on interactive elements */
button,
a,
div,
img,
svg,
.pixel-button,
[role="button"],
[role="link"] {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
cursor: pointer;
}
/* Children of interactive elements should not have text cursor */
button *,
a *,
.pixel-button *,
[role="button"] *,
[role="link"] * {
cursor: pointer;
user-select: none;
}
/* Images should never show text cursor */
img,
svg,
video,
canvas {
user-select: none;
-webkit-user-select: none;
cursor: default;
}
/* Allow text selection for content elements but keep default cursor */
p,
span,
h1,
h2,
h3,
h4,
h5,
h6,
li {
user-select: text;
-webkit-user-select: text;
cursor: default;
}
body {
@apply text-foreground;
/* MiaoMiaoWu Background Gradients */
background-image:
radial-gradient(1100px circle at 5% -10%, rgba(217, 119, 87, 0.2), transparent 60%),
radial-gradient(900px circle at 90% 0%, rgba(96, 165, 250, 0.12), transparent 65%),
linear-gradient(180deg, rgba(255, 247, 242, 0.98), rgba(255, 247, 242, 1));
background-attachment: fixed;
background-size: cover;
background-color: hsl(var(--background));
}
.dark body {
background-image:
radial-gradient(900px circle at 15% -5%, rgba(241, 140, 110, 0.32), transparent 65%),
radial-gradient(800px circle at 82% 0%, rgba(56, 189, 248, 0.18), transparent 70%),
linear-gradient(180deg, rgba(16, 19, 28, 1), rgba(10, 12, 20, 1));
background-attachment: fixed;
background-size: cover;
@apply bg-background text-foreground;
}
}

View File

@@ -1,525 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'OPlusSans3-Medium';
src: url('/fonts/OPlusSans3-Medium.woff');
font-weight: 100 800;
font-style: normal;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono[wght].woff2') format('woff2-variations');
font-weight: 100 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono-Italic[wght].woff2') format('woff2-variations');
font-weight: 100 800;
font-style: italic;
font-display: swap;
}
.styles-module_calendar__sT1ND text {
fill: currentColor;
}
@layer base {
:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 34.12deg 53.68% 81.37%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
.light {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
*,
*::before,
*::after {
@apply border-border;
}
html {
color-scheme: dark;
@apply bg-background text-foreground forced-color-adjust-none;
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
}
.disable-transitions,
.disable-transitions * {
@apply !transition-none;
}
.theme {
--animate-gradient: gradient 8s linear infinite;
}
}
.svg-bg-color {
background-color: hsl(var(--primary));
}
:root,
::backdrop {
--sl-color-white: hsl(0, 0%, 100%);
--sl-color-gray-1: hsl(224, 20%, 94%);
--sl-color-gray-2: hsl(224, 6%, 77%);
--sl-color-gray-3: hsl(224, 6%, 56%);
--sl-color-gray-4: hsl(224, 7%, 36%);
--sl-color-gray-5: hsl(224, 10%, 23%);
--sl-color-gray-6: hsl(224, 14%, 16%);
--sl-color-black: hsl(224, 10%, 10%);
--sl-hue-orange: 41;
--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);
--sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);
--sl-hue-green: 101;
--sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);
--sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);
--sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);
--sl-hue-blue: 234;
--sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);
--sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);
--sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);
--sl-hue-purple: 281;
--sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);
--sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);
--sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);
--sl-hue-red: 339;
--sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);
--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);
--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);
--sl-color-accent-low: hsl(224, 54%, 20%);
--sl-color-accent: hsl(224, 100%, 60%);
--sl-color-accent-high: hsl(224, 100%, 85%);
--sl-color-text: var(--sl-color-gray-2);
--sl-color-text-accent: var(--sl-color-accent-high);
--sl-color-text-invert: var(--sl-color-accent-low);
--sl-color-bg: var(--sl-color-black);
--sl-color-bg-nav: var(--sl-color-gray-6);
--sl-color-bg-sidebar: var(--sl-color-gray-6);
--sl-color-bg-inline-code: var(--sl-color-gray-5);
--sl-color-bg-accent: var(--sl-color-accent-high);
--sl-color-hairline-light: var(--sl-color-gray-5);
--sl-color-hairline: var(--sl-color-gray-6);
--sl-color-hairline-shade: var(--sl-color-black);
--sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12),
0px 2px 1px hsla(0, 0%, 0%, 0.24);
--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08),
0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12),
0px 1px 1px hsla(0, 0%, 0%, 0.15);
--sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03),
0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33),
0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);
--sl-text-2xs: 0.75rem;
--sl-text-xs: 0.8125rem;
--sl-text-sm: 0.875rem;
--sl-text-base: 1rem;
--sl-text-lg: 1.125rem;
--sl-text-xl: 1.25rem;
--sl-text-2xl: 1.5rem;
--sl-text-3xl: 1.8125rem;
--sl-text-4xl: 2.1875rem;
--sl-text-5xl: 2.625rem;
--sl-text-6xl: 4rem;
--sl-text-body: var(--sl-text-base);
--sl-text-body-sm: var(--sl-text-xs);
--sl-text-code: var(--sl-text-sm);
--sl-text-code-sm: var(--sl-text-xs);
--sl-text-h1: var(--sl-text-4xl);
--sl-text-h2: var(--sl-text-3xl);
--sl-text-h3: var(--sl-text-2xl);
--sl-text-h4: var(--sl-text-xl);
--sl-text-h5: var(--sl-text-lg);
--sl-line-height: 1.75;
--sl-line-height-headings: 1.2;
--sl-font-system: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
--__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system);
--__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)),
var(--sl-font-system-mono);
--sl-nav-height: 3.5rem;
--sl-nav-pad-x: 1rem;
--sl-nav-pad-y: 0.75rem;
--sl-mobile-toc-height: 3rem;
--sl-sidebar-width: 18.75rem;
--sl-sidebar-pad-x: 1rem;
--sl-content-width: 45rem;
--sl-content-pad-x: 1rem;
--sl-menu-button-size: 2rem;
--sl-nav-gap: var(--sl-content-pad-x);
--sl-outline-offset-inside: -0.1875rem;
--sl-z-index-toc: 4;
--sl-z-index-menu: 5;
--sl-z-index-navbar: 10;
--sl-z-index-skiplink: 20;
}
:root[data-theme='light'],
[data-theme='light'] ::backdrop {
--sl-color-white: hsl(224, 10%, 10%);
--sl-color-gray-1: hsl(224, 14%, 16%);
--sl-color-gray-2: hsl(224, 10%, 23%);
--sl-color-gray-3: hsl(224, 7%, 36%);
--sl-color-gray-4: hsl(224, 6%, 56%);
--sl-color-gray-5: hsl(224, 6%, 77%);
--sl-color-gray-6: hsl(224, 20%, 94%);
--sl-color-gray-7: hsl(224, 19%, 97%);
--sl-color-black: hsl(0, 0%, 100%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);
--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);
--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);
--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);
--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);
--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);
--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);
--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);
--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);
--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);
--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);
--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);
--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);
--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);
--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);
--sl-color-accent-high: hsl(234, 80%, 30%);
--sl-color-accent: hsl(234, 90%, 60%);
--sl-color-accent-low: hsl(234, 88%, 90%);
--sl-color-text-accent: var(--sl-color-accent);
--sl-color-text-invert: var(--sl-color-black);
--sl-color-bg-nav: var(--sl-color-gray-7);
--sl-color-bg-sidebar: var(--sl-color-bg);
--sl-color-bg-inline-code: var(--sl-color-gray-6);
--sl-color-bg-accent: var(--sl-color-accent);
--sl-color-hairline-light: var(--sl-color-gray-6);
--sl-color-hairline-shade: var(--sl-color-gray-6);
--sl-color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.06),
0px 2px 1px hsla(0, 0%, 0%, 0.06);
--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.03),
0px 5px 2px hsla(0, 0%, 0%, 0.03), 0px 3px 2px hsla(0, 0%, 0%, 0.06),
0px 1px 1px hsla(0, 0%, 0%, 0.06);
--sl-shadow-lg: 0px 25px 7px rgba(0, 0, 0, 0.01),
0px 16px 6px hsla(0, 0%, 0%, 0.03), 0px 9px 5px hsla(223, 13%, 10%, 0.08),
0px 4px 4px hsla(0, 0%, 0%, 0.16), 0px 4px 2px hsla(0, 0%, 0%, 0.04);
}
@layer components {
article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words;
@apply prose-p:break-words;
@apply prose-a:!text-primary prose-a:!decoration-primary/50 prose-a:!underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-inherit;
@apply prose-blockquote:!not-italic prose-blockquote:!text-muted-foreground;
@apply prose-pre:!px-0;
@apply prose-img:mx-auto prose-img:rounded-xl prose-img:border;
@apply prose-table:mx-auto prose-table:block prose-table:max-w-fit prose-table:overflow-x-auto prose-td:break-words;
@apply sm:prose-table:mx-0 sm:prose-table:table sm:prose-table:max-w-none;
/* Fixing Katex display */
.katex-display {
@apply overflow-x-auto overflow-y-hidden py-4;
}
/* Fixing Katex fractions */
.frac-line {
@apply border-foreground;
}
/* Removes background from <mark> elements */
mark {
@apply bg-transparent;
}
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span {
color: var(--shiki-dark);
}
.dark & span {
color: var(--shiki-dark);
}
}
/* Inline code */
:not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium before:!content-none after:!content-none;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply break-words rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {
@apply mt-0 rounded-t-none;
}
}
/* Shadcn-like scrollbar */
pre::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
pre::-webkit-scrollbar-track {
@apply bg-transparent;
}
pre::-webkit-scrollbar-thumb {
@apply rounded-full bg-border bg-clip-padding p-px;
}
/* Code block styles */
pre {
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
/* Code block content */
> code {
@apply whitespace-pre-wrap;
counter-reset: line;
/* For code blocks with line numbers */
&[data-line-numbers] {
> [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
}
/* For each line in the code block */
> [data-line] {
@apply px-4;
}
/* Highlighted lines */
[data-highlighted-line] {
@apply bg-foreground/10;
}
/* Highlighted characters */
[data-highlighted-chars] > span {
@apply bg-muted-foreground/40 py-[7px];
}
.tab {
@apply relative;
}
.tab::before {
@apply absolute opacity-30;
content: '⇥';
}
/* Skip lines */
.skip {
@apply my-2 bg-foreground/5 text-center text-foreground;
&::before {
content: '' !important;
}
}
/* Diff lines */
.diff {
&.add {
@apply bg-additive/15;
}
&.remove {
@apply bg-destructive/15;
&::before {
content: '-';
counter-increment: none;
}
}
&.highlight {
@apply bg-foreground/10;
}
}
/* Copy button */
> button:has(> span) {
@apply right-1 top-1 m-0 size-8 rounded-lg bg-secondary p-1 backdrop-blur-none transition-all duration-200;
}
}
}
}
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@keyframes shake {
0% {
transform: translate(1px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-1deg);
}
20% {
transform: translate(-3px, 0px) rotate(1deg);
}
30% {
transform: translate(3px, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(3px, 1px) rotate(-1deg);
}
80% {
transform: translate(-1px, -1px) rotate(1deg);
}
90% {
transform: translate(1px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
@theme inline {
@keyframes gradient {
to {
backgroundposition: var(--bg-size, 300%) 0;
}
}
}
/* patch */
.recharts-cartesian-grid-vertical {
display: none;
}
/*
@media screen and (min-width: 1280px) {
html {
zoom: 0.95;
}
} */
.bg-color {
background-color: #e9d3b6 !important;
}
.shiki-transformer-button-copy {
border: none !important;
background-color: hsl(0deg 0% 0% / 0%) !important;
}

View File

@@ -1,252 +0,0 @@
/* MiaoMiaoWu Pixel Style Components */
/* Pixel Border - 像素边框 */
.pixel-border {
position: relative;
border-width: 2px;
border-style: solid;
border-color: color-mix(in srgb, hsl(var(--primary)) 60%, transparent);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.pixel-border:hover {
border-color: color-mix(in srgb, hsl(var(--primary)) 75%, transparent);
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.dark .pixel-border {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.dark .pixel-border:hover {
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
/* Pixel Button - 像素按钮 */
.pixel-button {
position: relative;
border-width: 2px;
border-style: solid;
border-color: transparent;
border-radius: 0;
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.18);
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
background-color 0.18s ease,
border-color 0.18s ease;
}
.pixel-button:hover {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22);
transform: translateY(-1px);
}
.pixel-button:active {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.16);
transform: translate(0);
}
.dark .pixel-button {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45);
}
.dark .pixel-button:hover {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
}
.dark .pixel-button:active {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
}
/* Pixel Card - 像素卡片 */
.pixel-card {
position: relative;
border-width: 2px;
border-style: solid;
border-color: color-mix(in srgb, hsl(var(--primary)) 22%, hsl(var(--border)));
background: hsl(var(--card));
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22),
0 0 0 1px rgba(255, 255, 255, 0.04);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.pixel-card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, hsl(var(--primary)) 40%, hsl(var(--border)));
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.26),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* 悬停时显示斜纹背景 */
.pixel-card::before {
content: '';
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
45deg,
transparent 0,
transparent 6px,
rgba(255, 255, 255, 0.04) 6px,
rgba(255, 255, 255, 0.04) 12px
);
opacity: 0;
transition: opacity 0.2s ease;
z-index: 0;
}
.pixel-card:hover::before {
opacity: 1;
}
.pixel-card > * {
position: relative;
z-index: 1;
}
.dark .pixel-card {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.65),
0 0 0 1px rgba(255, 255, 255, 0.04);
}
.dark .pixel-card:hover {
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.75),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* Pixel Badge - 像素徽章 */
.pixel-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.65rem;
border-radius: 0;
border-width: 2px;
border-style: solid;
border-color: rgba(241, 140, 110, 0.3);
background: rgba(241, 140, 110, 0.12);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
font-weight: 600;
}
/* Pixel Pill - 药丸按钮 */
.pixel-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 0;
border-width: 2px;
border-style: solid;
border-color: rgba(217, 119, 87, 0.45);
padding: 0.35rem 0.85rem;
background: color-mix(in srgb, hsl(var(--secondary)) 40%, transparent);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.15),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
transition: all 0.2s ease;
}
.pixel-pill:hover {
background: rgba(217, 119, 87, 0.16);
border-color: rgba(217, 119, 87, 0.65);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.dark .pixel-pill {
border-color: rgba(241, 140, 110, 0.55);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.dark .pixel-pill:hover {
background: rgba(241, 140, 110, 0.22);
border-color: rgba(241, 140, 110, 0.75);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
/* Pixel Text - 像素文字效果 */
.pixel-text {
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.25),
-1px -1px 0 rgba(255, 241, 232, 0.18);
}
.dark .pixel-text {
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55),
-1px -1px 0 rgba(255, 241, 232, 0.08);
}
/* Grid Pattern - 网格图案 */
.grid-pattern {
position: relative;
}
.grid-pattern::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(241, 140, 110, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(241, 140, 110, 0.05) 1px, transparent 1px);
background-size: 32px 32px;
opacity: 0.6;
pointer-events: none;
z-index: -1;
}
.dark .grid-pattern::after {
background-image:
linear-gradient(to right, rgba(241, 140, 110, 0.08) 1px, transparent 1px),
linear-gradient(to bottom, rgba(241, 140, 110, 0.08) 1px, transparent 1px);
opacity: 0.4;
}
/* Scanlines - 扫描线效果 */
.scanlines {
background-image: repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.05) 0,
rgba(255, 255, 255, 0.05) 1px,
transparent 1px,
transparent 6px
);
pointer-events: none;
}
.dark .scanlines {
background-image: repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.03) 0,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px,
transparent 6px
);
}
/* Remove rounded corners from common elements */
button:not(.short-cuts-template *, [class*="rounded-full"]),
input,
textarea,
select,
.card,
.badge {
border-radius: 0 !important;
}
/* Keep rounded corners for shortcut buttons and other rounded-full elements */
.short-cuts-template div[class*="rounded-full"],
div[class*="rounded-full"] {
border-radius: 9999px !important;
}

View File

@@ -8,7 +8,7 @@ const config: Config = {
extend: {
fontFamily: {
sans: [
'OPlusSans3-Medium',
'haipaiqiangdiaosenxiyuan',
...defaultTheme.fontFamily.sans
],
mono: [
@@ -19,18 +19,6 @@ const config: Config = {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
brand: {
50: 'var(--brand-50)',
100: 'var(--brand-100)',
200: 'var(--brand-200)',
300: 'var(--brand-300)',
400: 'var(--brand-400)',
500: 'var(--brand-500)',
600: 'var(--brand-600)',
700: 'var(--brand-700)',
800: 'var(--brand-800)',
900: 'var(--brand-900)'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
@@ -75,10 +63,9 @@ const config: Config = {
}
},
borderRadius: {
lg: '0',
md: '0',
sm: '0',
DEFAULT: '0'
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
animation: {
orbit: 'orbit calc(var(--duration)*1s) linear infinite',