Compare commits
91 Commits
1bb1440e40
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39a589e3f | ||
|
|
b0295c70ce | ||
|
|
672d4be561 | ||
|
|
107ac6b38e | ||
|
|
465e706210 | ||
|
|
c5dea71f59 | ||
|
|
65db8be5f7 | ||
|
|
37db59cba5 | ||
|
|
fd91f486d3 | ||
|
|
1bd1e5e31a | ||
|
|
d16b919464 | ||
|
|
90d8d89f7b | ||
|
|
546dde4923 | ||
|
|
d72963d648 | ||
|
|
f4e7112d4c | ||
|
|
bc4155ce82 | ||
|
|
f1bda537b4 | ||
|
|
1b2f79f65c | ||
|
|
62383aaf9e | ||
|
|
dcc5654603 | ||
|
|
cf36b71990 | ||
|
|
9cda45f112 | ||
| ef7ef44033 | |||
| 3ca63f62fb | |||
| 4c255a2672 | |||
| 0f4eaf8523 | |||
| b29f5c7c1e | |||
| 20b01c6f51 | |||
| 57e1724c34 | |||
| c720e605bf | |||
| 6575bf0c62 | |||
| 639e3b54db | |||
| 7be284fbd5 | |||
| c74c99e776 | |||
| 8026dd0321 | |||
| 35c1ebe9f1 | |||
| b9255a78ae | |||
| de76217b85 | |||
| cca8f502f7 | |||
| 44e437175e | |||
| bea9917330 | |||
| 90b53fc225 | |||
| 4c91a029a3 | |||
| 23c445ceca | |||
| 5281507386 | |||
| bb8b999644 | |||
| 5eb497db3f | |||
| 7376c3ee0b | |||
| daecb6c9d7 | |||
| cf443d730c | |||
| 9cbc974cda | |||
| d240cfe5a3 | |||
| 1eda3310ea | |||
| c3d250c9b1 | |||
| 2a422883c4 | |||
| 10d5dfa639 | |||
| 450a7a0eb2 | |||
| 1969c79fac | |||
| 7cf8a4fa55 | |||
| 253d2774a5 | |||
| a5e04fc004 | |||
| 479bf095cf | |||
| 80037a96c1 | |||
| 0342feb3bd | |||
| c055535e80 | |||
| 6156b512e9 | |||
| 9e5d98e2e2 | |||
| 101db5aa83 | |||
| 9ef5139d93 | |||
| f999f0912e | |||
| 75cd6ce315 | |||
| fe3978ddbf | |||
| f76e5719a2 | |||
| 79db601591 | |||
| 3b12d07b51 | |||
| 6e13092027 | |||
| ee31225f15 | |||
| cbe2957f80 | |||
| c3e50ccff2 | |||
| e0a0576ee1 | |||
| b60cdafc0a | |||
| fcf44c1d15 | |||
| 246eb86d8a | |||
| db2e8936f3 | |||
| 5e9389f5de | |||
| bdf3a1e496 | |||
| 8a9be72877 | |||
| 92d138d4ab | |||
| 114ff69cef | |||
| 6cca17def0 | |||
| 0c3d9a4852 |
5
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# build output
|
||||
dist/
|
||||
dist.zip
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
@@ -24,4 +25,6 @@ pnpm-debug.log*
|
||||
.idea/
|
||||
|
||||
teaser.pptx
|
||||
~$teaser.pptx
|
||||
~$teaser.pptx
|
||||
|
||||
.claude
|
||||
93
README.md
@@ -2,22 +2,25 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
## enscribe.dev
|
||||
## dev.2ha.me
|
||||
### frok by [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)
|
||||
|
||||
[![CodeFactor]](https://www.codefactor.io/repository/github/jktrn/enscribe.dev)
|
||||
![Stargazers]
|
||||
[![Code License]](LICENSE.md)
|
||||
[![Content License]](LICENSE.content.md)
|
||||
|
||||
[enscribe.dev](https://enscribe.dev) is my personal information security blog—built with [Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), and [shadcn/ui](https://ui.shadcn.com/). Based on my personal blogging template, [astro-erudite](https://github.com/jktrn/astro-erudite).
|
||||
[![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).
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
> [!WARNING]
|
||||
This project does not use i18n.
|
||||
|
||||
This is a list of the various technologies used to build this website:
|
||||
## Technology Stack
|
||||
|
||||
These are the technology stacks originally used in [enscribe.dev](https://github.com/jktrn/enscribe.dev.git)
|
||||
|
||||
| Category | Technology Name |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
@@ -29,70 +32,46 @@ This is a list of the various technologies used to build this website:
|
||||
| Graphics | [Figma](https://www.figma.com/) |
|
||||
| Deployment | [Vercel](https://vercel.com) |
|
||||
|
||||
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> This site was previously built using [Next.js](https://nextjs.org) via the [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog). If you wish to reference the codebase or utilize its components at that point (previously licensed via [Apache 2.0](https://github.com/jktrn/enscribe.dev/blob/ddda783b21d5d49783f4d98e9b06676af8f95031/LICENSE)), utilize the [v2.3.0](https://github.com/jktrn/enscribe.dev/releases/tag/v2.3.0) release or browse the tree at [`ddda783`](https://github.com/jktrn/enscribe.dev/tree/ddda783b21d5d49783f4d98e9b06676af8f95031).
|
||||
[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.
|
||||
|
||||
| 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) |
|
||||
---
|
||||
|
||||
## Licensing
|
||||
|
||||
This project uses a multi-tiered licensing approach to differentiate between various components:
|
||||
## License
|
||||
|
||||
### Original Template
|
||||
Original Template
|
||||
|
||||
From the [Original Template License](LICENSE.md#original-template-license) section within the license:
|
||||
Extract from the "Original Template License" section of the license:
|
||||
|
||||
> This website is based on [astro-erudite](https://github.com/jktrn/astro-erudite), which was loosely derived from an MIT-licensed project, [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).
|
||||
|
||||
The original MIT license is included in the full [LICENSE.md](LICENSE.md) file for compliance.
|
||||
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.
|
||||
|
||||
### Website Code
|
||||
### Site Code
|
||||
|
||||
[![Code License]](LICENSE.md)
|
||||
[![Code License]](LICENSE)
|
||||
|
||||
All modifications and custom implementations made to the original template are proprietary and all rights are reserved. The code is publicly available for viewing and reference only. Modification, redistribution, or commercial use requires explicit permission from the copyright holder.
|
||||
> 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.
|
||||
|
||||
From the [Website Code License](LICENSE.md#website-code-license) section:
|
||||
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.
|
||||
|
||||
> This license applies specifically to the custom modifications made to the [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.
|
||||
### 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.
|
||||
|
||||
From the [Disclaimer](LICENSE.md#disclaimer) section:
|
||||
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.
|
||||
|
||||
> If you are interested in utilizing a similar website structure for your own means, please refer to the original template at [jktrn/astro-erudite](https://github.com/jktrn/astro-erudite), which is available under the MIT license and is free for use and modification.
|
||||
|
||||
### Website Content
|
||||
|
||||
[![Content License]](LICENSE.content.md)
|
||||
|
||||
From the [Website Content License](LICENSE.md#website-content-license) section:
|
||||
|
||||
> The content of this website, including but not limited to text, images, graphics, MDX files/components, and any other materials (excluding code and design elements covered under the Website Code License above), is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0).
|
||||
>
|
||||
> To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/4.0/ or view [LICENSE.content.md](./LICENSE.content.md).
|
||||
|
||||
For full license details and permission requests, please refer to the [LICENSE.md](LICENSE.md) file in this repository or contact [jason@enscribe.dev](mailto:jason@enscribe.dev).
|
||||
|
||||
---
|
||||
|
||||
### Star History
|
||||
|
||||
<a href="https://star-history.com/#jktrn/enscribe.dev&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=jktrn/enscribe.dev&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=jktrn/enscribe.dev&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=jktrn/enscribe.dev&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
Extracted from the [Website Code License](LICENSE#website-code-license) section.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Made with ♥ by [enscribe](https://enscribe.dev)!
|
||||
|
||||
[cc-by-nc-nd]: http://creativecommons.org/licenses/by-nc-nd/4.0/
|
||||
[cc-by-nc-nd-shield]: https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-lightgrey.svg
|
||||
|
||||
[CodeFactor]: https://img.shields.io/codefactor/grade/github/jktrn/enscribe.dev?color=2f2a24&logo=codefactor&logoColor=fff&style=for-the-badge
|
||||
[Stargazers]: https://img.shields.io/github/stars/jktrn/enscribe.dev?color=463f37&logo=github&logoColor=fff&style=for-the-badge
|
||||
[Code License]: https://img.shields.io/badge/code%20license-proprietary-5d5449?style=for-the-badge&logo=github&logoColor=fff
|
||||
[Content License]: https://img.shields.io/badge/content%20license-CC%20BY--NC--ND%204.0-756a5b?style=for-the-badge&logo=creativecommons&logoColor=fff
|
||||
[Code License]: https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge&logo=github&logoColor=fff
|
||||
70
README_zh-Hans.md
Normal file
@@ -0,0 +1,70 @@
|
||||

|
||||
|
||||
<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
|
||||
119
README_zh-Hant.md
Normal file
@@ -0,0 +1,119 @@
|
||||

|
||||
|
||||
|
||||
|
||||
<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
|
||||
@@ -18,6 +18,7 @@ import remarkToc from 'remark-toc'
|
||||
import sectionize from '@hbsnow/rehype-sectionize'
|
||||
import { transformerNotationSkip } from './src/lib/transformerNotationSkip'
|
||||
import { transformerDiffHighlight } from './src/lib/transformerDiffHighlight'
|
||||
import { transformerCopyButton } from './src/lib/transformerCopyButton'
|
||||
|
||||
import icon from 'astro-icon'
|
||||
|
||||
@@ -64,6 +65,11 @@ export default defineConfig({
|
||||
transformerRenderWhitespace(),
|
||||
transformerNotationSkip(),
|
||||
transformerDiffHighlight(),
|
||||
transformerCopyButton({
|
||||
duration: 1000,
|
||||
successIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E",
|
||||
copyIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E",
|
||||
})
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
2639
package-lock.json
generated
@@ -9,7 +9,8 @@
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}"
|
||||
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
@@ -42,6 +43,7 @@
|
||||
"lucide-react": "^0.439.0",
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.7.5",
|
||||
"react-compiler-runtime": "^19.1.0-rc.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-use-lanyard": "^0.3.2",
|
||||
@@ -59,6 +61,7 @@
|
||||
},
|
||||
"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",
|
||||
@@ -86,5 +89,8 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "*"
|
||||
}
|
||||
}
|
||||
|
||||
14
patches/@shikijs+transformers+1.24.4.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
BIN
public/fonts/OPlusSans3-Medium.woff2
Normal file
BIN
public/static/anime-bg/225.mp4
Normal file
BIN
public/static/anime-bg/830.mp4
Normal file
BIN
public/static/anime-bg/guduyaogun.mp4
Normal file
BIN
public/static/anime-bg/guduyaogun1.mp4
Normal file
BIN
public/static/anime-bg/guduyaogun2.mp4
Normal file
BIN
public/static/anime-bg/kisaki.mp4
Normal file
BIN
public/static/anime-bg/lige.mp4
Normal file
BIN
public/static/anime-bg/loading.mp4
Normal file
BIN
public/static/anime-bg/lycoris2.mp4
Normal file
BIN
public/static/anime-bg/maoliang.mp4
Normal file
BIN
public/static/anime-bg/miku.mp4
Normal file
BIN
public/static/anime-bg/miku2.mp4
Normal file
BIN
public/static/anime-bg/sanlian.mp4
Normal file
BIN
public/static/avatar.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 9.3 MiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 10 KiB |
BIN
public/static/images/shortcuts-bg-mini.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 7.8 KiB |
BIN
public/static/loading.gif
Normal file
|
After Width: | Height: | Size: 415 KiB |
1
public/static/mmw.svg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
public/static/nyan-cat.gif
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
public/static/nyancat.gif
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/static/showcase-card.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/static/showcase-card.v1.png
Normal file
|
After Width: | Height: | Size: 959 KiB |
@@ -37,7 +37,7 @@ const socialLinks: SocialLink[] = [
|
||||
---
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border p-4 transition-colors duration-300 ease-in-out has-[a:hover]:bg-secondary/50"
|
||||
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)]"
|
||||
>
|
||||
<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://code.2ha.me/dev.2ha.me/dev.2ha.me" target="_blank">
|
||||
<Link href="https://github.com/iluobei/dev.2ha.me" target="_blank">
|
||||
<svg
|
||||
class="size-4 ml-1 mr-1"
|
||||
style="filter: invert(100%);"
|
||||
|
||||
192
src/components/Badge.astro
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
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>
|
||||
@@ -24,13 +24,14 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === entry.data.title)
|
||||
const totalBody = [entry.body!, ...subposts.map((p) => p.body!)]
|
||||
.map(stripCodeBlocks)
|
||||
.join('')
|
||||
const readTime = readingTime(totalBody)
|
||||
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
|
||||
const readTime = readingTime(wordCount)
|
||||
|
||||
const authors = await parseAuthors(entry.data.authors ?? [])
|
||||
---
|
||||
|
||||
<div
|
||||
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
||||
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)]"
|
||||
>
|
||||
<Link
|
||||
href={`/${entry.collection}/${entry.id}`}
|
||||
|
||||
111
src/components/DevShortCuts.astro
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
import { DEV_LINKS } from '@/consts'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import Link from './Link.astro'
|
||||
|
||||
|
||||
// interface ShortCutProps {
|
||||
// href: string | undefined
|
||||
// title: string
|
||||
// ariaLabel: string
|
||||
// icon: string
|
||||
// }
|
||||
|
||||
// const SHORT_CUTS: ShortCutProps[] = [
|
||||
// {
|
||||
// href: getDevLinkHref('Gitea'),
|
||||
// title: "代码仓库",
|
||||
// ariaLabel: "代码仓库",
|
||||
// icon: "mdi:git"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('Nexus'),
|
||||
// title: "Maven仓库",
|
||||
// ariaLabel: "Maven仓库",
|
||||
// icon: "mdi:chart-doughnut-variant"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('Bytebase'),
|
||||
// title: "数据库管理",
|
||||
// ariaLabel: "数据库管理",
|
||||
// icon: "mdi:database-cog"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('Zfile'),
|
||||
// title: "网盘",
|
||||
// ariaLabel: "网盘",
|
||||
// icon: "mdi:cloud-arrow-up"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('FileServer'),
|
||||
// title: "文件服务器",
|
||||
// ariaLabel: "文件服务器",
|
||||
// icon: "mdi:file-arrow-up-down-outline"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('immich'),
|
||||
// title: "相册",
|
||||
// ariaLabel: "相册",
|
||||
// icon: "mdi:camera"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('Emby'),
|
||||
// title: "Emby",
|
||||
// ariaLabel: "Emby",
|
||||
// icon: "mdi:emby"
|
||||
// },
|
||||
// {
|
||||
// href: getDevLinkHref('Emby'),
|
||||
// title: "Emby",
|
||||
// ariaLabel: "Emby",
|
||||
// icon: "mdi:emby"
|
||||
// }
|
||||
// ]
|
||||
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 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]'
|
||||
]
|
||||
|
||||
---
|
||||
{
|
||||
DEV_LINKS.map((item, index) => (
|
||||
<div
|
||||
class={SHORTS_CUTS_CLASS_NAMES[index]}
|
||||
>
|
||||
<Link
|
||||
href={item.href} || '#'}
|
||||
title={item.title}
|
||||
aria-label={item.label}
|
||||
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)))]"
|
||||
>
|
||||
{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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
406
src/components/DevShortcutsHexagon.astro
Normal file
@@ -0,0 +1,406 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import '../styles/global.css'
|
||||
import '../styles/katex.css'
|
||||
// import '../styles/katex.css'
|
||||
|
||||
import { SITE } from '@/consts'
|
||||
import { ClientRouter } from 'astro:transitions'
|
||||
|
||||
@@ -4,27 +4,30 @@ 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-20 bg-background/50 backdrop-blur-md"
|
||||
class="sticky top-0 z-[99] bg-background/50 backdrop-blur-md"
|
||||
transition:persist
|
||||
>
|
||||
<Container>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 py-4 ">
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="Logo" class="size-8 -scale-x-100" />
|
||||
<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>
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<nav class="hidden items-center gap-4 text-base sm:flex sm:gap-6">
|
||||
<nav class="hidden items-center gap-2 text-base sm:flex sm:gap-3">
|
||||
{
|
||||
NAV_LINKS.map((item) => (
|
||||
<Link
|
||||
href={item.href}
|
||||
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
|
||||
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"
|
||||
>
|
||||
{item.label}
|
||||
<Icon name={item.icon} class="size-[18px] shrink-0" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
@@ -35,3 +38,31 @@ 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>
|
||||
|
||||
36
src/components/PageHead.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
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" />
|
||||
@@ -1,10 +1,13 @@
|
||||
---
|
||||
import Badge from '@/components/Badge.astro'
|
||||
import Link from '@/components/Link.astro'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { extractDomain, formatMonthYear } from '@/lib/utils'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { Image } from 'astro:assets'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
project: CollectionEntry<'projects'>
|
||||
}
|
||||
|
||||
@@ -12,30 +15,63 @@ const { project } = Astro.props
|
||||
---
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
||||
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)]"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
{project.data.description}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{
|
||||
project.data.tags.map((tag) => (
|
||||
<Badge variant="secondary" showHash={false}>
|
||||
{tag}
|
||||
</Badge>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
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>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ const iconMap = {
|
||||
Gitea: 'mdi:git',
|
||||
Maven: 'mavenrepo',
|
||||
DevIntro: 'lucide:info',
|
||||
HubProxy: 'lucide:rocket',
|
||||
}
|
||||
|
||||
const getSocialLink = ({ href, label }: SocialLink) => ({
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
import AvatarComponent from '@/components/ui/avatar'
|
||||
|
||||
const AuthorPresence = () => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden sm:aspect-square">
|
||||
<div className="relative overflow-hidden sm:aspect-square select-none" style={{ cursor: 'default' }}>
|
||||
<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 gap-x-1">
|
||||
<div className="flex justify-between">
|
||||
<div className="relative">
|
||||
<AvatarComponent
|
||||
src="/static/avatar.webp"
|
||||
src="/static/avatar.png"
|
||||
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="/static/images/badges.png"
|
||||
src="https://contrib.rocks/image?repo=iluobei/dev.2ha.me"
|
||||
alt="Discord Badges"
|
||||
width={104}
|
||||
height={24}
|
||||
className="grayscale"
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
li@2ha.me
|
||||
</span>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<img
|
||||
src="/static/images/lieflat.svg"
|
||||
alt="No Status Image"
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-full rounded-lg"
|
||||
className="h-full rounded-lg select-none"
|
||||
/>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
<div className="text-[10px] text-muted-foreground select-none cursor-default">
|
||||
但行好事,莫问前程。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,12 +55,12 @@ const WakatimeGraph = ({ }: Props) => {
|
||||
useEffect(() => {
|
||||
setLanguages([
|
||||
{ name: 'java', hours: 1009, fill: 'hsl(var(--chart-1))' },
|
||||
{ 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))' },
|
||||
{ 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))' },
|
||||
])
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
@@ -11,12 +11,12 @@ import Calendar, {
|
||||
|
||||
async function fetchCalendarData(): Promise<ApiResponse> {
|
||||
const response = await fetch(
|
||||
`https://dev.2ha.me/api/Calendar`,
|
||||
`https://api.2ha.me/api/Calendar`,
|
||||
)
|
||||
const data: ApiResponse | ApiErrorResponse = await response.json()
|
||||
if (!response.ok) {
|
||||
throw Error(
|
||||
`Fetching Gitea(code.2ha.me) contribution data for jimlee failed, see https://github.com/luckykeeper/giteaCalendar.`,
|
||||
`Fetching Gitea(code.2ha.me) contribution data for luobei failed, see https://github.com/luckykeeper/giteaCalendar.`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ interface Track {
|
||||
image: { '#text': string }[] //
|
||||
url: string
|
||||
'@attr'?: { nowplaying: string },
|
||||
outerurl: string
|
||||
outerurl: string,
|
||||
backupurl: string
|
||||
}
|
||||
|
||||
const SpotifyPresence = () => {
|
||||
const Music163Player = () => {
|
||||
const [displayData, setDisplayData] = useState<Track | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
@@ -34,7 +35,7 @@ const SpotifyPresence = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://dev.2ha.me/api/v1/play/record?uid=91859315&type=1')
|
||||
fetch('https://api.2ha.me/api/v1/play/record?uid=91859315&type=1')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
let lastweekFirstSong = data.weekData[0].song
|
||||
@@ -47,9 +48,10 @@ const SpotifyPresence = () => {
|
||||
album: {
|
||||
'#text': lastweekFirstSong.al.name
|
||||
},
|
||||
image: [{'#text': lastweekFirstSong.al.picUrl}],
|
||||
image: [{'#text': lastweekFirstSong.al.picUrl.replace('http:', 'https:')}],
|
||||
url: 'https://music.163.com/song?id=' + lastweekFirstSong.id,
|
||||
outerurl: "https://music.163.com/song/media/outer/url?id=" + lastweekFirstSong.id + ".mp3"
|
||||
outerurl: "https://music.163.com/song/media/outer/url?id=" + lastweekFirstSong.id + ".mp3",
|
||||
backupurl: "https://f.2ha.me/music/" + lastweekFirstSong.id + ".mp3"
|
||||
}
|
||||
setDisplayData(track)
|
||||
setIsLoading(false)
|
||||
@@ -60,6 +62,13 @@ const SpotifyPresence = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 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">
|
||||
@@ -82,7 +91,7 @@ const SpotifyPresence = () => {
|
||||
|
||||
if (!displayData) return <p>Something absolutely horrible has gone wrong</p>
|
||||
|
||||
const { name: song, artist, album, image, url, outerurl } = displayData
|
||||
const { name: song, artist, album, image, url, outerurl, backupurl } = displayData
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -100,7 +109,7 @@ const SpotifyPresence = () => {
|
||||
alt="Album art"
|
||||
width={128}
|
||||
height={128}
|
||||
className="mb-2 w-[55%] rounded-xl border border-border"
|
||||
className="mb-2 w-[55%] border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))]"
|
||||
/>
|
||||
</a>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-end overflow-hidden">
|
||||
@@ -139,7 +148,10 @@ const SpotifyPresence = () => {
|
||||
<span className="w-[85%] truncate text-xs text-muted-foreground">
|
||||
<span className="font-semibold text-secondary-foreground">
|
||||
<div>
|
||||
<audio ref={audioRef} src={outerurl} />
|
||||
<audio ref={audioRef}>
|
||||
<source src={outerurl} type="audio/mp3" />
|
||||
<source src={backupurl} type="audio/mp3" />
|
||||
</audio>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
@@ -160,4 +172,4 @@ const SpotifyPresence = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default SpotifyPresence
|
||||
export default Music163Player
|
||||
@@ -18,7 +18,7 @@ const SpotifyPresence = () => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://dev.2ha.me/api/v1/play/record?uid=91859315&type=1')
|
||||
fetch('https://api.2ha.me/api/v1/play/record?uid=91859315&type=1')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
let lastweekFirstSong = data.weekData[0].song
|
||||
71
src/components/custom/RandomAnimeBackgrounds.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export const videoBackgrounds: string[] = [
|
||||
'225.mp4',
|
||||
'830.mp4',
|
||||
// 'guduyaogun.mp4',
|
||||
'guduyaogun1.mp4',
|
||||
'guduyaogun2.mp4',
|
||||
'lige.mp4',
|
||||
'maoliang.mp4',
|
||||
// 'miku.mp4',
|
||||
'miku2.mp4',
|
||||
// 'sanlian.mp4',
|
||||
'lycoris2.mp4',
|
||||
]
|
||||
|
||||
|
||||
const RandomAnimeBackground = () => {
|
||||
const [index, setIndex] = useState<number>(0)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [bindEvent, setBindEvent] = useState<boolean>(true);
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
setIsLoading(true)
|
||||
setIndex(getRandomIndex())
|
||||
if (bindEvent && videoRef.current) {
|
||||
videoRef.current.addEventListener('canplay', handleCanPlayThrough);
|
||||
setBindEvent(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanPlayThrough = () => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 200);
|
||||
}
|
||||
|
||||
const getRandomIndex = () => {
|
||||
return Math.floor(Math.random() * videoBackgrounds.length);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIndex(getRandomIndex())
|
||||
// setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
// }, 100);
|
||||
}, [])
|
||||
|
||||
// if (isLoading) {
|
||||
// return (
|
||||
// <video className="no-repeat relative w-full justify-center rounded-[1.4em] object-cover"
|
||||
// src='/static/anime-bg/loading.mp4'
|
||||
// autoPlay muted loop>
|
||||
// Your browser does not support the video tag.
|
||||
// </video>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<video ref={videoRef} width="100" height="100" className="no-repeat relative w-full justify-center object-cover"
|
||||
src={'/static/anime-bg/' + videoBackgrounds[index]}
|
||||
style={{ display: isLoading ? 'none' : 'block' }}
|
||||
onEnded={handleVideoEnded}
|
||||
autoPlay muted>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)
|
||||
}
|
||||
|
||||
export default RandomAnimeBackground
|
||||
@@ -24,7 +24,7 @@ export default function GradientText({
|
||||
// console.log(children)
|
||||
return (
|
||||
<div
|
||||
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}`}
|
||||
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}`}
|
||||
>
|
||||
{showBorder && (
|
||||
<div
|
||||
|
||||
@@ -280,7 +280,7 @@ const LetterGlitch = ({
|
||||
}, [glitchSpeed, smooth])
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden rounded-3xl bg-black">
|
||||
<div className="relative h-full w-full overflow-hidden 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>
|
||||
|
||||
@@ -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 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',
|
||||
'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',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
'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)]',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
'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)]',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
'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)]',
|
||||
secondary:
|
||||
'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',
|
||||
'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',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
sm: 'h-8 px-3 text-xs',
|
||||
lg: 'h-10 px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'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',
|
||||
'flex cursor-default select-none items-center 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 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',
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
@@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'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',
|
||||
'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',
|
||||
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 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',
|
||||
'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',
|
||||
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 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',
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -7,7 +7,20 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { NAV_LINKS } from '@/consts'
|
||||
import { Menu } from 'lucide-react'
|
||||
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
|
||||
}
|
||||
|
||||
const MobileMenu = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -33,25 +46,29 @@ const MobileMenu = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="sm:hidden"
|
||||
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)]"
|
||||
title="Menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<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 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>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
@@ -5,11 +5,13 @@ 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 = {
|
||||
@@ -26,19 +28,20 @@ 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: '主页' },
|
||||
{ href: '/blog', label: '博客' },
|
||||
{ href: '/tags', label: '标签' },
|
||||
// { href: '/authors', label: '作者' },
|
||||
{ href: '/authors', label: '关于' },
|
||||
{ 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' },
|
||||
]
|
||||
|
||||
export const SOCIAL_LINKS: Link[] = [
|
||||
{ href: 'https://github.com/jimleerx', label: 'GitHub' },
|
||||
{ href: 'https://maven.2ha.me', label: 'Maven' },
|
||||
{ href: 'https://github.com/iluobei', label: 'GitHub' },
|
||||
{ href: 'https://1ms.cc', label: 'HubProxy' },
|
||||
{ href: 'https://code.2ha.me', label: 'Gitea' },
|
||||
{ href: 'li@2ha.me', label: 'Email' },
|
||||
]
|
||||
@@ -51,10 +54,10 @@ export const DEV_LINKS: DevLink[] = [
|
||||
icon: 'mdi:git',
|
||||
},
|
||||
{
|
||||
href: 'https://maven.2ha.me',
|
||||
href: 'https://img.2ha.me',
|
||||
label: 'Nexus',
|
||||
title: 'Maven仓库',
|
||||
icon: 'mdi:chart-doughnut-variant',
|
||||
title: '图床',
|
||||
icon: 'mdi:image-multiple',
|
||||
},
|
||||
{
|
||||
href: 'https://dms.2ha.me',
|
||||
@@ -66,13 +69,13 @@ export const DEV_LINKS: DevLink[] = [
|
||||
href: 'https://p.2ha.me',
|
||||
label: 'Zfile',
|
||||
title: '网盘',
|
||||
icon: 'mdi:cloud-arrow-up',
|
||||
icon: 'mdi:harddisk',
|
||||
},
|
||||
{
|
||||
href: 'https://photo.2ha.me',
|
||||
label: 'immich',
|
||||
title: '相册',
|
||||
icon: 'mdi:camera',
|
||||
href: 'https://tz.2ha.me',
|
||||
label: 'VPS Monitor',
|
||||
title: '探针',
|
||||
icon: 'mdi:chart-areaspline',
|
||||
},
|
||||
{
|
||||
href: 'https://f.2ha.me',
|
||||
@@ -80,17 +83,36 @@ export const DEV_LINKS: DevLink[] = [
|
||||
title: '文件服务器',
|
||||
icon: 'mdi:file-arrow-up-down-outline',
|
||||
},
|
||||
{ href: 'https://v.2ha.me', label: 'Emby', title: 'Emby', icon: 'mdi:emby' },
|
||||
{
|
||||
href: 'https://plex.2ha.me',
|
||||
label: 'Plex',
|
||||
title: 'Plex',
|
||||
icon: 'mdi:plex',
|
||||
{
|
||||
href: 'https://status.2ha.me',
|
||||
label: 'Domain Status',
|
||||
title: '站点检测',
|
||||
icon: 'mdi:cloud-check'
|
||||
},
|
||||
{
|
||||
href: 'https://mr.2ha.me',
|
||||
label: 'MovieRobot',
|
||||
title: '媒体订阅工具',
|
||||
icon: 'mdi:fruit-cherries',
|
||||
href: 'https://in.2ha.me',
|
||||
label: '2ha.me statistics',
|
||||
title: '统计',
|
||||
icon: 'mdi:sun-azimuth',
|
||||
},
|
||||
{
|
||||
href: 'https://miaomiaowu.net',
|
||||
label: '妙妙屋',
|
||||
title: '个人Clash订阅管理工具',
|
||||
icon: '/static/mmw.svg',
|
||||
// icon: 'mdi:cat',
|
||||
},
|
||||
{
|
||||
href: 'https://1ms.cc',
|
||||
label: 'hubproxy',
|
||||
title: 'GitHub&DockerHub加速',
|
||||
icon: 'mdi:rocket-launch-outline',
|
||||
},
|
||||
]
|
||||
|
||||
export const getDevLinkHref = (label: string): string => {
|
||||
return DEV_LINKS.find((link) => link.label === label)?.href || '#'
|
||||
}
|
||||
export const getDevLink = (label: string): DevLink | undefined => {
|
||||
return DEV_LINKS.find((link) => link.label === label)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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(),
|
||||
@@ -66,6 +67,9 @@ 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(),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: 'jimlee'
|
||||
name: '胡萝北(🥕)'
|
||||
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/jimlee'
|
||||
devintro: 'https://code.2ha.me/luobei'
|
||||
gitea: 'https://code.2ha.me/dev.2ha.me'
|
||||
# twitter: 'https://twitter.com/enscry'
|
||||
github: 'https://github.com/jimleerx'
|
||||
github: 'https://github.com/iluobei'
|
||||
mail: 'li@2ha.me'
|
||||
---
|
||||
|
||||
47
src/content/blog/buildnginx-2025/buildnginx.mdx
vendored
@@ -4,37 +4,45 @@ description: '在debian上根据需要添加nginx模块,编译自定义的ngin
|
||||
date: 2025-04-11
|
||||
tags: ['nginx', 'debian', 'build']
|
||||
image: 'assets/nginx.svg'
|
||||
authors: ['jimlee']
|
||||
authors: ['胡萝北(🥕)']
|
||||
---
|
||||
|
||||
## 在debian上安装nginx
|
||||
debian默认软件库的nginx没有fancy-index模块, fancy-index是一个html文件服务器, 下面就开始手动给nginx添加fancy-index模块。
|
||||
## 1. 安装编译环境
|
||||
```shellscript
|
||||
包含pcre(正则库), zlib(压缩库), ssl(https)
|
||||
```shellscript title="shell"
|
||||
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev
|
||||
# 包含pcre(正则库), zlib(压缩库), ssl(https)
|
||||
```
|
||||
## 2. 下载并解压nginx源码
|
||||
官网查看最新版本(当前20250111为1.26.3)
|
||||
官网查看最新版本(当前20251111为1.29.3)
|
||||
https://nginx.org/en/download.html
|
||||
```shellscript
|
||||
wget https://nginx.org/download/nginx-1.26.3.tar.gz
|
||||
tar -xf nginx-1.26.3.tar.gz
|
||||
```shellscript title="shell"
|
||||
wget https://nginx.org/download/nginx-1.29.3.tar.gz
|
||||
tar -xf nginx-1.29.3.tar.gz
|
||||
```
|
||||
## 3. 下载并解压nginx-fancyindex模块
|
||||
```shellscript
|
||||
cd nginx-1.26.3
|
||||
```shellscript title="shell"
|
||||
cd nginx-1.29.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
|
||||
```
|
||||
## 4. 配置需要编译的模块
|
||||
```shellscript
|
||||
# NGINX_ROOT_PATH = nginx的安装目录, 需要替换成你想要安装的目录
|
||||
创建目录
|
||||
```shellscript title="shell"
|
||||
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=jimlee \
|
||||
--group=jimlee \
|
||||
--user=$(whoami) \
|
||||
--group=$(id -Gn) \
|
||||
--sbin-path=${NGINX_ROOT_PATH}/sbin/nginx \
|
||||
--conf-path=${NGINX_ROOT_PATH}/nginx.conf \
|
||||
--error-log-path=/var/log/nginx/error.log \
|
||||
@@ -68,15 +76,20 @@ mkdir -p /var/cache/nginx
|
||||
--with-stream \
|
||||
--with-stream_realip_module \
|
||||
--with-stream_ssl_module \
|
||||
--with-stream_ssl_preread_module
|
||||
--with-stream_ssl_preread_module \
|
||||
--with-http_v3_module
|
||||
```
|
||||
## 5. 编译
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
sudo make && sudo make install
|
||||
```
|
||||
## 6. 创建systemctl服务
|
||||
vim编辑service文件
|
||||
```systemd
|
||||
vim /usr/lib/systemd/system/nginx.service
|
||||
```
|
||||
输入如下内容
|
||||
```systemd
|
||||
[Unit]
|
||||
Description=nginx
|
||||
After=network.target
|
||||
@@ -92,11 +105,11 @@ PrivateTmp=true
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
## 7. 通过systemctl管理nginx
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
systemctl start nginx
|
||||
systemctl status nginx
|
||||
```
|
||||
## 8. 修改setcap让普通用户可以使用1024以下端口
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
setcap cap_net_bind_service=+eip ${NGINX_ROOT_PATH}/sbin/nginx
|
||||
```
|
||||
|
||||
@@ -2,20 +2,39 @@
|
||||
title: '常用代码块合集'
|
||||
description: '常用的kotlin, python, shell, regex等代码块合集'
|
||||
date: 2025-04-10
|
||||
tags: ['kotlin', 'python', 'regex', 'linux', 'shell', 'javascript']
|
||||
tags: ['kotlin', 'python', 'regex', 'linux', 'shell', 'javascript', 'nginx']
|
||||
image: 'assets/commoncodebanner.webp'
|
||||
authors: ['jimlee']
|
||||
authors: ['胡萝北(🥕)']
|
||||
---
|
||||
|
||||
## kotlin
|
||||
### 添加全局logger
|
||||
```kotlin
|
||||
```kotlin title="kotlin"
|
||||
val <T : Any> T.LOGGER: Logger
|
||||
get() = LoggerFactory.getLogger(this::class.java)
|
||||
```
|
||||
|
||||
## windows
|
||||
### 上传公钥到服务器
|
||||
```cmd title="cmd"
|
||||
type %USERPROFILE%\.ssh\id_rsa.pub | ssh user@host "cat >> ~/.ssh/authorized_keys"
|
||||
```
|
||||
|
||||
## linux
|
||||
### 打印文件树
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
find . -maxdepth 2 | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
|
||||
```
|
||||
### linux lo 网卡缺少ip
|
||||
```shellscript
|
||||
ip addr add 127.0.0.1/8 dev lo
|
||||
ip addr add ::1/128 dev lo
|
||||
```
|
||||
|
||||
## nginx
|
||||
### nginx反代后跳转变成原始ip+端口号
|
||||
加上如下配置
|
||||
```shellscript
|
||||
server_name_in_redirect off;
|
||||
port_in_redirect off;
|
||||
```
|
||||
22
src/content/blog/crackemby-2024/crackemby.mdx
vendored
@@ -4,7 +4,7 @@ description: '在dsm7.2上破解emby套件, 输入任意激活码即可获取小
|
||||
date: 2024-12-31
|
||||
tags: ['emby', 'synology', 'crack']
|
||||
image: 'assets/emby_banner.png'
|
||||
authors: ['jimlee']
|
||||
authors: ['胡萝北(🥕)']
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import Link from '@/components/Link.astro'
|
||||
@@ -17,7 +17,7 @@ import FileTree from '@/components/starlight/FileTree.astro'
|
||||
| 2 | Windows10/11 | [dnSpy](https://github.com/dnSpy/dnSpy/releases) |
|
||||
|
||||
## 1. SSH登录DSM, 切换到root用户
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
ssh {user}@{dsm_ip} // 输入密码
|
||||
sudo -i // 输入密码
|
||||
```
|
||||
@@ -44,18 +44,18 @@ sudo -i // 输入密码
|
||||

|
||||
找到与DSM对应的系统版本与系统架构, 我的系统是DSM7.2 x86_64
|
||||
复制下载链接, 使用wget下载到NAS临时目录/tmp/emby
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
mkdir -p /tmp/emby
|
||||
cd /tmp/emby
|
||||
wget --no-check-certificate https://github.com/MediaBrowser/Emby.Releases/releases/download/4.8.11.0/emby-server-synology72_4.8.11.0_x86_64.spk
|
||||
```
|
||||
## 3. 解包emby套件
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
mkdir output
|
||||
/usr/syno/sbin/synoarchive -xf 'emby-server-synology72_4.8.11.0_x86_64.spk' -C output
|
||||
```
|
||||
查看文件树
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
find . -maxdepth 2 | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
|
||||
```
|
||||
现在文件结构应该是这样
|
||||
@@ -81,7 +81,7 @@ find . -maxdepth 2 | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
|
||||
</FileTree>
|
||||
|
||||
## 4. 解压缩package.tgz
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
mkdir pkg
|
||||
tar -xf output/package.tgz -C pkg
|
||||
```
|
||||
@@ -227,19 +227,19 @@ rm /var/packages/EmbyServer/target/system/Emby.Server.Implementations.dll \
|
||||
- embypremiere.js
|
||||
</FileTree>
|
||||
在dsm终端执行
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
rm pkg/system/Emby.Server.Implementations.dll pkg/system/Emby.Web.dll pkg/system/MediaBrowser.Model.dll pkg/system/dashboard-ui/embypremiere/embypremiere.js
|
||||
```
|
||||
### 7.2 替换原文件
|
||||
先把修改好的文件上传回DSM, 再使用mv或cp命令移动到pkg/system下
|
||||
### 7.3 打包package.tgz
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
cd pkg
|
||||
tar -zcvf package.tgz *
|
||||
```
|
||||
|
||||
### 7.4 打包套件
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
cd ..
|
||||
rm -f output/package.tgz
|
||||
mv pkg/package.tgz output/
|
||||
@@ -250,13 +250,13 @@ mv emby.tar emby-server-synology72_4.8.11.0_x86_64_**unlock**.spk
|
||||
|
||||
## 8. 自动破解脚本
|
||||
### 8.1 ssh登录群晖, 切换到root用户
|
||||
```shellscript
|
||||
```shellscript title="shell"
|
||||
curl -LOk https://crackemby.2ha.me/sh && chmod +x emby.sh && bash emby.sh
|
||||
```
|
||||
|
||||
## 9. EmbyLisenceServer
|
||||
### 9.1 使用Nginx模拟
|
||||
```nginx
|
||||
```nginx title="nginx.conf"
|
||||
server {
|
||||
listen 80;
|
||||
server_name {你的域名};
|
||||
|
||||
BIN
src/content/blog/nginxfail2ban-2025/assets/nginxfail2ban.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
140
src/content/blog/nginxfail2ban-2025/nginxfail2ban.mdx
vendored
Normal file
|
After Width: | Height: | Size: 219 KiB |
404
src/content/blog/shikicodecopybutton-2025/shikicodecopybutton.mdx
vendored
Normal file
@@ -0,0 +1,404 @@
|
||||
---
|
||||
title: 'Astro集成Shikijs 和RehypePrettyCode踩坑'
|
||||
description: '1. shikijs重复依赖打包失败; 2. 使用Rehype Pretty Code/Copy Button 给markdown代码块添加复制按钮时发生错误Cannot read properties of undefined (reading "type")'
|
||||
date: 2025-05-12
|
||||
tags: ['typescript', 'astro', 'shiki']
|
||||
image: 'assets/shikicodecopybutton.png'
|
||||
authors: ['胡萝北(🥕)']
|
||||
---
|
||||
import FileTree from '@/components/starlight/FileTree.astro'
|
||||
|
||||
|
||||
# Shikijs重复依赖导致代码报错,打包失败
|
||||
## 执行 npm run build 后发生以下错误
|
||||
```log title="npm run build log"
|
||||
src/lib/transformerNotationSkip.ts - error ts(2322): Type 'import("/var/jenkins_home/workspace/dev.2ha.me/node_modules/@shikijs/transformers/node_modules/@shikijs/types/dist/index").ShikiTransformer' is not assignable to type 'import("/var/jenkins_home/workspace/dev.2ha.me/node_modules/@shikijs/types/dist/index").ShikiTransformer'.
|
||||
Types of property 'preprocess' are incompatible.
|
||||
|
||||
return createCommentNotationTransformer(
|
||||
~~~~~~
|
||||
|
||||
Result (78 files):
|
||||
- 1 error
|
||||
- 0 warnings
|
||||
- 0 hints
|
||||
```
|
||||
## 检查src/lib/transformerNotationSkip.ts代码
|
||||
这里引用的ShikiTransformer是/node_modules/@shikijs/types 中的 ShikiTransformer, 与 node_modules/@shikijs/transformers/node_modules/@shikijs/types 中的 ShikiTransformer 代码相同,但是引用不同
|
||||
```typescript title="TransformerNotationSkipOptions.ts"
|
||||
import { type ShikiTransformer } from '@shikijs/types';
|
||||
import { createCommentNotationTransformer } from '@shikijs/transformers'
|
||||
|
||||
export interface TransformerNotationSkipOptions {
|
||||
/**
|
||||
* Class for skipped lines
|
||||
*/
|
||||
classActiveSkip?: string
|
||||
/**
|
||||
* Class added to the root element when the code has skipped lines
|
||||
*/
|
||||
classActivePre?: string
|
||||
}
|
||||
|
||||
export function transformerNotationSkip(
|
||||
options: TransformerNotationSkipOptions = {},
|
||||
): ShikiTransformer {
|
||||
const { classActiveSkip = 'skip', classActivePre = undefined } = options
|
||||
|
||||
return createCommentNotationTransformer(
|
||||
'skip-lines',
|
||||
// comment-start | marker | range | comment-end
|
||||
/^\s*(?:\/\/|\/\*|<!--|#)\s+\[!code skip:(\d+):(\d+)\]\s*(?:\*\/|-->)?/,
|
||||
function ([_, start, end], _line) {
|
||||
_line.children = [{ type: 'text', value: `${start}-${end}` }]
|
||||
_line.properties = { style: `counter-set:line ${end}` }
|
||||
|
||||
if (classActiveSkip) this.addClassToHast(_line, classActiveSkip)
|
||||
if (classActivePre) this.addClassToHast(this.pre, classActivePre)
|
||||
return false
|
||||
},
|
||||
undefined, // remove empty lines
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
## 检查安装好依赖后的@shikijs目录
|
||||
<FileTree>
|
||||
- node_modules
|
||||
- @shikijs
|
||||
- engine-javascript
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
- types
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
- langs
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
- engine-oniguruma
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
- transformers
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- **node_modules** // 重复依赖最外层 node_modules 下的 @shikijs shiki
|
||||
- **shiki** // 与 node_modules/shiki 重复
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
- oniguruma-to-es
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- types
|
||||
- LICENSE
|
||||
- **@shikijs** // 与 node_modules/@shikijs 重复
|
||||
- engine-javascript
|
||||
- types
|
||||
- engine-oniguruma
|
||||
- vscode-textmate
|
||||
- core
|
||||
- LICENSE
|
||||
- themes
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
- vscode-textmate
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE.md
|
||||
- core
|
||||
- dist
|
||||
- README.md
|
||||
- package.json
|
||||
- LICENSE
|
||||
</FileTree>
|
||||
## 删除node_modules中的重复依赖
|
||||
```shellscript title="shell"
|
||||
rm -rf node_modules/@shikijs/transformers/node_modules/
|
||||
```
|
||||
|
||||
# 给Astro博客Markdown代码块添加复制按钮
|
||||
通过查找[Rehype Pretty Code文档](https://rehype-pretty.pages.dev/)找到 [Rehype Pretty Code/Copy Button](https://rehype-pretty.pages.dev/plugins/copy-button/)这个实验性功能
|
||||
## 1. 安装依赖
|
||||
```shellscript title="shell"
|
||||
npm install @rehype-pretty/transformers
|
||||
```
|
||||
## 2. 添加transformerCopyButton.ts
|
||||
这个ts文件是基于node_modules\@rehype-pretty\transformers\dist\copy-button.js修改而来
|
||||
```typescript title="transformerCopyButton.ts"
|
||||
import type { ShikiTransformer } from "shiki";
|
||||
import { h } from "hastscript";
|
||||
|
||||
export interface CopyButtonOptions {
|
||||
duration?: number;
|
||||
copyIcon?: string;
|
||||
successIcon?: string
|
||||
}
|
||||
|
||||
export const transformerCopyButton = (
|
||||
options: CopyButtonOptions = {
|
||||
duration: 1000
|
||||
}
|
||||
): ShikiTransformer => {
|
||||
return {
|
||||
name: 'shiki-transformer-copy-button',
|
||||
code(node) {
|
||||
const button = h('button', {
|
||||
class: 'shiki-transformer-button-copy',
|
||||
'data-code': this.source,
|
||||
onclick: `
|
||||
navigator.clipboard.writeText(this.dataset.code);
|
||||
this.classList.add('shiki-transformer-button-copied');
|
||||
setTimeout(() => this.classList.remove('shiki-transformer-button-copied'), ${options.duration})
|
||||
`
|
||||
}, [
|
||||
h('span', { class: 'ready' }),
|
||||
h('span', { class: 'success' })
|
||||
]);
|
||||
node.children.push(button)
|
||||
node.children.push({
|
||||
type: 'element',
|
||||
tagName: 'style',
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: buttonStyles({
|
||||
successIcon: options.successIcon,
|
||||
copyIcon: options.copyIcon
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buttonStyles({
|
||||
successIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M16 3h2.6A2.4 2.4 0 0 1 21 5.4v15.2a2.4 2.4 0 0 1-2.4 2.4H5.4A2.4 2.4 0 0 1 3 20.6V5.4A2.4 2.4 0 0 1 5.4 3H8m0 11l3 3l5-7M8.8 1h6.4a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-.8.8H8.8a.8.8 0 0 1-.8-.8V1.8a.8.8 0 0 1 .8-.8'/%3E%3C/svg%3E",
|
||||
copyIcon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='rgba(128,128,128,1)'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='2'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='8'%20height='4'%20x='8'%20y='2'%20rx='1'%20ry='1'/%3E%3Cpath%20d='M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2'/%3E%3C/svg%3E",
|
||||
}: {
|
||||
successIcon?: string,
|
||||
copyIcon?: string
|
||||
}) {
|
||||
let buttonStyle =
|
||||
`
|
||||
:root {
|
||||
--border-color: #e2e2e3;
|
||||
--background-color: #f6f6f7;
|
||||
--hover-background-color: #ffff
|
||||
}
|
||||
|
||||
pre:has(code) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre button.shiki-transformer-button-copy {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 3;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
background-color: var(--background-color);
|
||||
cursor: pointer;
|
||||
background-repeat: no-repeat;
|
||||
transition: var(--border-color) .25s, var(--background-color) .25s, opacity .25s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hover-background-color);
|
||||
}
|
||||
|
||||
& span {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
& .ready {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("${copyIcon}");
|
||||
}
|
||||
|
||||
& .success {
|
||||
display: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("${successIcon}");
|
||||
}
|
||||
|
||||
&.shiki-transformer-button-copied {
|
||||
& .success {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .ready {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}`
|
||||
return buttonStyle
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 添加插件
|
||||
```typescript title="astro.config.ts" {1,45,46,47,48,49}#a
|
||||
+ import { transformerCopyButton } from './src/lib/transformerCopyButton'
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://dev.2ha.me',
|
||||
integrations: [
|
||||
tailwind({
|
||||
applyBaseStyles: false,
|
||||
}),
|
||||
sitemap(),
|
||||
mdx(),
|
||||
react(),
|
||||
icon(),
|
||||
],
|
||||
markdown: {
|
||||
syntaxHighlight: false,
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
{
|
||||
target: '_blank',
|
||||
rel: ['nofollow', 'noreferrer', 'noopener'],
|
||||
},
|
||||
],
|
||||
rehypeHeadingIds,
|
||||
[
|
||||
rehypeKatex,
|
||||
{
|
||||
strict: false,
|
||||
},
|
||||
],
|
||||
sectionize as any,
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: {
|
||||
light: 'everforest-dark',
|
||||
dark: 'everforest-dark',
|
||||
},
|
||||
transformers: [
|
||||
transformerNotationDiff(),
|
||||
transformerMetaHighlight(),
|
||||
transformerRenderWhitespace(),
|
||||
transformerNotationSkip(),
|
||||
transformerDiffHighlight(),
|
||||
+ transformerCopyButton({
|
||||
+ duration: 1000,
|
||||
+ successIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E",
|
||||
+ copyIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E",
|
||||
+ })
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
remarkPlugins: [remarkToc, remarkMath, remarkEmoji],
|
||||
},
|
||||
server: {
|
||||
port: 1234,
|
||||
host: true,
|
||||
},
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
## 3. 启动项目后,访问blog后报错
|
||||
```log title="npm run build log"
|
||||
TypeError: Cannot read properties of undefined (reading 'type')
|
||||
at /vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:516:22
|
||||
at Array.flatMap (<anonymous>)
|
||||
at /vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:507:41
|
||||
at Array.forEach (<anonymous>)
|
||||
at Object.root (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:501:21)
|
||||
at tokensToHast (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/core/dist/index.mjs:1313:33)
|
||||
at codeToHast (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/core/dist/index.mjs:1188:10… …
|
||||
```
|
||||
|
||||
## 5. 修改node_modules/@shikijs/transformers依赖中的transformerRenderWhitespace方法
|
||||
```javascript title="node_modules/@shikijs/transformers/dist/index.mjs" {28,29,30}#a
|
||||
function transformerRenderWhitespace(options = {}) {
|
||||
const classMap = {
|
||||
" ": options.classSpace ?? "space",
|
||||
" ": options.classTab ?? "tab"
|
||||
};
|
||||
const position = options.position ?? "all";
|
||||
const keys = Object.keys(classMap);
|
||||
return {
|
||||
name: "@shikijs/transformers:render-whitespace",
|
||||
// We use `root` hook here to ensure it runs after all other transformers
|
||||
root(root) {
|
||||
const pre = root.children[0];
|
||||
const code = pre.children[0];
|
||||
code.children.forEach(
|
||||
(line) => {
|
||||
if (line.type !== "element")
|
||||
return;
|
||||
const elements = line.children.filter((token) => token.type === "element");
|
||||
const last = elements.length - 1;
|
||||
line.children = line.children.flatMap((token) => {
|
||||
if (token.type !== "element")
|
||||
return token;
|
||||
const index = elements.indexOf(token);
|
||||
if (position === "boundary" && index !== 0 && index !== last)
|
||||
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;
|
||||
const parts = splitSpaces(
|
||||
node.value.split(/([ \t])/).filter((i) => i.length),
|
||||
position === "boundary" && index === last && last !== 0 ? "trailing" : position,
|
||||
position !== "trailing"
|
||||
);
|
||||
if (parts.length <= 1)
|
||||
return token;
|
||||
return parts.map((part) => {
|
||||
const clone = {
|
||||
...token,
|
||||
properties: { ...token.properties }
|
||||
};
|
||||
clone.children = [{ type: "text", value: part }];
|
||||
if (keys.includes(part)) {
|
||||
this.addClassToHast(clone, classMap[part]);
|
||||
delete clone.properties.style;
|
||||
}
|
||||
return clone;
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
9
src/content/project.mdx
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
---
|
||||
|
||||
<h3 class="not-prose text-lg font-medium mb-1">
|
||||
Some work I’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>
|
||||
BIN
src/content/projects/assets/mmw.png
Normal file
|
After Width: | Height: | Size: 1013 KiB |
8
src/content/projects/miaomiaowu-clashmanager.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: '妙妙屋(个人clash订阅管理工具)'
|
||||
description: '妙妙屋是一个功能强大的Clash订阅管理平台,帮助您轻松管理订阅、节点和用户。'
|
||||
tags: ['open-source', 'personal', 'clash', 'substore']
|
||||
image: 'assets/mmw.png'
|
||||
link: 'https://miaomiaowu.net'
|
||||
startDate: '2025-10-10'
|
||||
---
|
||||
@@ -14,13 +14,14 @@ const { title, description, image } = Astro.props
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<html lang="zh" class="dark">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
|
||||
325
src/lib/data-utils.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
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
|
||||
}
|
||||
133
src/lib/transformerCopyButton.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { ShikiTransformer } from "shiki";
|
||||
import { h } from "hastscript";
|
||||
|
||||
/**
|
||||
* shikijs transformerRenderWhitespace 会报错 Cannot read properties of undefined (reading 'type'), 解决办法是
|
||||
* @shikijs/transformers/dist/index.mjs:516:22 加一个判断 提前返回
|
||||
* 如下
|
||||
* if (token.children.length === 0) {
|
||||
return token;
|
||||
}
|
||||
在这行代码之前加上面这段代码
|
||||
const node = token.children[0];
|
||||
if (node.type !== "text" || !node.value)
|
||||
return token;
|
||||
*/
|
||||
export interface CopyButtonOptions {
|
||||
duration?: number;
|
||||
copyIcon?: string;
|
||||
successIcon?: string
|
||||
}
|
||||
|
||||
export const transformerCopyButton = (
|
||||
options: CopyButtonOptions = {
|
||||
duration: 1000
|
||||
}
|
||||
): ShikiTransformer => {
|
||||
return {
|
||||
name: 'shiki-transformer-copy-button',
|
||||
code(node) {
|
||||
const button = h('button', {
|
||||
class: 'shiki-transformer-button-copy',
|
||||
'data-code': this.source,
|
||||
onclick: `
|
||||
navigator.clipboard.writeText(this.dataset.code);
|
||||
this.classList.add('shiki-transformer-button-copied');
|
||||
setTimeout(() => this.classList.remove('shiki-transformer-button-copied'), ${options.duration})
|
||||
`
|
||||
}, [
|
||||
h('span', { class: 'ready' }),
|
||||
h('span', { class: 'success' })
|
||||
]);
|
||||
node.children.push(button)
|
||||
node.children.push({
|
||||
type: 'element',
|
||||
tagName: 'style',
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: buttonStyles({
|
||||
successIcon: options.successIcon,
|
||||
copyIcon: options.copyIcon
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buttonStyles({
|
||||
successIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M16 3h2.6A2.4 2.4 0 0 1 21 5.4v15.2a2.4 2.4 0 0 1-2.4 2.4H5.4A2.4 2.4 0 0 1 3 20.6V5.4A2.4 2.4 0 0 1 5.4 3H8m0 11l3 3l5-7M8.8 1h6.4a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-.8.8H8.8a.8.8 0 0 1-.8-.8V1.8a.8.8 0 0 1 .8-.8'/%3E%3C/svg%3E",
|
||||
copyIcon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='rgba(128,128,128,1)'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='2'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='8'%20height='4'%20x='8'%20y='2'%20rx='1'%20ry='1'/%3E%3Cpath%20d='M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2'/%3E%3C/svg%3E",
|
||||
}: {
|
||||
successIcon?: string,
|
||||
copyIcon?: string
|
||||
}) {
|
||||
let buttonStyle =
|
||||
`
|
||||
:root {
|
||||
--border-color: #e2e2e3;
|
||||
--background-color: #f6f6f7;
|
||||
--hover-background-color: #ffff
|
||||
}
|
||||
|
||||
pre:has(code) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre button.shiki-transformer-button-copy {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 3;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
background-color: var(--background-color);
|
||||
cursor: pointer;
|
||||
background-repeat: no-repeat;
|
||||
transition: var(--border-color) .25s, var(--background-color) .25s, opacity .25s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hover-background-color);
|
||||
}
|
||||
|
||||
& span {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
& .ready {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("${copyIcon}");
|
||||
}
|
||||
|
||||
& .success {
|
||||
display: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("${successIcon}");
|
||||
}
|
||||
|
||||
&.shiki-transformer-button-copied {
|
||||
& .success {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .ready {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}`
|
||||
return buttonStyle
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function transformerNotationSkip(
|
||||
if (classActivePre) this.addClassToHast(this.pre, classActivePre)
|
||||
return false
|
||||
},
|
||||
false, // remove empty lines
|
||||
)
|
||||
undefined, // remove empty lines
|
||||
) as ShikiTransformer
|
||||
|
||||
}
|
||||
|
||||
@@ -13,13 +13,36 @@ export function formatDate(date: Date) {
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
export function readingTime(html: string) {
|
||||
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
|
||||
const textOnly = html.replace(/<[^>]+>/g, '')
|
||||
const wordCount = textOnly.split(/\s+/).length
|
||||
const readingTimeMinutes = (wordCount / 200 + 1).toFixed()
|
||||
return textOnly.split(/\s+/).filter(Boolean).length
|
||||
}
|
||||
|
||||
export function readingTime(wordCount: number): string {
|
||||
const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200))
|
||||
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()
|
||||
@@ -33,3 +56,12 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const authors = await getCollection('authors')
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="max-w-[80px]">Path</TableHead>
|
||||
<TableHead className="max-w-[80px] flex items-center">Illustrator</TableHead>
|
||||
<TableHead className="max-w-[80px] flex items-center">Author</TableHead>
|
||||
<TableHead className="text-right">Resources</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -134,6 +134,19 @@ const authors = await getCollection('authors')
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Background</TableCell>
|
||||
<TableCell className="flex items-center">
|
||||
<Link class="contents" href="https://mall.bilibili.com/neul-next/index.html?page=mall-up_itemDetail&noTitleBar=1&itemsId=1107984035&from=items_share&msource=items_share" target="_blank">
|
||||
@小猫女仆降临 <svg class="size-8" style="filter: invert(100%);" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24"><title>Bilibili</title><path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/></svg>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Link href="https://www.bilibili.com/video/BV1462uY8Eo4" target="_blank">
|
||||
<img class="h-12 w-25" src="/static/images/maoliang.gif" />
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,8 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === post.data.title)
|
||||
const totalBody = [post.body!, ...subposts.map((p) => p.body!)]
|
||||
.map(stripCodeBlocks)
|
||||
.join('')
|
||||
const readTime = readingTime(totalBody)
|
||||
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
|
||||
const readTime = readingTime(wordCount)
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
||||
@@ -57,35 +57,24 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
|
||||
---
|
||||
|
||||
<Layout title="Blog" description="Blog">
|
||||
<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%);"
|
||||
>
|
||||
<!-- 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"
|
||||
>
|
||||
<Link
|
||||
href={`/blog`}
|
||||
class="hover:text-primary-500 dark:hover:text-primary-500 font-bold uppercase"
|
||||
class="block mb-2 font-bold uppercase text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
All Posts
|
||||
</Link>
|
||||
<ul>
|
||||
<ul class="space-y-0.5">
|
||||
{
|
||||
sortedTags.map((t) => {
|
||||
return (
|
||||
<li value={t} class="my-0">
|
||||
<li value={t}>
|
||||
<Link
|
||||
href={`/tags/${slug(t)}`}
|
||||
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"
|
||||
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"
|
||||
aria-label={`View posts tagged ${t}`}
|
||||
>
|
||||
{`${t} (${tagCounts[t]})`}
|
||||
@@ -95,22 +84,25 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<span
|
||||
class="ms-auto inline-flex h-6 items-center text-base sm:text-end"
|
||||
<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/"
|
||||
>
|
||||
<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>
|
||||
View all →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
|
||||
<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">
|
||||
{
|
||||
years.map((year) => (
|
||||
<section class="flex flex-col gap-y-4">
|
||||
@@ -125,13 +117,31 @@ 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>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
---
|
||||
import AuthorPresence from '@/components/bento/AuthorPresence'
|
||||
import WakatimeGraph from '@/components/bento/WakatimeGraph.tsx'
|
||||
import WakatimeGraph from '@/components/bento/WakatimeGraph'
|
||||
import Link from '@/components/Link.astro'
|
||||
import FuzzyText from '@/components/magicui/fuzzy-text'
|
||||
// import FuzzyText from '@/components/magicui/fuzzy-text'
|
||||
import GradientText from '@/components/magicui/gradint-text'
|
||||
import LetterGlitch from '@/components/magicui/letter-glitch'
|
||||
import ShortCuts from '@/components/ShortCuts.astro'
|
||||
import { SITE, SOCIAL_LINKS } from '@/consts'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { getCollection } from 'astro:content'
|
||||
import GiteaCalendar from '@/components/bento/GiteaCalendar'
|
||||
import Music163Player from '@/components/bento/Music163Player'
|
||||
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'
|
||||
|
||||
const latestPost = await getCollection('blog').then((posts: any[]) =>
|
||||
posts
|
||||
@@ -22,86 +24,48 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
|
||||
.filter((post) => !post.data.hidden && !post.data.draft)
|
||||
.at(0),
|
||||
)
|
||||
|
||||
---
|
||||
|
||||
<Layout title="主页" description={SITE.DESCRIPTION}>
|
||||
<section
|
||||
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"
|
||||
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"
|
||||
aria-label="Personal information and activity grid"
|
||||
>
|
||||
>
|
||||
<div
|
||||
class="first flex flex-row justify-center aspect-square rounded-3xl border bg-[url('/static/images/tou.gif')] bg-cover bg-color bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] sm:bg-[url('/static/images/tou.gif')] xl:aspect-auto"
|
||||
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)]"
|
||||
role="img"
|
||||
aria-label="Introduction"
|
||||
style="filter: contrast(2) saturate(0) invert(100) sepia(100); border-color: #afafaf;"
|
||||
>
|
||||
<!-- <div
|
||||
class="overlay size-full rounded-3xl bg-[url('/static/404.webp')] bg-cover bg-center bg-no-repeat opacity-0 transition-opacity duration-200"
|
||||
>
|
||||
</div> -->
|
||||
<!-- class="first relative flex aspect-square justify-center rounded-3xl border [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto" -->
|
||||
<!-- <img
|
||||
class="no-repeat relative w-full max-w-fit justify-center rounded-3xl object-cover"
|
||||
src="/static/404_white_mask.webp"
|
||||
/> -->
|
||||
<!-- <div class="absolute self-end p-2 justify-center">
|
||||
<FuzzyText
|
||||
client:load
|
||||
baseIntensity={0.04}
|
||||
hoverIntensity={0.24}
|
||||
fontSize={96}
|
||||
color="#fbb229"
|
||||
,>4O4</FuzzyText
|
||||
>
|
||||
<div class="mb-3 mt-3"></div>
|
||||
<FuzzyText
|
||||
client:load
|
||||
baseIntensity={0.04}
|
||||
hoverIntensity={0.24}
|
||||
fontSize={36}
|
||||
color="#f95038">Not Found</FuzzyText
|
||||
>
|
||||
</div> -->
|
||||
|
||||
aria-label="Introduction"
|
||||
>
|
||||
<RandomAnimeBackground client:load/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="has-overlay relative grid aspect-square grid-cols-4 grid-rows-3 items-center
|
||||
justify-center bg-[url('/static/honeycomb.webp')] [grid-area:b]"
|
||||
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)]"
|
||||
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 no-repeat w-full max-w-fit justify-center rounded-3xl object-cover z-9"
|
||||
src="/static/images/shortcuts-bg.png"
|
||||
class="overlay absolute bottom-0 right-0 no-repeat max-w-[28%] object-contain z-[88]"
|
||||
src="/static/images/shortcuts-bg-mini.png"
|
||||
/>
|
||||
<!-- <DevStackIconsCloud client:load/> -->
|
||||
<ShortCuts />
|
||||
<!-- <DevShortCuts /> -->
|
||||
<DevShortcutsHexagon />
|
||||
</div>
|
||||
|
||||
<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]">
|
||||
<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)]">
|
||||
<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"
|
||||
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)]"
|
||||
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-center 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-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
|
||||
>
|
||||
</div>
|
||||
{
|
||||
@@ -114,7 +78,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 rounded-2xl border border-border sm:ml-1 sm:w-[82%] "
|
||||
class="w-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] sm:w-[82%]"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
@@ -134,7 +98,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">
|
||||
<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">
|
||||
<Icon name="lucide:move-up-right" size={16} />
|
||||
</div>
|
||||
</Link>
|
||||
@@ -144,36 +108,30 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="has-overlay relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto"
|
||||
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)]"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
</div>
|
||||
<GiteaCalendar client:load />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none"
|
||||
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)]"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
</div>
|
||||
<Music163Player client:load />
|
||||
</div>
|
||||
|
||||
<div
|
||||
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] "
|
||||
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)]"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
</div>
|
||||
<Icon
|
||||
@@ -195,12 +153,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 ">
|
||||
<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)]">
|
||||
<WakatimeGraph omitLanguages={['Markdown', 'JSON']} client:load />
|
||||
</div>
|
||||
|
||||
<!-- 字符滚动 -->
|
||||
<div class="aspect-square [grid-area:k]">
|
||||
<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)]">
|
||||
<LetterGlitch
|
||||
client:load
|
||||
glitchColors={['#de17a5', '#5617de', '#e9d3b6']}
|
||||
@@ -213,3 +171,21 @@ 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>
|
||||
|
||||
57
src/pages/projects.astro
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,12 +1,16 @@
|
||||
@import './pixel-components.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'haipaiqiangdiaosenxiyuan';
|
||||
src: url('/fonts/haipaiqiangdiaosenxiyuan.woff');
|
||||
font-weight: 100 800;
|
||||
font-family: 'OPlusSans3-Medium';
|
||||
src: url('/fonts/OPlusSans3-Medium.woff2') format('woff2'),
|
||||
url('/fonts/OPlusSans3-Medium.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
|
||||
}
|
||||
|
||||
@@ -32,100 +36,117 @@
|
||||
|
||||
@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%;
|
||||
/* 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%;
|
||||
--additive: 112 50% 36%;
|
||||
--additive-foreground: 0 0% 9%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive: 0 72% 51%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--border: 15 30% 55% / 0.38;
|
||||
--ring: 15 66% 59% / 0.6;
|
||||
--input: 15 30% 55% / 0.45;
|
||||
--radius: 0; /* 无圆角! */
|
||||
|
||||
--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-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-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%;
|
||||
--card: 60 20% 99%;
|
||||
--card-foreground: 16 68% 13%;
|
||||
--popover: 60 20% 99%;
|
||||
--popover-foreground: 16 62% 14%;
|
||||
}
|
||||
|
||||
.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%;
|
||||
/* 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%;
|
||||
--additive: 112 50% 36%;
|
||||
--additive-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive: 0 72% 51%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--border: 15 30% 55% / 0.38;
|
||||
--ring: 15 66% 59% / 0.6;
|
||||
--input: 15 30% 55% / 0.45;
|
||||
--radius: 0;
|
||||
|
||||
--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-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-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;
|
||||
--card: 60 20% 99%;
|
||||
--card-foreground: 16 68% 13%;
|
||||
--popover: 60 20% 99%;
|
||||
--popover-foreground: 16 62% 14%;
|
||||
}
|
||||
|
||||
.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%;
|
||||
/* 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 */
|
||||
--additive: 112 50% 36%;
|
||||
--additive-foreground: 0 0% 9%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive: 0 70% 68%; /* #f87171 亮红 */
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--border: 0 0% 100% / 0.08; /* 半透明白 */
|
||||
--ring: 15 71% 69% / 0.45;
|
||||
--input: 0 0% 100% / 0.12;
|
||||
--radius: 0;
|
||||
|
||||
--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-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-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%;
|
||||
--card: 222 29% 8%;
|
||||
--card-foreground: 22 75% 94%;
|
||||
--popover: 222 29% 8%;
|
||||
--popover-foreground: 33 83% 97%;
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -136,6 +157,7 @@
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
overflow-y: scroll;
|
||||
@apply bg-background text-foreground forced-color-adjust-none;
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
@@ -304,159 +326,109 @@
|
||||
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 bg-background text-foreground;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,4 +489,9 @@
|
||||
|
||||
.bg-color {
|
||||
background-color: #e9d3b6 !important;
|
||||
}
|
||||
|
||||
.shiki-transformer-button-copy {
|
||||
border: none !important;
|
||||
background-color: hsl(0deg 0% 0% / 0%) !important;
|
||||
}
|
||||
525
src/styles/global.css.backup
Normal file
@@ -0,0 +1,525 @@
|
||||
@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;
|
||||
}
|
||||
252
src/styles/pixel-components.css
Normal file
@@ -0,0 +1,252 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const config: Config = {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'haipaiqiangdiaosenxiyuan',
|
||||
'OPlusSans3-Medium',
|
||||
...defaultTheme.fontFamily.sans
|
||||
],
|
||||
mono: [
|
||||
@@ -19,6 +19,18 @@ 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))'
|
||||
@@ -63,9 +75,10 @@ const config: Config = {
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
lg: '0',
|
||||
md: '0',
|
||||
sm: '0',
|
||||
DEFAULT: '0'
|
||||
},
|
||||
animation: {
|
||||
orbit: 'orbit calc(var(--duration)*1s) linear infinite',
|
||||
|
||||