Compare commits

...
This repository has been archived on 2025-05-12. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

43 commits

Author SHA1 Message Date
Timothy Rogers
81c60d8931 adding new metrics service 2025-01-15 20:39:09 -05:00
Timothy Rogers
054d8e3bb8 Adding a new post plus some upgrades from upstream 2025-01-15 18:56:31 -05:00
hack13
73d324bec1 Merge remote-tracking branch 'github.com/master' 2024-08-23 09:35:57 -04:00
hack13
32467fdbc9 Adding new post + upgrade astro 2024-08-23 09:35:15 -04:00
Timothy Rogers
54cdc43981
Adding site metrics 2024-08-08 08:01:57 -04:00
Timothy Rogers
e4a725b781 New post 2024-08-07 21:51:27 -04:00
Timothy Rogers
a74a0539f2 Update footer to include astro 2024-08-07 21:51:16 -04:00
Timothy Rogers
aa8e43fb48 fixing some posts 2024-07-08 21:22:18 -04:00
Timothy Rogers
306cd23ed2 new post 2024-07-07 13:52:15 -04:00
Timothy Rogers
1fb6b46b17 fixing date on post 2024-07-07 12:56:33 -04:00
Timothy Rogers
8263030f87 new post for moving to astro 2024-07-07 12:54:17 -04:00
Timothy Rogers
5995155073 adding support for old links 2024-07-07 12:54:06 -04:00
Timothy Rogers
a06878c84c add redirect for feed from wordpress 2024-07-07 09:24:36 -04:00
Timothy Rogers
eb845d0f1b Fixing description 2024-07-07 00:13:28 -04:00
Timothy Rogers
6b38e6edc9 adding correct domain 2024-07-06 23:58:36 -04:00
Timothy Rogers
4e1e514545 maybe this time 2024-07-06 23:51:37 -04:00
Timothy Rogers
e387e942f7 hopefully fixing pnpm 2024-07-06 23:43:05 -04:00
Timothy Rogers
7b04ed640c fix build... 2024-07-06 23:31:10 -04:00
Timothy Rogers
46daf98523 switched to astro 2024-07-06 23:03:52 -04:00
hack13
9c8264f186
add new post for MFF 2023-12-06 21:17:16 -05:00
hack13
2d4958223e
updated to point at personal git 2023-09-16 11:38:32 -04:00
hack13
235ee86a2f
updating baseURL 2023-09-16 11:26:42 -04:00
hack13
fc5c5bab76
would help if I put used correct dir 2023-09-16 11:13:48 -04:00
hack13
c2b5feae5c
oops missed one 2023-09-16 11:12:24 -04:00
hack13
6b04f8ff09
fixing to point at github for action step 2023-09-16 11:11:11 -04:00
hack13
527d554bf0
new blog post 2023-09-16 11:05:58 -04:00
hack13
01a832e34e
removing github runner 2023-09-16 11:05:46 -04:00
hack13
ad20d08cb2
migrate to forgejo runners 2023-09-16 11:05:20 -04:00
Timothy Rogers
e5b4b8f929 new blog post
Signed-off-by: Timothy Rogers <github@hack13.me>
2023-05-26 21:23:35 -04:00
Timothy Rogers
0eea14836d
update title 2023-05-03 10:06:34 -04:00
Timothy Rogers
9eb456d04b
added new post 2023-05-03 10:04:04 -04:00
Timothy Rogers
be7f38bf8a
Create README.md 2023-03-23 10:37:16 -04:00
Timothy Rogers
01ffc0df29 more oops fixes 2023-03-12 16:34:32 -04:00
Timothy Rogers
a761cc2736 fixing variables, oops 2023-03-12 16:33:24 -04:00
Timothy Rogers
396c93adcf trying different tool 2023-03-12 16:31:49 -04:00
Timothy Rogers
a5ef469a24 updated github action 2023-03-12 16:18:55 -04:00
Timothy Rogers
dd5f6926dd fixing mistake 2023-01-24 09:15:14 -05:00
Timothy Rogers
a9a779655e updating rss stuff 2023-01-24 09:11:43 -05:00
Timothy Rogers
c9a258d345 switching back, but adding perm fixer 2023-01-22 14:28:37 -05:00
Timothy Rogers
6dd0da0e88 changing out hug builder 2023-01-22 14:21:51 -05:00
Timothy Rogers
0286443b89 Updating blog to auto-deploy and new post 2023-01-22 14:14:31 -05:00
hack13
faadb821d3 updating to be blog focused version of site 2023-01-22 08:14:31 -05:00
Timothy Rogers
4f61252525 attempt to fix UTF-8 chars 2022-09-22 08:13:37 -04:00
226 changed files with 16898 additions and 19484 deletions

77
.gitignore vendored
View file

@ -1,68 +1,23 @@
# Build and Release Folders
bin/
bin-debug/
bin-release/
[Oo]bj/ # FlashDevelop obj
[Bb]in/ # FlashDevelop bin
# build output
dist/
# Other files and folders
.settings/
.hugo_build.lock
.nova/
# generated types
.astro/
# Executables
*.swf
*.air
*.ipa
*.apk
# dependencies
node_modules/
# Hugo
public/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Backup Files
*.markdown~
*.md~
# environment variables
.env
.env.production
# Apache
.htpasswd
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Build directories
public
dist
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
package-lock.json
bower_components
# Mac File System File...utterly useless to anyone but me.
# macOS-specific files
.DS_Store
# AWS stuff for publishing static files
.aws-credentials.json
.awspublish*
.vercel

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome", "astro-build.astro-vscode"]
}

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"quickfix.biome": "always",
"source.organizeImports.biome": "always"
},
"frontMatter.dashboard.openOnStart": false
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 saicaca
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

58
README.es.md Normal file
View file

@ -0,0 +1,58 @@
# 🍥Fuwari
Un tema estático para blogs construido con [Astro](https://astro.build).
[**🖥️ Demostración en Vivo (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦 Versión Antigua de Hexo**](https://github.com/saicaca/hexo-theme-vivia)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
> Versión del README: `2024-04-07`
![Imagen de Vista Previa](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ Características
- [x] Construido con [Astro](https://astro.build) y [Tailwind CSS](https://tailwindcss.com)
- [x] Animaciones suaves y transiciones de página
- [x] Modo claro / oscuro
- [x] Colores del tema y banner personalizables
- [x] Diseño responsivo
- [ ] Comentarios
- [x] Buscador
- [ ] TOC (Tabla de Contenidos)
## 🚀 Cómo Usar
1. [Genera un nuevo repositorio](https://github.com/saicaca/fuwari/generate) desde esta plantilla o haz un fork de este repositorio.
2. Para editar tu blog localmente, clona tu repositorio, ejecuta `pnpm install` y `pnpm add sharp` para instalar las dependencias.
- Instala [pnpm](https://pnpm.io) `npm install -g pnpm` si aún no lo tienes.
3. Edita el archivo de configuración `src/config.ts` para personalizar tu blog.
4. Ejecuta `pnpm new-post <nombre-de-archivo>` para crear una nueva entrada y edítala en `src/content/posts/`.
5. Despliega tu blog en Vercel, Netlify, GitHub Pages, etc., siguiendo [las guías](https://docs.astro.build/en/guides/deploy/). Necesitas editar la configuración del sitio en `astro.config.mjs` antes del despliegue.
## ⚙️ Cabecera de las Entradas
```yaml
---
title: Mi Primer Post en el Blog
published: 2023-09-09
description: Esta es la primera entrada de mi nuevo blog con Astro.
image: /images/cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
```
## 🧞 Comandos
Todos los comandos se ejecutan desde la raíz del proyecto, desde una terminal:
| Comando | Acción |
|:------------------------------------|:--------------------------------------------------|
| `pnpm install` y `pnpm add sharp` | Instala las dependencias |
| `pnpm dev` | Inicia el servidor de desarrollo local en `localhost:4321` |
| `pnpm build` | Compila tu web para producción en `./dist/` |
| `pnpm preview` | Previsualiza la web localmente, antes del despliegue |
| `pnpm new-post <nombre-de-archivo>` | Crea una nueva entrada |
| `pnpm astro ...` | Ejecuta comandos CLI como `astro add`, `astro check` |
| `pnpm astro --help` | Obtén ayuda para usar el CLI de Astro |

55
README.ja-JP.md Normal file
View file

@ -0,0 +1,55 @@
# 🍥Fuwari
[Astro](https://astro.build)で構築された静的ブログテンプレート
[**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**🌏中文**](https://github.com/saicaca/fuwari/blob/main/README.zh-CN.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**🌏日本語**](https://github.com/saicaca/fuwari/blob/main/README.ja-JP.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**📦旧Hexoバージョン**](https://github.com/saicaca/hexo-theme-vivia)
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ 特徴
- [x] [Astro](https://astro.build)及び [Tailwind CSS](https://tailwindcss.com)で構築
- [x] スムーズなアニメーションとページ遷移
- [x] ライト/ダークテーマ対応
- [x] カスタマイズ可能なテーマカラーとバナー
- [x] レスポンシブデザイン
- [ ] コメント機能
- [x] 検索機能
- [ ] 目次
## 🚀 使用方法
1. [テンプレート](https://github.com/saicaca/fuwari/generate)から新しいリポジトリを作成するかCloneをします。
2. ブログをローカルで編集するには、リポジトリをクローンした後、`pnpm install``pnpm add sharp` を実行して依存関係をインストールします。
- [pnpm](https://pnpm.io)がインストールされていない場合は `npm install -g pnpm` で導入可能です。
3. `src/config.ts`ファイルを編集する事でブログを自分好みにカスタマイズ出来ます。
4. `pnpm new-post <filename>`で新しい記事を作成し、`src/content/posts/`.フォルダ内で編集します。
5. 作成したブログをVercel、Netlify、GitHub Pagesなどにデプロイするには[ガイド](https://docs.astro.build/ja/guides/deploy/)に従って下さい。加えて、別途デプロイを行う前に`astro.config.mjs`を編集してサイト構成を変更する必要があります。
## ⚙️ 記事のフロントマター
```yaml
---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: /images/cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
```
## 🧞 コマンド
すべてのコマンドは、ターミナルでプロジェクトのルートから実行する必要があります:
| Command | Action |
|:------------------------------------|:-------------------------------------------------|
| `pnpm install` AND `pnpm add sharp` | 依存関係のインストール |
| `pnpm dev` | `localhost:4321`で開発用ローカルサーバーを起動 |
| `pnpm build` | `./dist/`にビルド内容を出力 |
| `pnpm preview` | デプロイ前の内容をローカルでプレビュー |
| `pnpm new-post <filename>` | 新しい投稿を作成 |
| `pnpm astro ...` | `astro add`, `astro check`の様なコマンドを実行する際に使用 |
| `pnpm astro --help` | Astro CLIのヘルプを表示 |

57
README.ko.md Normal file
View file

@ -0,0 +1,57 @@
# 🍥Fuwari
[Astro](https://astro.build)로 구축된 정적 블로그 템플릿입니다.
[**🖥️미리보기 (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia)
> README 버전: `2024-04-07`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ 특징
- [x] [Astro](https://astro.build) 및 [Tailwind CSS](https://tailwindcss.com)로 구축됨
- [x] 부드러운 애니메이션 및 페이지 전환
- [x] 라이트 모드 / 다크 모드
- [x] 사용자 정의 가능한 테마 색상 및 배너
- [x] 반응형 디자인
- [ ] 댓글
- [x] 검색
- [ ] 목차
## 🚀 사용하는 방법
1. 이 템플릿에서 [새 저장소를 생성](https://github.com/saicaca/fuwari/generate)하거나 이 저장소를 포크하세요.
2. 블로그를 로컬에서 편집하려면 저장소를 복제하고 `pnpm install``pnpm add sharp`를 실행하여 종속성을 설치하세요.
- 아직 [pnpm](https://pnpm.io)을 설치하지 않았다면 `npm install -g pnpm`을 실행하여 [pnpm](https://pnpm.io)을 설치하세요.
3. 블로그를 사용자 정의하려면 `src/config.ts` 구성 파일을 편집하세요.
4. `pnpm new-post <filename>`을 실행하여 새 게시물을 만들고 `src/content/posts/`에서 편집하세요.
5. [가이드](https://docs.astro.build/en/guides/deploy/)에 따라 블로그를 Vercel, Netlify, GitHub 페이지 등에 배포하세요. 배포하기 전에 `astro.config.mjs`에서 사이트 구성을 편집해야 합니다.
## ⚙️ 게시물의 머리말 설정
```yaml
---
title: 내 첫 블로그 게시물
published: 2023-09-09
description: 내 새로운 Astro 블로그의 첫 번째 게시물입니다!
image: /images/cover.jpg
tags: [푸, 바, 오]
category: 앞-끝
draft: false
---
```
## 🧞 명령어
모든 명령어는 프로젝트 최상단, 터미널에서 실행됩니다:
| Command | Action |
|:------------------------------------|:-------------------------------------------------|
| `pnpm install` AND `pnpm add sharp` | 종속성을 설치합니다. |
| `pnpm dev` | `localhost:4321`에서 로컬 개발 서버를 시작합니다. |
| `pnpm build` | `./dist/`에 프로덕션 사이트를 구축합니다. |
| `pnpm preview` | 배포하기 전에 로컬에서 빌드 미리보기 |
| `pnpm new-post <filename>` | 새 게시물 작성 |
| `pnpm astro ...` | `astro add`, `astro check`와 같은 CLI 명령어 실행 |
| `pnpm astro --help` | Astro CLI를 사용하여 도움 받기 |

55
README.md Normal file
View file

@ -0,0 +1,55 @@
# 🍥Fuwari
A static blog template built with [Astro](https://astro.build).
[**🖥Live Demo (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**🌏中文 README**](https://github.com/saicaca/fuwari/blob/main/README.zh-CN.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**🌏日本語 README**](https://github.com/saicaca/fuwari/blob/main/README.ja-JP.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**📦Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia)
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ Features
- [x] Built with [Astro](https://astro.build) and [Tailwind CSS](https://tailwindcss.com)
- [x] Smooth animations and page transitions
- [x] Light / dark mode
- [x] Customizable theme colors & banner
- [x] Responsive design
- [ ] Comments
- [x] Search
- [ ] TOC
## 🚀 How to Use
1. [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template or fork this repository.
2. To edit your blog locally, clone your repository, run `pnpm install` AND `pnpm add sharp` to install dependencies.
- Install [pnpm](https://pnpm.io) `npm install -g pnpm` if you haven't.
3. Edit the config file `src/config.ts` to customize your blog.
4. Run `pnpm new-post <filename>` to create a new post and edit it in `src/content/posts/`.
5. Deploy your blog to Vercel, Netlify, GitHub Pages, etc. following [the guides](https://docs.astro.build/en/guides/deploy/). You need to edit the site configuration in `astro.config.mjs` before deployment.
## ⚙️ Frontmatter of Posts
```yaml
---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: /images/cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
```
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
|:------------------------------------|:-------------------------------------------------|
| `pnpm install` AND `pnpm add sharp` | Installs dependencies |
| `pnpm dev` | Starts local dev server at `localhost:4321` |
| `pnpm build` | Build your production site to `./dist/` |
| `pnpm preview` | Preview your build locally, before deploying |
| `pnpm new-post <filename>` | Create a new post |
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm astro --help` | Get help using the Astro CLI |

59
README.th.md Normal file
View file

@ -0,0 +1,59 @@
# 🍥Fuwari
แม่แบบสำหรับเว็บบล็อกแบบ static สร้างด้วย [Astro](https://astro.build)
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦 เวอร์ชั่นเก่าสำหรับ Hexo**](https://github.com/saicaca/hexo-theme-vivia)
> เวอร์ชั่นของ README: `2024-09-10`
![ภาพตัวอย่าง](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ คุณสมบัติ
- [x] สร้างด้วย [Astro](https://astro.build) และ [Tailwind CSS](https://tailwindcss.com)
- [x] มีอนิเมชั่นและการเปลี่ยนหน้าอย่างลื่นไหล
- [x] รองรับโหมดสว่าง / โหมดมืด
- [x] ปรับแต่งสีธีมและแบนเนอร์ได้
- [x] Responsive design (หน้าตาเว็บปรับเปลี่ยนตามขนาดจอ)
- [ ] การแสดงความคิดเห็น
- [x] การค้นหา
- [ ] TOC (สารบัญ)
## 🚀 วิธีใช้งาน
1. [Generate repository ใหม่](https://github.com/saicaca/fuwari/generate)ขึ้นมาจากแม่แบบนี้ หรือจะ fork repository นี้ก็ได้
2. เริ่มแก้ไขบล็อกของคุณแบบ local โดยการ clone repository ของคุณ (จากข้อ 1) ไว้ในเครื่องของคุณ แล้วรันคำสั่ง `pnpm install` และ `pnpm add sharp` เพื่อติดตั้ง dependencies ที่จำเป็น
- ติดตั้ง [pnpm](https://pnpm.io) ด้วยคำสั่ง `npm install -g pnpm` ถ้ายังไม่เคยติดตั้ง
3. แก้ไขไฟล์การตั้งค่า `src/config.ts` เพื่อปรับแต่งบล็อกของคุณ
4. รันคำสั่ง `pnpm new-post <filename>` เพื่อสร้างโพสต์ใหม่ใน `src/content/posts/` และแก้ไขไฟล์โพสต์นั้นๆ ให้สมบูรณ์
5. Deploy เว็บบล็อกของคุณไปยัง Vercel, Netlify, GitHub Pages หรือบริการอื่นๆ โดยอ้างอิงวิธีการจาก[คู่มือนี้](https://docs.astro.build/en/guides/deploy/) อย่าลืมแก้ไขการตั้งค่าเว็บไซต์ในไฟล์ `astro.config.mjs` ก่อนที่คุณจะ deploy เว็บ
## ⚙️ Frontmatter ของโพสต์
```yaml
---
title: โพสต์แรกของฉัน
published: 2023-09-09
description: นี่คือโพสต์แรกของเว็บบล็อก Astro อันใหม่ของฉัน
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # เขียนค่านี้เมื่อภาษาของโพสต์นั้นๆ แตกต่างจากภาษาของเว็บไซต์ที่ตั้งค่าไว้ใน `config.ts` เท่านั้น
---
```
## 🧞 คำสั่ง
คำสั่งที่รันได้ใน terminal จาก root ของโปรเจค:
| คำสั่ง | ผลที่เกิด |
|:------------------------------------|:--------------------------------------------------|
| `pnpm install` และ `pnpm add sharp` | ติดตั้ง dependencies |
| `pnpm dev` | เปิดเซิร์ฟเวอร์เพื่อพัฒนาเว็บแบบ local ที่ `localhost:4321` |
| `pnpm build` | Build เว็บไซต์แบบพร้อมใช้งานจริงไปยังโฟลเดอร์ `./dist/` |
| `pnpm preview` | ดูตัวอย่าง build ของคุณแบบ local ก่อนที่จะ deploy จริง |
| `pnpm new-post <filename>` | สร้างโพสต์ใหม่ |
| `pnpm astro ...` | รันคำสั่ง CLI เช่น `astro add`, `astro check` |
| `pnpm astro --help` | ดูข้อมูลเพิ่มเติมเกี่ยวกับ Astro CLI |

55
README.zh-CN.md Normal file
View file

@ -0,0 +1,55 @@
# 🍥Fuwari
基于 [Astro](https://astro.build) 开发的静态博客模板。
[**🖥在线预览Vercel**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**🌏English README**](https://github.com/saicaca/fuwari)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**🌏日本語 README**](https://github.com/saicaca/fuwari/blob/main/README.ja-JP.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;[**📦旧 Hexo 版本**](https://github.com/saicaca/hexo-theme-vivia)
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ 功能特性
- [x] 基于 Astro 和 Tailwind CSS 开发
- [x] 流畅的动画和页面过渡
- [x] 亮色 / 暗色模式
- [x] 自定义主题色和横幅图片
- [x] 响应式设计
- [ ] 评论
- [x] 搜索
- [ ] 文内目录
## 🚀 使用方法
1. 使用此模板[生成新仓库](https://github.com/saicaca/fuwari/generate)或 Fork 此仓库
2. 进行本地开发Clone 新的仓库,执行 `pnpm install``pnpm add sharp` 以安装依赖
- 若未安装 [pnpm](https://pnpm.io),执行 `npm install -g pnpm`
3. 通过配置文件 `src/config.ts` 自定义博客
4. 执行 `pnpm new-post <filename>` 创建新文章,并在 `src/content/posts/` 目录中编辑
5. 参考[官方指南](https://docs.astro.build/zh-cn/guides/deploy/)将博客部署至 Vercel, Netlify, GitHub Pages 等;部署前需编辑 `astro.config.mjs` 中的站点设置。
## ⚙️ 文章 Frontmatter
```yaml
---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: /images/cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
```
## 🧞 指令
下列指令均需要在项目根目录执行:
| Command | Action |
|:----------------------------------|:----------------------------------|
| `pnpm install``pnpm add sharp` | 安装依赖 |
| `pnpm dev` | 在 `localhost:4321` 启动本地开发服务器 |
| `pnpm build` | 构建网站至 `./dist/` |
| `pnpm preview` | 本地预览已构建的网站 |
| `pnpm new-post <filename>` | 创建新文章 |
| `pnpm astro ...` | 执行 `astro add`, `astro check` 等指令 |
| `pnpm astro --help` | 显示 Astro CLI 帮助 |

View file

@ -1,6 +0,0 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

192
astro.config.mjs Normal file
View file

@ -0,0 +1,192 @@
import sitemap from '@astrojs/sitemap'
import svelte from '@astrojs/svelte'
import tailwind from '@astrojs/tailwind'
import swup from '@swup/astro'
import Compress from 'astro-compress'
import icon from 'astro-icon'
import { defineConfig } from 'astro/config'
import Color from 'colorjs.io'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeComponents from 'rehype-components' /* Render the custom directive content */
import rehypeKatex from 'rehype-katex'
import rehypeSlug from 'rehype-slug'
import remarkDirective from 'remark-directive' /* Handle directives */
import remarkGithubAdmonitionsToDirectives from 'remark-github-admonitions-to-directives'
import remarkMath from 'remark-math'
import { AdmonitionComponent } from './src/plugins/rehype-component-admonition.mjs'
import { GithubCardComponent } from './src/plugins/rehype-component-github-card.mjs'
import { parseDirectiveNode } from './src/plugins/remark-directive-rehype.js'
import { remarkReadingTime } from './src/plugins/remark-reading-time.mjs'
const oklchToHex = str => {
const DEFAULT_HUE = 250
const regex = /-?\d+(\.\d+)?/g
const matches = str.string.match(regex)
const lch = [matches[0], matches[1], DEFAULT_HUE]
return new Color('oklch', lch).to('srgb').toString({
format: 'hex',
})
}
// https://astro.build/config
export default defineConfig({
site: 'https://hack13.blog/',
base: '/',
trailingSlash: 'always',
redirects: {
'/feed': '/rss.xml',
'/2010/06/new-blog': '/posts/new-blog',
'/2012/04/been-a-while': '/posts/been-a-while',
'/2012/04/my-take-on-opensim-grids': '/posts/my-take-on-opensim-grids',
'/2012/04/my-vision-of-the-hypergrid': '/posts/my-vision-of-the-hypergrid',
'/2012/04/we-have-hypergrid-use-it': '/posts/we-have-hypergrid-use-it',
'/2012/05/coming-changes': '/posts/coming-changes',
'/2012/06/timothys-thoughts': '/posts/timothys-thoughts',
'/2012/07/finding-my-place': '/posts/finding-my-place',
'/2012/07/lil-about-me-some-rage': '/posts/lil-about-me-some-rage',
'/2012/08/battle-of-business-vs-opensource':
'/posts/2012-08-09-battle-of-business-vs-opensource',
'/2012/12/new-years-resolutions': '/posts/2012-12-31-new-years-resolutions',
'/2013/02/explaining-opensim-memory-usage':
'/posts/2013-02-11-explaining-opensim-memory-usage',
'/2013/02/thank-you': '/posts/2013-02-13-thank-you',
'/2013/02/needing-rest': '/posts/2013-02-20-needing-rest',
'/2013/02/top-10-reasons-not-to-give-up-on-aurora-sim':
'/posts/2013-02-21-top-10-reasons-not-to-give-up-on-aurora-sim',
'/2013/02/who-is-timothy-vyperhoxleyrogers':
'/posts/2013-02-23-who-is-timothy-vyperhoxleyrogers',
'/2013/03/why-i-closed-aurorascape':
'/posts/2013-03-05-why-i-closed-aurorascape',
'/2013/03/closed-vs-open-really': '/posts/2013-03-11-closed-vs-open-really',
'/2013/04/moving-up-around': '/posts/2013-04-08-moving-up-around',
'/2013/04/my-clocks-are-ticking': '/posts/2013-04-26-my-clocks-are-ticking',
'/2013/06/upset-with-stiffled-innovation':
'/posts/2013-06-19-upset-with-stiffled-innovation',
'/2013/08/depression': '/posts/2013-08-12-depression',
'/2013/09/my-response-to-kitely-marketplace':
'/posts/2013-09-02-my-response-to-kitely-marketplace',
'/2013/11/why-i-dont-use-the-cloud':
'/posts/2013-11-03-why-i-dont-use-the-cloud',
'/2016/01/moved-to-wordpress': '/posts/2016-01-04-moved-to-wordpress',
'/2018/01/where-has-tim-been': '/posts/2018-01-06-where-has-tim-been',
'/2018/01/taking-control-of-my-cloud-storage':
'/posts/2018-01-14-taking-control-of-my-cloud-storage',
'/2018/04/finding-my-linux-distro':
'/posts/2018-04-26-finding-my-linux-distro',
'/2019/01/2019-goals': '/posts/2019-01-26-2019-goals',
'/2019/01/speeding-up-zadaroo-files':
'/posts/2019-01-28-speeding-up-zadaroo-files',
'/2021/10/cloudflare-workers-and-pages':
'/posts/2021-10-29-cloudflare-workers-and-pages',
'/2022/02/struggling-with-anxiety':
'/posts/2022-02-02-struggling-with-anxiety',
'/2022/02/nft-polarization': '/posts/2022-02-17-nft-polarization',
'/2022/04/neosvr-event-hosting': '/posts/2022-04-17-neosvr-event-hosting',
'/2022/05/continuing-to-find-myself':
'/posts/2022-05-08-continuing-to-find-myself',
'/2023/05/decentralized-protocols':
'/posts/2023-05-03-decentralized-protocols',
'/2023/09/perspectives': '/posts/2023-09-16-upset-over-crypto',
'/2023/12/mff-2023-fedi-panel': '/posts/2023-12-06-mff-2023-panel',
},
integrations: [
tailwind(),
swup({
theme: false,
animationClass: 'transition-',
containers: ['main'],
smoothScrolling: true,
cache: true,
preload: true,
accessibility: true,
globalInstance: true,
}),
icon({
include: {
'material-symbols': ['*'],
'fa6-brands': ['*'],
'fa6-regular': ['*'],
'fa6-solid': ['*'],
},
}),
Compress({
Image: false,
}),
svelte(),
sitemap(),
],
markdown: {
remarkPlugins: [
remarkMath,
remarkReadingTime,
remarkGithubAdmonitionsToDirectives,
remarkDirective,
parseDirectiveNode,
],
rehypePlugins: [
rehypeKatex,
rehypeSlug,
[
rehypeComponents,
{
components: {
github: GithubCardComponent,
note: (x, y) => AdmonitionComponent(x, y, 'note'),
tip: (x, y) => AdmonitionComponent(x, y, 'tip'),
important: (x, y) => AdmonitionComponent(x, y, 'important'),
caution: (x, y) => AdmonitionComponent(x, y, 'caution'),
warning: (x, y) => AdmonitionComponent(x, y, 'warning'),
},
},
],
[
rehypeAutolinkHeadings,
{
behavior: 'append',
properties: {
className: ['anchor'],
},
content: {
type: 'element',
tagName: 'span',
properties: {
className: ['anchor-icon'],
'data-pagefind-ignore': true,
},
children: [
{
type: 'text',
value: '#',
},
],
},
},
],
],
},
vite: {
build: {
rollupOptions: {
onwarn(warning, warn) {
// temporarily suppress this warning
if (
warning.message.includes('is dynamically imported by') &&
warning.message.includes('but also statically imported by')
) {
return
}
warn(warning)
},
},
},
css: {
preprocessorOptions: {
stylus: {
define: {
oklchToHex: oklchToHex,
},
},
},
},
},
})

66
biome.json Normal file
View file

@ -0,0 +1,66 @@
{
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"extends": [],
"files": { "ignoreUnknown": true },
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"ignore": [],
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
},
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "single",
"trailingComma": "all",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded"
}
},
"json": {
"parser": { "allowComments": true },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
}
},
"linter": {
"ignore": [],
"rules": {
"a11y": {
"recommended": true
},
"complexity": {
"recommended": true
},
"correctness": {
"recommended": true
},
"performance": {
"recommended": true
},
"security": {
"recommended": true
},
"style": {
"recommended": true
},
"suspicious": {
"recommended": true
},
"nursery": {
"recommended": true
}
}
}
}

View file

@ -1,19 +0,0 @@
baseURL = "https://hack13.me"
languageCode = "en-us"
title = "Hack13 Site"
theme = "hacksite"
sectionPagesMenu = "main"
[menu]
[[menu.main]]
identifier = "home"
name = "Home"
title = "home"
weight = "10"
url = "/"
[[menu.main]]
identifier = "posts"
name = "Blog"
title = "posts"
weight = "15"
url = "/posts/"

View file

@ -1,53 +0,0 @@
---
title: "About Me"
menu:
main:
weight: 40
---
# About Me
### Short Bio
Why hello there, I am Hack13... Just some fox you met on the internet, or stumbled upon. I have am a Linux and UNIX SysAdmin by day and hobbyist developer by night.
I also love playing in VR, my current favorite platform is NeosVR followed by VRChat from time to time. I enjoy the more nerdy community and flexibility NeosVR gives
to me as a person who loves to tinker and make things. However, I know that VRChat is slowly becoming a bit better with allowing things like NeosVR but I don't ever
think the platforms will be the same.
Where do I go from here? I am not fully sure, I am still figuring myself out. I have recently come out as Non-Binary, and I am still discovering myself, and exploring
the more feminine side of myself. We shall see what happens and where it goes, and don't be affraind to say hello!
### Contact Info
{{< rawhtml >}}
<div class="container">
<center>
<div class="row">
<div class="col">
<i class="fab fa-telegram-plane fa-5x"></i><br><a href="https://t.me/kite5521" target="_blank">@kite5521</a>
</div>
<div class="col">
<i class="fab fa-discord fa-5x"></i><br>@hack13#4761
</div>
<div class="col">
<i class="fab fa-mastodon fa-5x"></i><br><a href="https://meow.social/@hack13" target="_blank">@hack13</a>
</div>
<div class="col">
<i class="fab fa-twitter fa-5x"></i><br><a href="https://twitter.com/kite552" target="_blank">@kite552</a>
</div>
</div>
<br>
<div class="row">
<div class="col">
<i class="fab fa-github fa-5x"></i><br><a href="https://github.com/hack13" target="_blank">hack13</a>
</div>
<div class="col">
<i class="fab fa-gitlab fa-5x"></i><br><a href="https://gitlab.com/hack13" target="_blank">hack13</a>
</div>
<div class="col">
<i class="fab fa-lastfm fa-5x"></i><br><a href="https://www.last.fm/user/dothacker552" target="_blank">dothacker552</a>
</div>
<div class="col">
<i class="far fa-envelope-open fa-5x"></i><br><a href="mailto:me@hack13.me">me<i class="fas fa-at"></i>hack13.me</a>
</div>
</div>
</center>
</div>
{{< /rawhtml >}}

View file

@ -1,72 +0,0 @@
---
title: "API"
menu:
main:
weight: 25
---
# API Docs
{{< rawhtml >}}
<div class="container">
<div class="accordion" id="accordionExample">
<div class="card">
<div class="card-header" id="headingOne">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
LastFM - For NeosVR
</button>
</h2>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordionExample">
<div class="card-body">
<strong>URI:</strong> <code>https://api.hack13.dev/lastfm/?username=<i>your-lastfm-username</i></code>
<br />
<strong>Example Response:</strong>
<pre><code>
"PUNCHING BAG","blackwinterwells","https://www.last.fm/music/blackwinterwells/_/PUNCHING+BAG",true
</code></pre>
<strong>Response Breakdown:</strong>
<pre><code>
<i>"Song Name"</i>,<i>"Artist Name"</i>,<i>"LastFM Song Page Link"</i>,<i>Now Playing</i>
</code></pre>
<ul>
<li>Song Name <span class="badge badge-info">String</span></li>
<li>Artist Name <span class="badge badge-info">String</span></li>
<li>Song LastFM Page Link <span class="badge badge-info">String/URL</span></li>
<li>Now Playing <span class="badge badge-info">Bool</span></li>
</ul>
<strong>API Source Code:</strong> <a href="https://gist.github.com/hack13/1e6c9340815814e9286b5d2ee566bb50" target="_blank">GitHub Gist</a>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
Coming Soon ...
</button>
</h2>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionExample">
<div class="card-body">
Nothing here yet...
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingThree">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
Coming Soon ...
</button>
</h2>
</div>
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordionExample">
<div class="card-body">
Nothing here yet...
</div>
</div>
</div>
</div>
</div>
{{< /rawhtml >}}

View file

@ -1,83 +0,0 @@
---
title: Coming Changes
author: Timothy Rogers
type: post
date: 2012-05-05T11:17:00+00:00
url: /2012/05/coming-changes/
categories:
- Archived
---
{{< rawhtml >}}
<div dir="ltr">
</p>
<table cellpadding="0" cellspacing="0">
</p>
<tr>
<td>
<a href="http://3.bp.blogspot.com/-vkIcs3lVHyQ/T6S5NvtVEfI/AAAAAAAAAQE/o-Dk6As1Ly4/s1600/Snapshot_001.png"><img loading="lazy" border="0" height="167" src="https://3.bp.blogspot.com/-vkIcs3lVHyQ/T6S5NvtVEfI/AAAAAAAAAQE/o-Dk6As1Ly4/s320/Snapshot_001.png" width="320" /></a>
</td>
</tr>
<p>
</p>
<tr>
<td>
Me reading in our new office location in OSgrid
</td>
</tr>
<p>
</tbody> </table>
<p>
As you might have heard, I have recently moved my entire grid to OSgrid. This was a decision made my grid residents, and I was inclined to agree as I want to go back to focusing on community related efforts for the HyperGrid. So moving everything to OSgrid, allows me to not worry about any of the issues, just worry about all the work I need to get done for the community.
</p>
<p>
I am not just working on the furry community for the hypergrid, but I want to bring together everyone, all walks of life. I am still working on creating a site, that will help residents of all hypergrid enabled grids come together and interact on a social network level. I feel the reason we are all having troubles of the hypergrid, is we are all spread to thin. We need to increase ourselves in community, to do this we just need to pull all our numbers together.
</p>
<p>
I know that I cannot please everyone all the time, so I am still going to be offering some really nice deals on OSgrid region hosting. I will be introducing a few new features and places to the sites I have been wanting to work on for bringing the hypergrid community all together.
</p>
<div>
</div>
<p>
</p>
<div>
</p>
<div>
</div>
<table align="center" cellpadding="0" cellspacing="0">
</p>
<tr>
<td>
<a href="http://4.bp.blogspot.com/-9hL4j-axGZM/T6S5PbIZhcI/AAAAAAAAAQU/7jwUHX9stNo/s1600/Snapshot_003.png"><img loading="lazy" border="0" height="167" src="https://4.bp.blogspot.com/-9hL4j-axGZM/T6S5PbIZhcI/AAAAAAAAAQU/7jwUHX9stNo/s320/Snapshot_003.png" width="320" /></a>
</td>
</tr>
<p>
</p>
<tr>
<td>
Enjoying the view of the new regions from up above
</td>
</tr>
<p>
</tbody> </table> </div>
<p>
</div>
{{< /rawhtml >}}

View file

@ -1,68 +0,0 @@
---
title: "Privacy"
menu:
main:
weight: 50
---
# Projects
{{< rawhtml >}}
<div class="container">
<h2>Privacy Policy of <span class="website_url">https://zadaroo.com & https://hack13.me</span></h2>
<p>At <span class="website_name">Zadaroo</span>, we collect and manage user data according to the following Privacy Policy.</p>
<h3>Data Collected</h3>
<p>We collect information you provide directly to us. For example, we collect information when you create an account, subscribe, participate in any interactive features of our services, fill out a form, request customer support or otherwise communicate with us. The types of information we may collect include your name, email address, postal address, credit card information and other contact or identifying information you choose to provide.</p>
<p>We collect anonymous data from every visitor of the Website to monitor traffic and fix bugs. For example, we collect information like web requests, the data sent in response to such requests, the Internet Protocol address, the browser type, the browser language, and a timestamp for the request.</p>
<p>We also use various technologies to collect information, and this may include sending cookies to your computer. Cookies are small data files stored on your hard drive or in your device memory that helps us to improve our services and your experience, see which areas and features of our services are popular and count visits. We may also collect information using web beacons (also known as "tracking pixels"). Web beacons are electronic images that may be used in our services or emails and to track count visits or understand usage and campaign effectiveness. Our Privacy Policy was created with the help of the <a href="https://www.privacy-policy-template.com">Privacy Policy Template</a> and the <a href="https://www.generateprivacypolicy.com">Generate Privacy Policy Generator</a>.</p>
<h3>Use of the Data</h3>
<p>We only use your personal information to provide you the <span class="website_name">Zadaroo</span> services or to communicate with you about the Website or the services.</p>
<p>We employ industry standard techniques to protect against unauthorized access of data about you that we store, including personal information.</p>
<p>We do not share personal information you have provided to us without your consent, unless:</p>
<ul>
<li>Doing so is appropriate to carry out your own request</li>
<li>We believe it's needed to enforce our legal agreements or that is legally required</li>
<li>We believe it's needed to detect, prevent or address fraud, security or technical issues</li>
</ul>
<h3>Sharing of Data</h3>
<p>We don't share your personal information with third parties. Aggregated, anonymized data is periodically transmitted to external services to help us improve the Website and service.</p>
<p>We may allow third parties to provide analytics services. These third parties may use cookies, web beacons and other technologies to collect information about your use of the services and other websites, including your IP address, web browser, pages viewed, time spent on pages, links clicked and conversion information.</p>
<p>We also use social buttons provided by services like Twitter, Google+, LinkedIn and Facebook. Your use of these third party services is entirely optional. We are not responsible for the privacy policies and/or practices of these third party services, and you are responsible for reading and understanding those third party services' privacy policies.</p>
<h3>Cookies</h3>
<p>We may use cookies on our site to remember your preferences.</p>
<p>For more general information on cookies, please read <a href="https://www.cookieconsent.com/what-are-cookies/">"What Are Cookies"</a>.</p>
<h3>Opt-Out, Communication Preferences</h3>
<p>You may modify your communication preferences and/or opt-out from specific communications at any time. Please specify and adjust your preferences.</p>
<h3>Security</h3>
<p>We take reasonable steps to protect personally identifiable information from loss, misuse, and unauthorized access, disclosure, alteration, or destruction. But, you should keep in mind that no Internet transmission is ever completely secure or error-free. In particular, email sent to or from the Sites may not be secure.</p>
<h3>About Children</h3>
<p>The Website is not intended for children under the age of 13. We do not knowingly collect personally identifiable information via the Website from visitors in this age group.</p>
<h3>Changes to the Privacy Policy</h3>
<p>We may amend this Privacy Policy from time to time. Use of information we collect now is subject to the Privacy Policy in effect at the time such information is used.</p>
<p>If we make major changes in the way we collect or use information, we will notify you by posting an announcement on the Website or sending you an email.</p>
</div>
{{< /rawhtml >}}

View file

@ -1,97 +0,0 @@
---
title: "Projects"
menu:
main:
weight: 30
---
# Projects
{{< rawhtml >}}
<div class="container">
<div class="row">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">About</th>
<th scope="col">Tags</th>
<th scope="col">Link(s)</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Cyberfurz</th>
<td>Just a collection of services I offer for free for furries of the metaverse.</td>
<td><span class="badge badge-info">Photos</span> <span class="badge badge-info">Service</span></td>
<td>
<a href="https://cyberfurz.com" target="_blank" class="btn btn-secondary">Visit</a>
</td>
</tr>
<tr>
<th scope="row">Zadaroo</th>
<td>Free public OpenSimulator resources licensed under CC0.</td>
<td><span class="badge badge-info">OpenSimulator</span> <span class="badge badge-info">Service</span></td>
<td>
<a href="https://zadaroo.com" target="_blank" class="btn btn-secondary">Visit</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3>Code:</h3>
<div class="container">
<div class="row">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">About</th>
<th scope="col">Tags</th>
<th scope="col">Link(s)</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Ansible OpenNIC Setup</th>
<td>Simple Ansible Playbook to install and configure an OpenNIC Tier 2 node on multiple distributions of Linux.</td>
<td><span class="badge badge-info">Ansible Playbook</span> <span class="badge badge-info">Automation</span></td>
<td>
<a href="https://github.com/hack13/ansible-opennic-setup" target="_blank" class="btn btn-secondary"><i class="fab fa-github"></i></a>
</td>
<td><h4><span class="badge badge-warning">Un-Maintained</span></h4></td>
</tr>
<tr>
<th scope="row">Ansible OpenSim Grid</th>
<td>Grid Setup for OpenSim using Ansible on CentOS 7</td>
<td><span class="badge badge-info">Ansible Playbook</span> <span class="badge badge-info">Automation</span></td>
<td>
<a href="https://github.com/hack13/ansible-opensim-grid" target="_blank" class="btn btn-secondary"><i class="fab fa-github"></i></a>
</td>
<td><h4><span class="badge badge-danger">Discontinued</span></h4></td>
</tr>
<tr>
<th scope="row">OpenSim Libre Panel</th>
<td>OpenSimulator Standalone/Grid Management System</td>
<td><span class="badge badge-info">PHP</span> <span class="badge badge-info">SQL</span></td>
<td>
<a href="https://github.com/hack13/opensim-libre-panel" target="_blank" class="btn btn-secondary"><i class="fab fa-github"></i></a>
</td>
<td><h4><span class="badge badge-danger">Discontinued</span></h4></td>
</tr>
<tr>
<th scope="row">Zadaroo</th>
<td>A static redesign of the Zadaroo site, to provide a more streamlined site.</td>
<td><span class="badge badge-info">HTML</span> <span class="badge badge-info">JavaScript</span></td>
<td>
<a href="https://github.com/hack13/Zadaroo" target="_blank" class="btn btn-secondary"><i class="fab fa-github"></i></a>
</td>
<td><h4><span class="badge badge-success">Completed</span></h4></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{{< /rawhtml >}}

62
frontmatter.json Normal file
View file

@ -0,0 +1,62 @@
{
"$schema": "https://frontmatter.codes/frontmatter.schema.json",
"frontMatter.framework.id": "astro",
"frontMatter.preview.host": "http://localhost:4321",
"frontMatter.content.publicFolder": "public",
"frontMatter.content.pageFolders": [
{
"title": "posts",
"path": "[[workspace]]/src/content/posts"
}
],
"frontMatter.taxonomy.contentTypes": [
{
"name": "default",
"pageBundle": true,
"previewPath": "'blog'",
"filePrefix": null,
"clearEmpty": true,
"fields": [
{
"title": "title",
"name": "title",
"type": "string",
"single": true
},
{
"title": "description",
"name": "description",
"type": "string"
},
{
"title": "published",
"name": "published",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "preview",
"name": "image",
"type": "image",
"isPreviewImage": true
},
{
"title": "tags",
"name": "tags",
"type": "list"
},
{
"title": "category",
"name": "category",
"type": "string"
},
{
"title": "draft",
"name": "draft",
"type": "boolean"
}
]
}
]
}

View file

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
{{- partial "header.html" . -}}
<br>
<main role="main">
<div class="container">
{{- block "main" . }}{{- end }}
<h1>{{ .Params.title }}</h1>
<strong>Author:</strong> {{ .Params.author }} | <strong>Published:</strong> {{ .Date.Format "Jan 2, 2006" }} <hr/>
{{ .Content }}
<button class="btn btn-secondary" onclick="window.history.go(-1)">&#8701; Go Back</button>
<br/>
</div>
</main>
{{- partial "footer.html" . -}}
</body>
</html>

View file

@ -1,2 +0,0 @@
<!-- raw html -->
{{.Inner}}

72
package.json Normal file
View file

@ -0,0 +1,72 @@
{
"name": "fuwari",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build && pagefind --site dist",
"preview": "astro preview",
"astro": "astro",
"new-post": "node scripts/new-post.js",
"format": "biome format --write ./src",
"lint": "biome check --apply ./src"
},
"dependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/rss": "^4.0.7",
"@astrojs/sitemap": "^3.1.6",
"@astrojs/svelte": "^5.7.0",
"@astrojs/tailwind": "^5.1.0",
"@fontsource-variable/jetbrains-mono": "^5.0.21",
"@fontsource/roboto": "^5.0.13",
"@swup/astro": "^1.4.1",
"astro": "^4.14.5",
"astro-compress": "^2.2.28",
"astro-icon": "1.1.0",
"colorjs.io": "^0.5.0",
"hastscript": "^9.0.0",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.8.3",
"pagefind": "^1.1.0",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-components": "^0.3.0",
"rehype-katex": "^7.0.0",
"rehype-slug": "^6.0.0",
"remark-directive": "^3.0.0",
"remark-directive-rehype": "^0.4.2",
"remark-math": "^6.0.0",
"sanitize-html": "^2.13.0",
"sharp": "^0.33.4",
"svelte": "^4.2.18",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.2",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.1",
"@biomejs/biome": "1.8.2",
"@iconify-json/fa6-brands": "^1.1.19",
"@iconify-json/fa6-regular": "^1.1.19",
"@iconify-json/fa6-solid": "^1.1.21",
"@iconify-json/material-symbols": "^1.1.82",
"@iconify/svelte": "^4.0.2",
"@rollup/plugin-yaml": "^4.1.2",
"@tailwindcss/typography": "^0.5.13",
"@types/markdown-it": "^14.1.1",
"@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.11.0",
"remark-github-admonitions-to-directives": "^1.0.5",
"sass": "^1.77.6",
"stylus": "^0.63.0"
},
"pnpm": {
"overrides": {
"vite-imagetools": "^6.2.7",
"sharp": "^0.33.0"
}
},
"packageManager": "pnpm@9.5.0-beta.3+sha512.8b0c4d7fd9937d73f06ad284e681ddfd0f08b8f792a4f854a3ea24d1c7ade57dfe952ce88ed174d57da694e8323f0bb5a25011f780ed2787759c647b35263fa1"
}

10581
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

11
postcss.config.mjs Normal file
View file

@ -0,0 +1,11 @@
import postcssImport from 'postcss-import';
import postcssNesting from 'tailwindcss/nesting/index.js';
import tailwindcss from 'tailwindcss';
export default {
plugins: {
'postcss-import': postcssImport, // to combine multiple css files
'tailwindcss/nesting': postcssNesting,
tailwindcss: tailwindcss,
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

View file

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 565 KiB

After

Width:  |  Height:  |  Size: 565 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 588 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 525 KiB

After

Width:  |  Height:  |  Size: 525 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 673 KiB

After

Width:  |  Height:  |  Size: 673 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Before After
Before After

52
scripts/new-post.js Normal file
View file

@ -0,0 +1,52 @@
/* This is a script to create a new post markdown file with front-matter */
import fs from "fs"
import path from "path"
function getDate() {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, "0")
const day = String(today.getDate()).padStart(2, "0")
return `${year}-${month}-${day}`
}
const args = process.argv.slice(2)
if (args.length === 0) {
console.error(`Error: No filename argument provided
Usage: npm run new-post -- <filename>`)
process.exit(1) // Terminate the script and return error code 1
}
let fileName = args[0]
// Add .md extension if not present
const fileExtensionRegex = /\.(md|mdx)$/i
if (!fileExtensionRegex.test(fileName)) {
fileName += ".md"
}
const targetDir = "./src/content/posts/"
const fullPath = path.join(targetDir, fileName)
if (fs.existsSync(fullPath)) {
console.error(`ErrorFile ${fullPath} already exists `)
process.exit(1)
}
const content = `---
title: ${args[0]}
published: ${getDate()}
description: ''
image: ''
tags: []
category: ''
draft: false
---
`
fs.writeFileSync(path.join(targetDir, fileName), content)
console.log(`Post ${fullPath} created`)

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 KiB

BIN
src/assets/images/hack-avatar.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

View file

@ -0,0 +1,128 @@
---
import {getSortedPosts} from "../utils/content-utils";
import {getPostUrlBySlug} from "../utils/url-utils";
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {UNCATEGORIZED} from "@constants/constants";
interface Props {
keyword: string;
tags: string[];
categories: string[];
}
const { keyword, tags, categories} = Astro.props;
let posts = await getSortedPosts()
if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter(post =>
Array.isArray(post.data.tags) && post.data.tags.some(tag => tags.includes(tag))
);
}
if (Array.isArray(categories) && categories.length > 0) {
posts = posts.filter(post =>
(post.data.category && categories.includes(post.data.category)) ||
(!post.data.category && categories.includes(UNCATEGORIZED))
);
}
const groups = function () {
const groupedPosts = posts.reduce((grouped, post) => {
const year = post.data.published.getFullYear()
if (!grouped[year]) {
grouped[year] = []
}
grouped[year].push(post)
return grouped
}, {})
// convert the object to an array
const groupedPostsArray = Object.keys(groupedPosts).map(key => ({
year: key,
posts: groupedPosts[key]
}))
// sort years by latest first
groupedPostsArray.sort((a, b) => b.year - a.year)
return groupedPostsArray;
}();
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${month}-${day}`;
}
function formatTag(tag: string[]) {
return tag.map(t => `#${t}`).join(' ');
}
---
<div class="card-base px-8 py-6">
{
groups.map(group => (
<div>
<div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">{group.year}</div>
<div class="w-[15%] md:w-[10%]">
<div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div>
</div>
<div class="w-[70%] md:w-[80%] transition text-left text-50">{group.posts.length} {i18n(I18nKey.postsCount)}</div>
</div>
{group.posts.map(post => (
<a href={getPostUrlBySlug(post.slug)}
aria-label={post.data.title}
class="group btn-plain block h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]
"
></div>
</div>
<!-- post title -->
<div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
>
{post.data.title}
</div>
<!-- tag list -->
<div class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden
text-30"
>{formatTag(post.data.tags)}</div>
</div>
</a>
))}
</div>
))
}
</div>
<style>
@tailwind components;
@tailwind utilities;
@layer components {
.dash-line {
}
.dash-line::before {
content: "";
@apply w-[10%] h-full absolute -top-1/2 left-[calc(50%_-_1px)] -top-[50%] border-l-[2px]
border-dashed pointer-events-none border-[var(--line-color)] transition
}
}
</style>

View file

@ -0,0 +1,8 @@
---
import {siteConfig} from "../config";
---
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}>
</div>

13
src/components/Footer.astro Executable file
View file

@ -0,0 +1,13 @@
---
import { profileConfig } from '../config'
---
<div class="card-base max-w-[var(--page-width)] min-h-[4.5rem] rounded-b-none mx-auto flex items-center px-6">
<div class="transition text-50 text-sm">
© 2024 {profileConfig.name}. All Rights Reserved.
<br>
Powered by <a class="link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a> & <a class="link text-[var(--primary)] font-medium" target="_blank" href="https://astro.build">Astro</a>
</div>
</div>
<script defer src="https://stats.hack13.cloud/script.js" data-website-id="d1001cd6-aa86-4ae0-82ca-8562331a4c96"></script>

View file

@ -0,0 +1,298 @@
---
---
<div>
<slot/>
</div>
<style is:global lang="stylus">
/* utils */
white(a)
rgba(255, 255, 255, a)
black(a)
rgba(0, 0, 0, a)
isOklch(c)
return substr(c, 0, 5) == 'oklch'
oklch_fallback(c)
str = '' + c // convert color value to string
if isOklch(str)
return convert(oklchToHex(str))
return c
color_set(colors)
@supports (color: oklch(0 0 0))
:root
for key, value in colors
{key}: value[0]
:root.dark
for key, value in colors
if length(value) > 1
{key}: value[1]
/* provide fallback color for oklch */
@supports not (color: oklch(0 0 0))
:root
for key, value in colors
{key}: oklch_fallback(value[0])
:root.dark
for key, value in colors
if length(value) > 1
{key}: oklch_fallback(value[1])
rainbow-light = linear-gradient(to right, oklch(0.80 0.10 0), oklch(0.80 0.10 30), oklch(0.80 0.10 60), oklch(0.80 0.10 90), oklch(0.80 0.10 120), oklch(0.80 0.10 150), oklch(0.80 0.10 180), oklch(0.80 0.10 210), oklch(0.80 0.10 240), oklch(0.80 0.10 270), oklch(0.80 0.10 300), oklch(0.80 0.10 330), oklch(0.80 0.10 360))
rainbow-dark = linear-gradient(to right, oklch(0.70 0.10 0), oklch(0.70 0.10 30), oklch(0.70 0.10 60), oklch(0.70 0.10 90), oklch(0.70 0.10 120), oklch(0.70 0.10 150), oklch(0.70 0.10 180), oklch(0.70 0.10 210), oklch(0.70 0.10 240), oklch(0.70 0.10 270), oklch(0.70 0.10 300), oklch(0.70 0.10 330), oklch(0.70 0.10 360))
:root
--radius-large 1rem
--banner-height-home 60vh
--banner-height 40vh
--content-delay 150ms
color_set({
--primary: oklch(0.70 0.14 var(--hue)) oklch(0.75 0.14 var(--hue))
--page-bg: oklch(0.95 0.01 var(--hue)) oklch(0.16 0.014 var(--hue))
--card-bg: white oklch(0.23 0.015 var(--hue))
--btn-content: oklch(0.55 0.12 var(--hue)) oklch(0.75 0.1 var(--hue))
--btn-regular-bg: oklch(0.95 0.025 var(--hue)) oklch(0.33 0.035 var(--hue))
--btn-regular-bg-hover: oklch(0.9 0.05 var(--hue)) oklch(0.38 0.04 var(--hue))
--btn-regular-bg-active: oklch(0.85 0.08 var(--hue)) oklch(0.43 0.045 var(--hue))
--btn-plain-bg-hover: oklch(0.95 0.025 var(--hue)) oklch(0.30 0.035 var(--hue))
--btn-plain-bg-active: oklch(0.98 0.01 var(--hue)) oklch(0.27 0.025 var(--hue))
--btn-card-bg-hover: oklch(0.98 0.005 var(--hue)) oklch(0.3 0.03 var(--hue))
--btn-card-bg-active: oklch(0.9 0.03 var(--hue)) oklch(0.35 0.035 var(--hue))
--enter-btn-bg: var(--btn-regular-bg)
--enter-btn-bg-hover: var(--btn-regular-bg-hover)
--enter-btn-bg-active: var(--btn-regular-bg-active)
--deep-text: oklch(0.25 0.02 var(--hue))
--title-active: oklch(0.6 0.1 var(--hue))
--line-divider: black(0.08) white(0.08)
--line-color: black(0.1) white(0.1)
--meta-divider: black(0.2) white(0.2)
--inline-code-bg: var(--btn-regular-bg)
--inline-code-color: var(--btn-content)
--selection-bg: oklch(0.90 0.05 var(--hue)) oklch(0.40 0.08 var(--hue))
--codeblock-selection: oklch(0.40 0.08 var(--hue))
--codeblock-bg: oklch(0.2 0.015 var(--hue)) oklch(0.17 0.015 var(--hue))
--license-block-bg: black(0.03) var(--codeblock-bg)
--link-underline: oklch(0.93 0.04 var(--hue)) oklch(0.40 0.08 var(--hue))
--link-hover: oklch(0.95 0.025 var(--hue)) oklch(0.40 0.08 var(--hue))
--link-active: oklch(0.90 0.05 var(--hue)) oklch(0.35 0.07 var(--hue))
--float-panel-bg: white oklch(0.19 0.015 var(--hue))
--scrollbar-bg-light: black(0.4)
--scrollbar-bg-hover-light: black(0.5)
--scrollbar-bg-active-light: black(0.6)
--scrollbar-bg-dark: white(0.4)
--scrollbar-bg-hover-dark: white(0.5)
--scrollbar-bg-active-dark: white(0.6)
--scrollbar-bg: var(--scrollbar-bg-light) var(--scrollbar-bg-dark)
--scrollbar-bg-hover: var(--scrollbar-bg-hover-light) var(--scrollbar-bg-hover-dark)
--scrollbar-bg-active: var(--scrollbar-bg-active-light) var(--scrollbar-bg-active-dark)
--color-selection-bar: rainbow-light rainbow-dark
--display-light-icon: 1 0
--display-dark-icon: 0 1
--admonitions-color-tip: oklch(0.7 0.14 180) oklch(0.75 0.14 180)
--admonitions-color-note: oklch(0.7 0.14 250) oklch(0.75 0.14 250)
--admonitions-color-important: oklch(0.7 0.14 310) oklch(0.75 0.14 310)
--admonitions-color-warning: oklch(0.7 0.14 60) oklch(0.75 0.14 60)
--admonitions-color-caution: oklch(0.6 0.2 25) oklch(0.65 0.2 25)
})
/* some global styles */
::selection
background-color: var(--selection-bg)
.scrollbar-base.os-scrollbar
transition: width 0.15s ease-in-out, height 0.15s ease-in-out, opacity 0.15s, visibility 0.15s, top 0.15s, right 0.15s, bottom 0.15s, left 0.15s;
pointer-events: unset;
&.os-scrollbar-horizontal
padding-top: 4px;
padding-bottom: 4px;
height: 16px;
.os-scrollbar-track .os-scrollbar-handle
height: 4px;
border-radius: 4px;
&:hover
.os-scrollbar-track .os-scrollbar-handle
height: 8px;
&.px-2
padding-left: 8px;
padding-right: 8px;
&.os-scrollbar-vertical
padding-left: 4px;
padding-right: 4px;
width: 16px;
.os-scrollbar-track .os-scrollbar-handle
width: 4px;
border-radius: 4px;
&:hover
.os-scrollbar-track .os-scrollbar-handle
width: 8px;
&.py-1
padding-top: 4px;
padding-bottom: 4px;
.scrollbar-auto
&.os-scrollbar
--os-handle-bg: var(--scrollbar-bg);
--os-handle-bg-hover: var(--scrollbar-bg-hover);
--os-handle-bg-active: var(--scrollbar-bg-active);
.scrollbar-dark
&.os-scrollbar
--os-handle-bg: var(--scrollbar-bg-dark);
--os-handle-bg-hover: var(--scrollbar-bg-hover-dark);
--os-handle-bg-active: var(--scrollbar-bg-active-dark);
.scrollbar-light
&.os-scrollbar
--os-handle-bg: var(--scrollbar-bg-light);
--os-handle-bg-hover: var(--scrollbar-bg-hover-light);
--os-handle-bg-active: var(--scrollbar-bg-active-light);
</style>
<style is:global lang="scss">
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.card-base {
@apply rounded-[var(--radius-large)] overflow-hidden bg-[var(--card-bg)] transition;
}
h1, h2, h3, h4, h5, h6, p, a, span, li, ul, ol, blockquote, code, pre, table, th, td, strong {
@apply transition;
}
.card-shadow {
@apply drop-shadow-[0_2px_4px_rgba(0,0,0,0.005)]
}
.expand-animation {
@apply relative before:ease-out before:transition active:bg-none hover:before:bg-[var(--btn-plain-bg-hover)] active:before:bg-[var(--btn-plain-bg-active)] z-0
before:absolute before:rounded-[inherit] before:inset-0 before:scale-[0.85] hover:before:scale-100 before:-z-10
}
.link {
@apply transition rounded-md p-1 -m-1 expand-animation;
}
.link-lg {
@apply transition rounded-md p-1.5 -m-1.5 expand-animation;
}
.float-panel {
@apply top-[5.25rem] rounded-[var(--radius-large)] overflow-hidden bg-[var(--float-panel-bg)] transition shadow-xl dark:shadow-none
}
.float-panel-closed {
@apply -translate-y-1 opacity-0 pointer-events-none
}
.search-panel mark {
@apply bg-transparent text-[var(--primary)]
}
.btn-card {
@apply transition flex items-center justify-center bg-[var(--card-bg)] hover:bg-[var(--btn-card-bg-hover)]
active:bg-[var(--btn-card-bg-active)]
}
.btn-card.disabled {
@apply pointer-events-none text-black/10 dark:text-white/10
}
.btn-plain {
@apply transition relative flex items-center justify-center bg-none
text-black/75 hover:text-[var(--primary)] dark:text-white/75 dark:hover:text-[var(--primary)];
&:not(.scale-animation) {
@apply hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]
}
&.scale-animation {
@apply expand-animation;
&.current-theme-btn {
@apply before:scale-100 before:opacity-100 before:bg-[var(--btn-plain-bg-hover)] text-[var(--primary)]
}
}
}
.btn-regular {
@apply transition flex items-center justify-center bg-[var(--btn-regular-bg)] hover:bg-[var(--btn-regular-bg-hover)] active:bg-[var(--btn-regular-bg-active)]
text-[var(--btn-content)] dark:text-white/75
}
.link-underline {
@apply transition underline decoration-2 decoration-dashed decoration-[var(--link-underline)]
hover:decoration-[var(--link-hover)] active:decoration-[var(--link-active)] underline-offset-[0.25rem]
}
.text-90 {
@apply text-black/90 dark:text-white/90
}
.text-75 {
@apply text-black/75 dark:text-white/75
}
.text-50 {
@apply text-black/50 dark:text-white/50
}
.text-30 {
@apply text-black/30 dark:text-white/30
}
.text-25 {
@apply text-black/25 dark:text-white/25
}
html.is-changing .transition-fade {
@apply transition-all duration-200
}
html.is-animating .transition-fade {
@apply opacity-0 translate-y-4
}
}
@keyframes fade-in-up {
0% {
transform: translateY(2rem);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.onload-animation {
opacity: 0;
animation: 300ms fade-in-up;
animation-fill-mode: forwards;
}
#top-row {
animation-delay: 0ms
}
#sidebar {
animation-delay: 100ms
}
#content-wrapper {
animation-delay: var(--content-delay);
}
#footer {
animation-delay: 400ms;
}
</style>

View file

@ -0,0 +1,120 @@
<script lang="ts">
import type { LIGHT_DARK_MODE } from '@/types/config.ts'
import {
AUTO_MODE,
DARK_MODE,
LIGHT_MODE,
} from '@constants/constants.ts'
import I18nKey from '@i18n/i18nKey'
import { i18n } from '@i18n/translation'
import Icon from '@iconify/svelte'
import {
applyThemeToDocument,
getStoredTheme,
setTheme,
} from '@utils/setting-utils.ts'
import { onMount } from 'svelte'
const seq: LIGHT_DARK_MODE[] = [
LIGHT_MODE,
DARK_MODE,
AUTO_MODE,
]
let mode: LIGHT_DARK_MODE = AUTO_MODE
onMount(() => {
mode = getStoredTheme()
const darkModePreference = window.matchMedia(
'(prefers-color-scheme: dark)',
)
const changeThemeWhenSchemeChanged: Parameters<
typeof darkModePreference.addEventListener<'change'>
>[1] = e => {
applyThemeToDocument(mode)
}
darkModePreference.addEventListener(
'change',
changeThemeWhenSchemeChanged,
)
return () => {
darkModePreference.removeEventListener(
'change',
changeThemeWhenSchemeChanged,
)
}
})
function switchScheme(newMode: LIGHT_DARK_MODE) {
mode = newMode
setTheme(newMode)
}
function toggleScheme() {
let i = 0
for (; i < seq.length; i++) {
if (seq[i] === mode) {
break
}
}
switchScheme(seq[(i + 1) % seq.length])
}
function showPanel() {
const panel = document.querySelector('#light-dark-panel')
panel.classList.remove('float-panel-closed')
}
function hidePanel() {
const panel = document.querySelector('#light-dark-panel')
panel.classList.add('float-panel-closed')
}
</script>
<!-- z-50 make the panel higher than other float panels -->
<div class="relative z-50" role="menu" tabindex="-1" on:mouseleave={hidePanel}>
<button aria-label="Light/Dark Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch" on:click={toggleScheme} on:mouseenter={showPanel}>
<div class="absolute" class:opacity-0={mode !== LIGHT_MODE}>
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"></Icon>
</div>
<div class="absolute" class:opacity-0={mode !== DARK_MODE}>
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"></Icon>
</div>
<div class="absolute" class:opacity-0={mode !== AUTO_MODE}>
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"></Icon>
</div>
</button>
<div id="light-dark-panel" class="hidden lg:block absolute transition float-panel-closed top-11 -right-2 pt-5" >
<div class="card-base float-panel p-2">
<button class="flex transition whitespace-nowrap items-center justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
class:current-theme-btn={mode === LIGHT_MODE}
on:click={() => switchScheme(LIGHT_MODE)}
>
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
{i18n(I18nKey.lightMode)}
</button>
<button class="flex transition whitespace-nowrap items-center justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
class:current-theme-btn={mode === DARK_MODE}
on:click={() => switchScheme(DARK_MODE)}
>
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
{i18n(I18nKey.darkMode)}
</button>
<button class="flex transition whitespace-nowrap items-center justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95"
class:current-theme-btn={mode === AUTO_MODE}
on:click={() => switchScheme(AUTO_MODE)}
>
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"></Icon>
{i18n(I18nKey.systemMode)}
</button>
</div>
</div>
</div>
<style lang="css">
.current-setting {
background: var(--btn-plain-bg-hover);
color: var(--primary);
}
</style>

120
src/components/Navbar.astro Normal file
View file

@ -0,0 +1,120 @@
---
import { Icon } from 'astro-icon/components';
import DisplaySettings from "./widget/DisplaySettings.svelte";
import {LinkPreset, NavBarLink} from "../types/config";
import {navBarConfig, siteConfig} from "../config";
import NavMenuPanel from "./widget/NavMenuPanel.astro";
import Search from "./Search.svelte";
import {LinkPresets} from "../constants/link-presets";
import LightDarkSwitch from "./LightDarkSwitch.svelte";
import {url} from "../utils/url-utils";
const className = Astro.props.class;
let links: NavBarLink[] = navBarConfig.links.map((item: NavBarLink | LinkPreset): NavBarLink => {
if (typeof item === "number") {
return LinkPresets[item]
}
return item;
});
---
<div class:list={[
className,
"card-base sticky top-0 overflow-visible max-w-[var(--page-width)] h-[4.5rem] rounded-t-none mx-auto flex items-center justify-between px-4"]}>
<a href={url('/')} class="btn-plain scale-animation rounded-lg h-[3.25rem] px-5 font-bold active:scale-95">
<div class="flex flex-row text-[var(--primary)] items-center text-md">
<Icon name="material-symbols:home-outline-rounded" size={"1.75rem"} class="mb-1 mr-2" />
{siteConfig.title}
</div>
</a>
<div class="hidden md:flex">
{links.map((l) => {
return <a aria-label={l.name} href={l.external ? l.url : url(l.url)} target={l.external ? "_blank" : null}
class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95"
>
<div class="flex items-center">
{l.name}
{l.external && <Icon size="14" name="fa6-solid:arrow-up-right-from-square" class="transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]"></Icon>}
</div>
</a>;
})}
</div>
<div class="flex">
<!--<SearchPanel client:load>-->
<Search client:load>
<Icon slot="search-icon" name="material-symbols:search" size={"1.25rem"} class="absolute pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<!--<Icon slot="arrow-icon" name="material-symbols:chevron-right-rounded" size={"1.25rem"} class="transition my-auto text-[var(&#45;&#45;primary)]"></Icon>-->
<Icon slot="arrow-icon" name="fa6-solid:chevron-right" size={"0.75rem"} class="transition translate-x-0.5 my-auto text-[var(--primary)]"></Icon>
<Icon slot="search-switch" name="material-symbols:search" size={"1.25rem"}></Icon>
</Search>
{!siteConfig.themeColor.fixed && (
<button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch">
<Icon name="material-symbols:palette-outline" size={"1.25rem"}></Icon>
</button>
)}
<LightDarkSwitch client:load></LightDarkSwitch>
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:hidden" id="nav-menu-switch">
<Icon name="material-symbols:menu-rounded" size={"1.25rem"}></Icon>
</button>
</div>
<NavMenuPanel links={links}></NavMenuPanel>
<DisplaySettings client:only="svelte">
<Icon slot="restore-icon" name="fa6-solid:arrow-rotate-left" size={"0.875rem"} class=""></Icon>
</DisplaySettings>
</div>
<style lang="stylus">
</style>
<script>
function switchTheme() {
if (localStorage.theme === 'dark') {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
} else {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
}
}
function loadButtonScript() {
let switchBtn = document.getElementById("scheme-switch");
switchBtn.addEventListener("click", function () {
switchTheme()
});
let settingBtn = document.getElementById("display-settings-switch");
if (settingBtn) {
settingBtn.addEventListener("click", function () {
let settingPanel = document.getElementById("display-setting");
settingPanel.classList.toggle("float-panel-closed");
});
}
let menuBtn = document.getElementById("nav-menu-switch");
menuBtn.addEventListener("click", function () {
let menuPanel = document.getElementById("nav-menu-panel");
menuPanel.classList.toggle("float-panel-closed");
});
}
loadButtonScript();
document.addEventListener('astro:after-swap', () => {
loadButtonScript();
}, { once: false });
</script>
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
async function loadPagefind() {
const pagefind = await import(scriptUrl)
await pagefind.options({
'excerptLength': 20
})
pagefind.init()
window.pagefind = pagefind
pagefind.search('') // speed up the first search
}
loadPagefind()
</script>}

View file

@ -0,0 +1,95 @@
---
import path from "path";
import PostMetadata from "./PostMeta.astro";
import ImageWrapper from "./misc/ImageWrapper.astro";
import { Icon } from 'astro-icon/components';
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {getDir} from "../utils/url-utils";
interface Props {
class: string;
entry: any;
title: string;
url: string;
published: Date;
tags: string[];
category: string;
image: string;
description: string;
words: number;
draft: boolean;
style: string;
}
const { entry, title, url, published, tags, category, image, description, words, style } = Astro.props;
const className = Astro.props.class;
const hasCover = image !== undefined && image !== null && image !== '';
const coverWidth = "28%";
const { remarkPluginFrontmatter } = await entry.render();
---
<div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}>
<div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}>
<a href={url}
class="transition group w-full block font-bold mb-3 text-3xl text-90
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
active:text-[var(--title-active)] dark:active:text-[var(--title-active)]
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-[35px] before:left-[18px] before:hidden md:before:block
">
{title}
<Icon class="inline text-[var(--primary)] md:hidden translate-y-0.5 absolute" name="material-symbols:chevron-right-rounded" size="2rem" ></Icon>
<Icon class="text-[var(--primary)] transition hidden md:inline absolute translate-y-0.5 opacity-0 group-hover:opacity-100 -translate-x-1 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded" size="2rem" ></Icon>
</a>
<!-- metadata -->
<PostMetadata published={published} tags={tags} category={category} hideTagsForMobile={true} class="mb-4"></PostMetadata>
<!-- description -->
<div class="transition text-75 mb-3.5 pr-4">
{ description }
</div>
<!-- word count and read time -->
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
<div>{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
<div>|</div>
<div>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div>
</div>
</div>
{hasCover && <a href={url} aria-label={title}
class:list={["group",
"max-h-[20vh] md:max-h-none mx-4 mt-4 -mb-2 md:mb-0 md:mx-0 md:mt-0",
"md:w-[var(--coverWidth)] relative md:absolute md:top-3 md:bottom-3 md:right-3 rounded-xl overflow-hidden active:scale-95"
]} >
<div class="absolute pointer-events-none z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div>
<div class="absolute pointer-events-none z-20 w-full h-full flex items-center justify-center ">
<Icon name="material-symbols:chevron-right-rounded"
class="transition opacity-0 group-hover:opacity-100 scale-50 group-hover:scale-100 text-white text-5xl">
</Icon>
</div>
<ImageWrapper src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post"
class="w-full h-full">
</ImageWrapper>
</a>}
{!hasCover &&
<a href={url} aria-label={title} class="hidden md:flex btn-regular w-[3.25rem]
absolute right-3 top-3 bottom-3 rounded-xl bg-[var(--enter-btn-bg)]
hover:bg-[var(--enter-btn-bg-hover)] active:bg-[var(--enter-btn-bg-active)] active:scale-95
">
<Icon name="material-symbols:chevron-right-rounded"
class="transition text-[var(--primary)] text-4xl mx-auto">
</Icon>
</a>
}
</div>
<div class="transition border-t-[1px] border-dashed mx-6 border-black/10 dark:border-white/[0.15] last:border-t-0 md:hidden"></div>
<style lang="stylus" define:vars={{coverWidth}}>
</style>

View file

@ -0,0 +1,77 @@
---
import {formatDateToYYYYMMDD} from "../utils/date-utils";
import { Icon } from 'astro-icon/components';
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {url} from "../utils/url-utils";
interface Props {
class: string;
published: Date;
tags: string[];
category: string;
hideTagsForMobile: boolean;
}
const {published, tags, category, hideTagsForMobile} = Astro.props;
const className = Astro.props.class;
---
<div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-2", className]}>
<!-- publish date -->
<div class="flex items-center">
<div class="meta-icon"
>
<Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
</div>
<span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(published)}</span>
</div>
<!-- categories -->
<div class="flex items-center">
<div class="meta-icon"
>
<Icon name="material-symbols:menu-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row flex-nowrap items-center">
<a href={url(`/archive/category/${category || 'uncategorized'}/`)} aria-label=`View all posts in the ${category} category`
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{category || i18n(I18nKey.uncategorized)}
</a>
</div>
</div>
<!-- tags -->
<div class:list={["items-center", {"flex": !hideTagsForMobile, "hidden md:flex": hideTagsForMobile}]}>
<div class="meta-icon"
>
<Icon name="material-symbols:tag-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row flex-nowrap items-center">
{(tags && tags.length > 0) && tags.map((tag, i) => (
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
<a href={url(`/archive/tag/${tag}/`)} aria-label=`View all posts with the ${tag} tag`
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{tag}
</a>
))}
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
</div>
</div>
</div>
<style>
@tailwind components;
@layer components {
.meta-icon {
@apply w-8 h-8 transition rounded-md flex items-center justify-center bg-[var(--btn-regular-bg)]
text-[var(--btn-content)] mr-2
}
.with-divider {
@apply before:content-['/'] before:ml-1.5 before:mr-1.5 before:text-[var(--meta-divider)] before:text-sm
before:font-medium before:first-of-type:hidden before:transition
}
}
</style>

View file

@ -0,0 +1,28 @@
---
import {getPostUrlBySlug} from "@utils/url-utils";
import PostCard from "./PostCard.astro";
const {page} = Astro.props;
let delay = 0
const interval = 50
---
<div class="transition flex flex-col rounded-[var(--radius-large)] bg-[var(--card-bg)] py-1 md:py-0 md:bg-transparent md:gap-4 mb-4">
{page.data.map((entry: { data: { draft: boolean; title: string; tags: string[]; category: string; published: Date; image: string; description: string; }; slug: string; }) => {
return (
<PostCard
entry={entry}
title={entry.data.title}
tags={entry.data.tags}
category={entry.data.category}
published={entry.data.published}
url={getPostUrlBySlug(entry.slug)}
image={entry.data.image}
description={entry.data.description}
draft={entry.data.draft}
class:list="onload-animation"
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
></PostCard>
);
})}
</div>

View file

@ -0,0 +1,116 @@
<script lang="ts">
import { onMount } from 'svelte'
import {url} from "@utils/url-utils.ts"
import { i18n } from '@i18n/translation';
import I18nKey from '@i18n/i18nKey';
let keywordDesktop = ''
let keywordMobile = ''
let result = []
const fakeResult = [{
url: url('/'),
meta: {
title: 'This Is a Fake Search Result'
},
excerpt: 'Because the search cannot work in the <mark>dev</mark> environment.'
}, {
url: url('/'),
meta: {
title: 'If You Want to Test the Search'
},
excerpt: 'Try running <mark>npm build && npm preview</mark> instead.'
}]
let search = (keyword: string, isDesktop: boolean) => {}
onMount(() => {
search = async (keyword: string, isDesktop: boolean) => {
let panel = document.getElementById('search-panel')
if (!panel)
return
if (!keyword && isDesktop) {
panel.classList.add("float-panel-closed")
return
}
let arr = [];
if (import.meta.env.PROD) {
const ret = await pagefind.search(keyword)
for (const item of ret.results) {
arr.push(await item.data())
}
} else {
// Mock data for non-production environment
// arr = JSON.parse('[{"url":"/","content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","word_count":187,"filters":{},"meta":{"title":"This Is a Fake Search Result"},"anchors":[{"element":"h2","id":"front-matter-of-posts","text":"Front-matter of Posts","location":34},{"element":"h2","id":"where-to-place-the-post-files","text":"Where to Place the Post Files","location":151}],"weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"raw_content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","raw_url":"/posts/guide/","excerpt":"Because the search cannot work in the <mark>dev</mark> environment.","sub_results":[{"title":"Simple Guides for Fuwari - Fuwari","url":"/posts/guide/","weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"excerpt":"Simple Guides for <mark>Fuwari.</mark> Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers"}]},{"url":"/","content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","word_count":25,"filters":{},"meta":{"title":"If You Want to Test the Search"},"anchors":[{"element":"h1","id":"about","text":"About","location":0},{"element":"h3","id":"sources-of-images-used-in-this-site","text":"Sources of images used in this site","location":8}],"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"raw_content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","raw_url":"/about/","excerpt":"Try running <mark>npm build && npm preview</mark> instead.","sub_results":[{"title":"About","url":"/about/#about","anchor":{"element":"h1","id":"about","text":"About","location":0},"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"excerpt":"About. This is the demo site for <mark>Fuwari.</mark>"}]}]')
arr = fakeResult
}
if (!arr.length && isDesktop) {
panel.classList.add("float-panel-closed")
return
}
if (isDesktop) {
panel.classList.remove("float-panel-closed")
}
result = arr
}
})
const togglePanel = () => {
let panel = document.getElementById('search-panel')
panel?.classList.toggle("float-panel-closed")
}
$: search(keywordDesktop, true)
$: search(keywordMobile, false)
</script>
<!-- search bar for desktop view -->
<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<slot name="search-icon"></slot>
<input placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)}
class="transition-all pl-10 text-sm bg-transparent outline-0
h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"
>
</div>
<!-- toggle btn for phone/tablet view -->
<button on:click={togglePanel} aria-label="Search Panel" id="search-switch"
class="btn-plain scale-animation lg:hidden rounded-lg w-11 h-11 active:scale-90">
<slot name="search-switch"></slot>
</button>
<!-- search panel -->
<div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem]
top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">
<!-- search bar inside panel for phone/tablet -->
<div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<slot name="search-icon"></slot>
<input placeholder="Search" bind:value={keywordMobile}
class="pl-10 absolute inset-0 text-sm bg-transparent outline-0
focus:w-60 text-black/50 dark:text-white/50"
>
</div>
<!-- search results -->
{#each result as item}
<a href={item.url}
class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block
rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]">
<div class="transition text-90 inline-flex font-bold group-hover:text-[var(--primary)]">
{item.meta.title}<slot name="arrow-icon"></slot>
</div>
<div class="transition text-sm text-50">
{@html item.excerpt}
</div>
</a>
{/each}
</div>

View file

@ -0,0 +1,57 @@
---
import { Icon } from 'astro-icon/components';
---
<!-- There can't be a filter on parent element, or it will break `fixed` -->
<div class="back-to-top-wrapper hidden lg:block">
<div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()">
<button aria-label="Back to Top" class="btn-card h-[3.75rem] w-[3.75rem]">
<Icon name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon>
</button>
</div>
</div>
<style lang="stylus">
.back-to-top-wrapper
width: 3.75rem
height: 3.75rem
position: absolute
right: 0
top: 0
.back-to-top-btn
color: var(--primary)
font-size: 2.25rem
font-weight: bold
border: none
position: fixed
bottom: 15rem
opacity: 1
cursor: pointer
transform: translateX(5rem)
i
font-size: 1.75rem
&.hide
transform: translateX(5rem) scale(0.9)
opacity: 0
pointer-events: none
&:active
transform: translateX(5rem) scale(0.9)
</style>
<script is:raw>
function backToTop() {
window.scroll({ top: 0, behavior: 'smooth' });
}
function scrollFunction() {
let btn = document.getElementById('back-to-top-btn');
if (document.body.scrollTop > 600 || document.documentElement.scrollTop > 600) {
btn.classList.remove('hide')
} else {
btn.classList.add('hide')
}
}
window.onscroll = scrollFunction
</script>

View file

@ -0,0 +1,43 @@
---
interface Props {
badge?: string
url?: string
label?: string
}
const { badge, url, name } = Astro.props
---
<a href={url} aria-label={name}>
<button
class:list={`
w-full
h-10
rounded-lg
bg-none
hover:bg-[var(--btn-plain-bg-hover)]
active:bg-[var(--btn-plain-bg-active)]
transition-all
pl-2
hover:pl-3
text-neutral-700
hover:text-[var(--primary)]
dark:text-neutral-300
dark:hover:text-[var(--primary)]
`
}
>
<div class="flex items-center justify-between relative mr-2">
<div class="overflow-hidden text-left whitespace-nowrap overflow-ellipsis ">
<slot></slot>
</div>
{ badge !== undefined && badge !== null && badge !== '' &&
<div class="transition h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
text-[var(--btn-content)] dark:text-[var(--deep-text)]
bg-[oklch(0.95_0.025_var(--hue))] dark:bg-[var(--primary)]
flex items-center justify-center">
{ badge }
</div>
}
</div>
</button>
</a>

View file

@ -0,0 +1,13 @@
---
interface Props {
size?: string;
dot?: boolean;
href?: string;
label?: string;
}
const { size, dot, href, label }: Props = Astro.props;
---
<a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg">
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}
<slot></slot>
</a>

View file

@ -0,0 +1,91 @@
---
import type { Page } from "astro";
import { Icon } from 'astro-icon/components';
import {url} from "../../utils/url-utils";
interface Props {
page: Page;
class?: string;
style?: string;
}
const {page, style} = Astro.props;
const HIDDEN = -1;
const className = Astro.props.class;
const ADJ_DIST = 2;
const VISIBLE = ADJ_DIST * 2 + 1;
// for test
let count = 1;
let l = page.currentPage, r = page.currentPage;
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
count += 2;
l--;
r++;
}
while (0 < l - 1 && count < VISIBLE) {
count++;
l--;
}
while (r + 1 <= page.lastPage && count < VISIBLE) {
count++;
r++;
}
let pages: number[] = [];
if (l > 1)
pages.push(1);
if (l == 3)
pages.push(2);
if (l > 3)
pages.push(HIDDEN);
for (let i = l; i <= r; i++)
pages.push(i);
if (r < page.lastPage - 2)
pages.push(HIDDEN);
if (r == page.lastPage - 2)
pages.push(page.lastPage - 1);
if (r < page.lastPage)
pages.push(page.lastPage);
const getPageUrl = (p: number) => {
if (p == 1)
return '/';
return `/${p}/`;
}
---
<div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}>
<a href={url(page.url.prev)} aria-label={page.url.prev ? "Previous Page" : null}
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
{"disabled": page.url.prev == undefined}
]}
>
<Icon name="material-symbols:chevron-left-rounded" size="1.75rem"></Icon>
</a>
<div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold">
{pages.map((p) => {
if (p == HIDDEN)
return <Icon name="material-symbols:more-horiz" class="mx-1"/>;
if (p == page.currentPage)
return <div class="h-11 w-11 rounded-lg bg-[var(--primary)] flex items-center justify-center
font-bold text-white dark:text-black/70"
>
{p}
</div>
return <a href={url(getPageUrl(p))} aria-label=`Page ${p}`
class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
>{p}</a>
})}
</div>
<a href={url(page.url.next)} aria-label={page.url.next ? "Next Page" : null}
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
{"disabled": page.url.next == undefined}
]}
>
<Icon name="material-symbols:chevron-right-rounded" size="1.75rem"></Icon>
</a>
</div>

View file

@ -0,0 +1,37 @@
---
import path from "path";
interface Props {
id?: string
src: string;
class?: string;
alt?: string
position?: string;
basePath?: string
}
import { Image } from 'astro:assets';
import { url } from "../../utils/url-utils";
const {id, src, alt, position = 'center', basePath = '/'} = Astro.props;
const className = Astro.props.class;
const isLocal = !(src.startsWith('/') || src.startsWith('http') || src.startsWith('https') || src.startsWith('data:'));
const isPublic = src.startsWith('/');
// TODO temporary workaround for images dynamic import
// https://github.com/withastro/astro/issues/3373
let img;
if (isLocal) {
const files = import.meta.glob<ImageMetadata>("../../**", { import: 'default' });
let normalizedPath = path.normalize(path.join("../../", basePath, src)).replace(/\\/g, "/");
img = await (files[normalizedPath])();
}
const imageClass = 'w-full h-full object-cover';
const imageStyle = `object-position: ${position}`
---
<div class:list={[className, 'overflow-hidden relative']}>
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
{isLocal && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle}/>}
{!isLocal && <img src={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>}
</div>

View file

@ -0,0 +1,44 @@
---
import {formatDateToYYYYMMDD} from "../../utils/date-utils";
import { Icon } from 'astro-icon/components';
import {licenseConfig, profileConfig} from "../../config";
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
interface Props {
title: string;
slug: string;
pubDate: Date;
class: string;
}
const { title, slug, pubDate } = Astro.props;
const className = Astro.props.class;
const profileConf = profileConfig;
const licenseConf = licenseConfig;
const postUrl = decodeURIComponent(Astro.url.toString());
---
<div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`>
<div class="transition font-bold text-black/75 dark:text-white/75">
{title}
</div>
<a href={postUrl} class="link text-[var(--primary)]">
{postUrl}
</a>
<div class="flex gap-6 mt-2">
<div>
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.author)}</div>
<div class="transition text-black/75 dark:text-white/75 whitespace-nowrap">{profileConf.name}</div>
</div>
<div>
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.publishedAt)}</div>
<div class="transition text-black/75 dark:text-white/75 whitespace-nowrap">{formatDateToYYYYMMDD(pubDate)}</div>
</div>
<div>
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.license)}</div>
<a href={licenseConf.url} target="_blank" class="link text-[var(--primary)] whitespace-nowrap">{licenseConf.name}</a>
</div>
</div>
<Icon name="fa6-brands:creative-commons" class="transition absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5" size="240"></Icon>
</div>

View file

@ -0,0 +1,491 @@
---
import '@fontsource-variable/jetbrains-mono';
import '@fontsource-variable/jetbrains-mono/wght-italic.css';
interface Props {
class: string;
}
const className = Astro.props.class;
---
<div data-pagefind-body class=`prose dark:prose-invert prose-base max-w-none custom-md ${className}`>
<!--<div class="prose dark:prose-invert max-w-none custom-md">-->
<!--<div class="max-w-none custom-md">-->
<slot/>
</div>
<script>
const observer = new MutationObserver(addPreCopyButton);
observer.observe(document.body, { childList: true, subtree: true });
document.addEventListener("DOMContentLoaded", addPreCopyButton);
function addPreCopyButton() {
observer.disconnect();
let codeBlocks = Array.from(document.querySelectorAll("pre"));
for (let codeBlock of codeBlocks) {
if (codeBlock.parentElement?.nodeName === "DIV" && codeBlock.parentElement?.classList.contains("code-block")) continue
let wrapper = document.createElement("div");
wrapper.className = "relative code-block";
let copyButton = document.createElement("button");
copyButton.className = "copy-btn btn-regular-dark absolute active:scale-90 h-8 w-8 top-2 right-2 opacity-75 text-sm p-1.5 rounded-lg transition-all ease-in-out";
codeBlock.setAttribute("tabindex", "0");
if (codeBlock.parentNode) {
codeBlock.parentNode.insertBefore(wrapper, codeBlock);
}
let copyIcon = `<svg class="copy-btn-icon copy-icon" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M368.37-237.37q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-474.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26h378.26q34.48 0 58.74 24.26 24.26 24.26 24.26 58.74v474.26q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H368.37Zm0-83h378.26v-474.26H368.37v474.26Zm-155 238q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-515.76q0-17.45 11.96-29.48 11.97-12.02 29.33-12.02t29.54 12.02q12.17 12.03 12.17 29.48v515.76h419.76q17.45 0 29.48 11.96 12.02 11.97 12.02 29.33t-12.02 29.54q-12.03 12.17-29.48 12.17H213.37Zm155-238v-474.26 474.26Z"/></svg>`
let successIcon = `<svg class="copy-btn-icon success-icon" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="m389-377.13 294.7-294.7q12.58-12.67 29.52-12.67 16.93 0 29.61 12.67 12.67 12.68 12.67 29.53 0 16.86-12.28 29.14L419.07-288.41q-12.59 12.67-29.52 12.67-16.94 0-29.62-12.67L217.41-430.93q-12.67-12.68-12.79-29.45-.12-16.77 12.55-29.45 12.68-12.67 29.62-12.67 16.93 0 29.28 12.67L389-377.13Z"/></svg>`
copyButton.innerHTML = `<div>${copyIcon} ${successIcon}</div>
`
wrapper.appendChild(codeBlock);
wrapper.appendChild(copyButton);
let timeout;
copyButton.addEventListener("click", async () => {
if (timeout) {
clearTimeout(timeout);
}
let text = codeBlock?.querySelector("code")?.innerText;
await navigator.clipboard.writeText(text);
copyButton.classList.add("success");
timeout = setTimeout(() => {
copyButton.classList.remove("success");
}, 1000);
});
}
observer.observe(document.body, { childList: true, subtree: true });
}
</script>
<!-- Styles for copy-code-button -->
<style lang="css" is:global>
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-regular-dark {
@apply flex items-center justify-center
bg-[oklch(0.45_0.01_var(--hue))] hover:bg-[oklch(0.50_0.01_var(--hue))] active:bg-[oklch(0.55_0.01_var(--hue))]
dark:bg-[oklch(0.30_0.02_var(--hue))] dark:hover:bg-[oklch(0.35_0.03_var(--hue))] dark:active:bg-[oklch(0.40_0.03_var(--hue))]
}
.btn-regular-dark.success {
@apply bg-[oklch(0.75_0.14_var(--hue))] dark:bg-[oklch(0.75_0.14_var(--hue))]
}
.copy-btn-icon {
@apply absolute top-1/2 left-1/2 transition -translate-x-1/2 -translate-y-1/2
}
.copy-btn .copy-icon {
@apply opacity-100 fill-white dark:fill-white/75
}
.copy-btn.success .copy-icon {
@apply opacity-0 fill-[var(--deep-text)]
}
.copy-btn .success-icon {
@apply opacity-0
}
.copy-btn.success .success-icon {
@apply opacity-100
}
}
</style>
<style lang="stylus" is:global>
.custom-md
h1, h2, h3, h4, h5, h6
.anchor
margin: -0.125rem !important
margin-left: 0.2ch !important
padding: 0.125rem !important
user-select: none !important
opacity: 0 !important
text-decoration: none !important
transition: opacity 0.15s ease-in-out, background 0.15s ease-in-out !important
.anchor-icon
margin-left: 0.45ch !important
margin-right: 0.45ch !important
&:hover
.anchor
opacity: 1 !important
a:not(.no-styling)
position: relative
background: none
margin: -0.25rem
padding: 0.25rem
border-radius: 0.375rem
font-weight: 500
color: var(--primary)
text-decoration-line: underline
text-decoration-color: var(--link-underline)
text-decoration-thickness: 0.125rem
text-decoration-style: dashed
text-underline-offset: 0.25rem
/*&:after*/
/* content: ''*/
/* position: absolute*/
/* left: 2px*/
/* right: 2px*/
/* bottom: 4px*/
/* height: 6px*/
/* border-radius: 3px*/
/* background: var(--link-hover)*/
/* transition: background 0.15s ease-in-out;*/
/* z-index: -1;*/
&:hover
background: var(--link-hover)
text-decoration-color: var(--link-hover)
&:active
background: var(--link-active)
text-decoration-color: var(--link-active)
code
font-family: 'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace
background: var(--inline-code-bg)
color: var(--inline-code-color)
padding: 0.125rem 0.25rem
border-radius: 0.25rem
overflow: hidden
counter-reset: line
&:before
content: none
&:after
content: none
span.line
&:before
content: counter(line)
counter-increment: line
direction: rtl
display: inline-block
margin-right: 1rem
width: 1rem
color: rgba(255, 255, 255, 0.25)
&:last-child:empty, &:last-child:has(> span:empty:only-child)
display: none
pre
background: var(--codeblock-bg) !important
border-radius: 0.75rem
padding-left: 1.25rem
padding-right: 1.25rem
code
color: unset
font-size: 0.875rem
padding: 0
background: none
::selection
background: var(--codeblock-selection)
span.br::selection
background: var(--codeblock-selection)
ul
li
&::marker
color: var(--primary)
ol
li
&::marker
color: var(--primary)
blockquote
font-style: normal
font-weight: inherit
border-left-color: rgba(0, 0, 0, 0)
position: relative;
&:before
content: ''
position: absolute
left: -0.25rem
display: block
transition: background 0.15s ease-in-out;
background: var(--btn-regular-bg)
height: 100%
width: 0.25rem
border-radius: 1rem
p
&:before
content: none
&:after
content: none
blockquote.admonition
.bdm-title
display: flex
align-items: center
margin-bottom: -.9rem
font-weight: bold
&:before
content: ' '
display: inline-block
font-size: inherit
overflow: visible
margin-right: .6rem
height: 1em
width: 1em
vertical-align: -.126em
mask-size: contain
mask-position: center
mask-repeat: no-repeat
transform: translateY(-0.0625rem)
&.bdm-tip
.bdm-title
color: var(--admonitions-color-tip)
&:before
background: var(--admonitions-color-tip)
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z'%3E%3C/path%3E%3C/svg%3E")
&:before
background: var(--admonitions-color-tip)
&.bdm-note
.bdm-title
color: var(--admonitions-color-note)
&:before
background: var(--admonitions-color-note)
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill='var(--admonitions-color-tip)' d='M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'%3E%3C/path%3E%3C/svg%3E")
&:before
background: var(--admonitions-color-note)
&.bdm-important
.bdm-title
color: var(--admonitions-color-important)
&:before
background: var(--admonitions-color-important)
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z'%3E%3C/path%3E%3C/svg%3E")
&:before
background: var(--admonitions-color-important)
&.bdm-warning
.bdm-title
color: var(--admonitions-color-warning)
&:before
background: var(--admonitions-color-warning)
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z'%3E%3C/path%3E%3C/svg%3E")
&:before
background: var(--admonitions-color-warning)
&.bdm-caution
.bdm-title
color: var(--admonitions-color-caution)
&:before
background: var(--admonitions-color-caution)
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'%3E%3C/path%3E%3C/svg%3E")
&:before
background: var(--admonitions-color-caution)
img
border-radius: 0.75rem
hr
border-color: var(--line-divider)
border-style: dashed
iframe
border-radius: 0.75rem
margin-left: auto
margin-right: auto
max-width: 100%
a.card-github
display: block
background: var(--license-block-bg)
position: relative
margin: 0.5rem 0
padding: 1.1rem 1.5rem 1.1rem 1.5rem
color: var(--tw-prose-body)
border-radius: var(--radius-large)
text-decoration-thickness: 0px
text-decoration-line: none
&:hover
background-color: var(--btn-regular-bg-hover)
.gc-titlebar
color: var(--btn-content)
.gc-stars, .gc-forks, .gc-license, .gc-description
color: var(--tw-prose-headings)
&:before
background-color: var(--tw-prose-headings)
&:active
scale: .98
background-color: var(--btn-regular-bg-active);
.gc-titlebar
display: flex
align-items: center
justify-content: space-between
margin-bottom: 0.5rem
color: var(--tw-prose-headings)
font-size: 1.25rem
font-weight: 500
.gc-titlebar-left
display: flex
flex-flow: row nowrap
gap: 0.5rem
.gc-repo
font-weight: bold
.gc-owner
font-weight: 300
position: relative
display: flex
flex-flow: row nowrap
gap: 0.5rem
align-items: center
.gc-avatar
display: block
overflow: hidden
width: 1.5rem
height: 1.5rem
margin-top: -0.1rem
background-color: var(--primary)
background-size: cover
border-radius: 50%
.gc-description
margin-bottom: 0.7rem
font-size: 1rem
font-weight: 300
line-height: 1.5rem
color: var(--tw-prose-body)
.gc-infobar
display: flex
flex-flow: row nowrap
gap: 1.5rem
color: var(--tw-prose-body)
width: fit-content
.gc-language
display: none
.gc-stars, .gc-forks, .gc-license, .github-logo
font-weight: 500
font-size: 0.875rem
opacity: 0.9;
&:before
content: ' '
display: inline-block
height: 1.3em
width: 1.3em
margin-right: .4rem
vertical-align: -.24em
font-size: inherit
background-color: var(--tw-prose-body)
overflow: visible
mask-size: contain
mask-position: center
mask-repeat: no-repeat
transition-property: background-color, background;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)
transition-duration: 0.15s
.gc-stars
&:before
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z'%3E%3C/path%3E%3C/svg%3E")
.gc-license
&:before
margin-right: .5rem
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z'%3E%3C/path%3E%3C/svg%3E")
.gc-forks
&:before
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z'%3E%3C/path%3E%3C/svg%3E")
.github-logo
font-size: 1.25rem
&:before
background-color: var(--tw-prose-headings)
margin-right: 0
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='31' height='32' viewBox='0 0 496 512'%3E%3Cpath fill='%23a1f7cb' d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9c1.6 2.3 4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2'/%3E%3C/svg%3E")
a.card-github.fetch-waiting
pointer-events: none
opacity: 0.7
transition: opacity 0.15s ease-in-out
.gc-description, .gc-infobar
background-color: var(--tw-prose-body)
color: transparent
opacity: 0.5;
border-radius: 0.5rem
animation: pulsate 2s infinite linear
user-select: none
&:before
background-color: transparent
.gc-avatar
display: none
.gc-repo
margin-left: -0.1rem
a.card-github.fetch-error
pointer-events: all
opacity: 1
@keyframes pulsate
0%
opacity: 0.15
50%
opacity: 0.25
100%
opacity: 0.15
.card-github, .gc-description, .gc-titlebar, .gc-stars, .gc-forks, .gc-license, .gc-avatar, github-logo
transition-property: all
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)
transition-duration: 0.15s
</style>
<style lang="css" is:global>
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.custom-md h1 {
@apply text-3xl
}
}
</style>

View file

@ -0,0 +1,38 @@
---
import WidgetLayout from "./WidgetLayout.astro";
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
import {Category, getCategoryList} from "../../utils/content-utils";
import {getCategoryUrl} from "../../utils/url-utils";
import ButtonLink from "../control/ButtonLink.astro";
const categories = await getCategoryList();
const COLLAPSED_HEIGHT = "7.5rem";
const COLLAPSE_THRESHOLD = 5;
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD;
interface Props {
class?: string;
style?: string;
}
const className = Astro.props.class
const style = Astro.props.style
---
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}
class={className} style={style}
>
{categories.map((c) =>
<ButtonLink
url={getCategoryUrl(c.name)}
badge={c.count}
label=`View all posts in the ${c.name} category`
>
{c.name}
</ButtonLink>
)}
</WidgetLayout>

View file

@ -0,0 +1,91 @@
<script lang="ts">
import {i18n} from '@i18n/translation';
import I18nKey from '@i18n/i18nKey';
import {getDefaultHue, getHue, setHue} from '@utils/setting-utils';
let hue = getHue()
const defaultHue = getDefaultHue()
function resetHue() {
hue = getDefaultHue()
}
$: if (hue || hue === 0) {
setHue(hue)
}
</script>
<div id="display-setting" class="float-panel float-panel-closed absolute transition-all w-80 right-4 px-4 py-4">
<div class="flex flex-row gap-2 mb-3 items-center justify-between">
<div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:-left-3 before:top-[0.33rem]"
>
{i18n(I18nKey.themeColor)}
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90"
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}>
<div class="text-[var(--btn-content)]">
<slot name="restore-icon"></slot>
</div>
</button>
</div>
<div class="flex gap-1">
<div id="hueValue" class="transition bg-[var(--btn-regular-bg)] w-10 h-7 rounded-md flex justify-center
font-bold text-sm items-center text-[var(--btn-content)]">
{hue}
</div>
</div>
</div>
<div class="w-full h-6 px-1 bg-[oklch(0.80_0.10_0)] dark:bg-[oklch(0.70_0.10_0)] rounded select-none">
<input aria-label={i18n(I18nKey.themeColor)} type="range" min="0" max="360" bind:value={hue}
class="slider" id="colorSlider" step="5" style="width: 100%;">
</div>
</div>
<style lang="stylus">
#display-setting
input[type="range"]
-webkit-appearance: none;
height: 1.5rem;
background-image: var(--color-selection-bar)
transition: background-image 0.15s ease-in-out
/* Input Thumb */
::-webkit-slider-thumb
-webkit-appearance: none;
height: 1rem;
width: 0.5rem;
border-radius: 0.125rem;
background: rgba(255, 255, 255, 0.7);
box-shadow: none;
&:hover
background: rgba(255, 255, 255, 0.8);
&:active
background: rgba(255, 255, 255, 0.6);
::-moz-range-thumb
-webkit-appearance: none;
height: 1rem;
width: 0.5rem;
border-radius: 0.125rem;
border-width: 0
background: rgba(255, 255, 255, 0.7);
box-shadow: none;
&:hover
background: rgba(255, 255, 255, 0.8);
&:active
background: rgba(255, 255, 255, 0.6);
&::-ms-thumb
-webkit-appearance: none;
height: 1rem;
width: 0.5rem;
border-radius: 0.125rem;
background: rgba(255, 255, 255, 0.7);
box-shadow: none;
&:hover
background: rgba(255, 255, 255, 0.8);
&:active
background: rgba(255, 255, 255, 0.6);
</style>

View file

@ -0,0 +1,32 @@
---
import {NavBarLink} from "../../types/config";
import {Icon} from "astro-icon/components";
import {url} from "../../utils/url-utils";
interface Props {
links: NavBarLink[],
}
const links = Astro.props.links;
---
<div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}>
{links.map((link) => (
<a href={link.external ? link.url : url(link.url)} class="group flex justify-between items-center py-2 pl-3 pr-1 rounded-lg gap-8
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition
"
target={link.external ? "_blank" : null}
>
<div class="transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]">
{link.name}
</div>
{!link.external && <Icon name="material-symbols:chevron-right-rounded"
class="transition text-[var(--primary)]" size="20"
>
</Icon>}
{link.external && <Icon name="fa6-solid:arrow-up-right-from-square"
class="transition text-black/25 dark:text-white/25 -translate-x-1" size="12"
>
</Icon>}
</a>
))}
</div>

View file

@ -0,0 +1,39 @@
---
import ImageWrapper from "../misc/ImageWrapper.astro";
import {Icon} from "astro-icon/components";
import {profileConfig} from "../../config";
import {url} from "../../utils/url-utils";
const config = profileConfig;
---
<div class="card-base p-3">
<a aria-label="Go to About Page" href={url('/about/')}
class="group block relative mx-auto mt-1 lg:mx-0 lg:mt-0 mb-3
max-w-[240px] lg:max-w-none overflow-hidden rounded-xl active:scale-95">
<div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
w-full h-full z-50 flex items-center justify-center">
<Icon name="fa6-regular:address-card"
class="transition opacity-0 scale-90 group-hover:scale-100 group-hover:opacity-100 text-white text-5xl">
</Icon>
</div>
<ImageWrapper src={config.avatar} alt="Profile Image of the Author" class="mx-auto lg:w-full h-full lg:mt-0 "></ImageWrapper>
</a>
<div class="px-2">
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div>
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
<div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
<div class="flex gap-2 justify-center mb-1">
{config.links.length > 1 && config.links.map(item =>
<a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
<Icon name={item.icon} size="1.5rem"></Icon>
</a>
)}
{config.links.length == 1 && <a rel="me" aria-label={config.links[0].name} href={config.links[0].url} target="_blank"
class="btn-regular rounded-lg h-10 gap-2 px-3 font-bold active:scale-95">
<Icon name={config.links[0].icon} size="1.5rem"></Icon>
{config.links[0].name}
</a>}
</div>
</div>
</div>

View file

@ -0,0 +1,16 @@
---
import Profile from "./Profile.astro";
import Tag from "./Tags.astro";
import Categories from "./Categories.astro";
const className = Astro.props.class;
---
<div id="sidebar" class:list={[className, "w-full"]}>
<div class="flex flex-col w-full gap-4 mb-4">
<Profile></Profile>
</div>
<div class="flex flex-col w-full gap-4 top-4 sticky top-4">
<Categories class="onload-animation" style="animation-delay: 150ms"></Categories>
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
</div>
</div>

View file

@ -0,0 +1,261 @@
---
import type { MarkdownHeading } from 'astro';
import { siteConfig } from "../../config";
interface Props {
class?: string
headings: MarkdownHeading[]
}
let { headings = [] } = Astro.props;
let minDepth = 10;
for (const heading of headings) {
minDepth = Math.min(minDepth, heading.depth);
}
const className = Astro.props.class
const removeTailingHash = (text: string) => {
let lastIndexOfHash = text.lastIndexOf('#');
if (lastIndexOfHash != text.length - 1) {
return text;
}
return text.substring(0, lastIndexOfHash);
}
let heading1Count = 1;
const maxLevel = siteConfig.toc.depth;
---
<table-of-contents class:list={[className, "group"]}>
{headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) =>
<a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl
hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2
">
<div class:list={["transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
{
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]": heading.depth == minDepth,
"ml-4": heading.depth == minDepth + 1,
"ml-8": heading.depth == minDepth + 2,
}
]}
>
{heading.depth == minDepth && heading1Count++}
{heading.depth == minDepth + 1 && <div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]"></div>}
{heading.depth == minDepth + 2 && <div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10"></div>}
</div>
<div class:list={["transition text-sm", {
"text-50": heading.depth == minDepth || heading.depth == minDepth + 1,
"text-30": heading.depth == minDepth + 2,
}]}>{removeTailingHash(heading.text)}</div>
</a>
)}
<div id="active-indicator" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " +
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
</table-of-contents>
<script>
class TableOfContents extends HTMLElement {
tocEl: HTMLElement | null = null;
visibleClass = "visible";
observer: IntersectionObserver;
anchorNavTarget: HTMLElement | null = null;
headingIdxMap = new Map<string, number>();
headings: HTMLElement[] = [];
sections: HTMLElement[] = [];
tocEntries: HTMLAnchorElement[] = [];
active: boolean[] = [];
activeIndicator: HTMLElement | null = null;
constructor() {
super();
this.observer = new IntersectionObserver(
this.markVisibleSection, { threshold: 0 }
);
};
markVisibleSection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
const id = entry.target.children[0]?.getAttribute("id");
const idx = id ? this.headingIdxMap.get(id) : undefined;
if (idx != undefined)
this.active[idx] = entry.isIntersecting;
if (entry.isIntersecting && this.anchorNavTarget == entry.target.firstChild)
this.anchorNavTarget = null;
});
if (!this.active.includes(true))
this.fallback();
this.update();
};
toggleActiveHeading = () => {
let i = this.active.length - 1;
let min = this.active.length - 1, max = 0;
while (i >= 0 && !this.active[i]) {
this.tocEntries[i].classList.remove(this.visibleClass);
i--;
}
while (i >= 0 && this.active[i]) {
this.tocEntries[i].classList.add(this.visibleClass);
min = Math.min(min, i);
max = Math.max(max, i);
i--;
}
while (i >= 0) {
this.tocEntries[i].classList.remove(this.visibleClass);
i--;
}
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
let scrollOffset = this.tocEl?.scrollTop || 0;
let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
};
scrollToActiveHeading = () => {
// If the TOC widget can accommodate both the topmost
// and bottommost items, scroll to the topmost item.
// Otherwise, scroll to the bottommost one.
if (this.anchorNavTarget || !this.tocEl) return;
const activeHeading =
document.querySelectorAll<HTMLDivElement>(`#toc .${this.visibleClass}`);
if (!activeHeading.length) return;
const topmost = activeHeading[0];
const bottommost = activeHeading[activeHeading.length - 1];
const tocHeight = this.tocEl.clientHeight;
let top;
if (bottommost.getBoundingClientRect().bottom -
topmost.getBoundingClientRect().top < 0.9 * tocHeight)
top = topmost.offsetTop - 32;
else
top = bottommost.offsetTop - tocHeight * 0.8;
this.tocEl.scrollTo({
top,
left: 0,
behavior: "smooth",
});
};
update = () => {
requestAnimationFrame(() => {
this.toggleActiveHeading();
// requestAnimationFrame(() => {
this.scrollToActiveHeading();
// });
});
};
fallback = () => {
if (!this.sections.length) return;
for (let i = 0; i < this.sections.length; i++) {
let offsetTop = this.sections[i].getBoundingClientRect().top;
let offsetBottom = this.sections[i].getBoundingClientRect().bottom;
if (this.isInRange(offsetTop, 0, window.innerHeight)
|| this.isInRange(offsetBottom, 0, window.innerHeight)
|| (offsetTop < 0 && offsetBottom > window.innerHeight)) {
this.markActiveHeading(i);
}
else if (offsetTop > window.innerHeight) break;
}
};
markActiveHeading = (idx: number)=> {
this.active[idx] = true;
};
handleAnchorClick = (event: Event) => {
const anchor = event
.composedPath()
.find((element) => element instanceof HTMLAnchorElement);
if (anchor) {
const id = decodeURIComponent(anchor.hash?.substring(1));
const idx = this.headingIdxMap.get(id);
if (idx !== undefined) {
this.anchorNavTarget = this.headings[idx];
} else {
this.anchorNavTarget = null;
}
}
};
isInRange(value: number, min: number, max: number) {
return min < value && value < max;
};
connectedCallback() {
// wait for the onload animation to finish, which makes the `getBoundingClientRect` return correct values
const element = document.querySelector('.prose');
if (element) {
element.addEventListener('animationend', () => {
this.init();
}, { once: true });
} else {
console.warn('Animation element not found');
}
};
init() {
this.tocEl = document.getElementById(
"toc-inner-wrapper"
);
if (!this.tocEl) return;
this.tocEl.addEventListener("click", this.handleAnchorClick, {
capture: true,
});
this.activeIndicator = document.getElementById("active-indicator");
this.tocEntries = Array.from(
document.querySelectorAll<HTMLAnchorElement>("#toc a[href^='#']")
);
if (this.tocEntries.length === 0) return;
this.sections = new Array(this.tocEntries.length);
this.headings = new Array(this.tocEntries.length);
for (let i = 0; i < this.tocEntries.length; i++) {
const id = decodeURIComponent(this.tocEntries[i].hash?.substring(1));
const heading = document.getElementById(id);
const section = heading?.parentElement;
if (heading instanceof HTMLElement && section instanceof HTMLElement) {
this.headings[i] = heading;
this.sections[i] = section;
this.headingIdxMap.set(id, i);
}
}
this.active = new Array(this.tocEntries.length).fill(false);
this.sections.forEach((section) =>
this.observer.observe(section)
);
this.fallback();
this.update();
};
disconnectedCallback() {
this.sections.forEach((section) =>
this.observer.unobserve(section)
);
this.observer.disconnect();
this.tocEl?.removeEventListener("click", this.handleAnchorClick);
};
}
customElements.define("table-of-contents", TableOfContents);
</script>

View file

@ -0,0 +1,32 @@
---
import WidgetLayout from "./WidgetLayout.astro";
import ButtonTag from "../control/ButtonTag.astro";
import {getTagList} from "../../utils/content-utils";
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
import {url} from "../../utils/url-utils";
const tags = await getTagList();
const COLLAPSED_HEIGHT = "7.5rem";
const isCollapsed = tags.length >= 20;
interface Props {
class?: string;
style?: string;
}
const className = Astro.props.class
const style = Astro.props.style
---
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
<div class="flex gap-2 flex-wrap">
{tags.map(t => (
<ButtonTag href={url(`/archive/tag/${t.name}/`)} label={`View all posts with the ${t.name} tag`}>
{t.name}
</ButtonTag>
))}
</div>
</WidgetLayout>

View file

@ -0,0 +1,65 @@
---
import { Icon } from 'astro-icon/components';
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
interface Props {
id: string;
name?: string;
isCollapsed?: boolean;
collapsedHeight?: string;
class?: string;
style?: string;
}
const props = Astro.props;
const {
id,
name,
isCollapsed,
collapsedHeight,
style,
} = Astro.props
const className = Astro.props.class
---
<widget-layout data-id={id} data-is-collapsed={isCollapsed} class={"pb-4 card-base " + className} style={style}>
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
<div id={id} class:list={["collapse-wrapper px-4 overflow-hidden", {"collapsed": isCollapsed}]}>
<slot></slot>
</div>
{isCollapsed && <div class="expand-btn px-4 -mb-2">
<button class="btn-plain rounded-lg w-full h-9">
<div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2">
<Icon name="material-symbols:more-horiz" size={28}></Icon> {i18n(I18nKey.more)}
</div>
</button>
</div>}
</widget-layout>
<style define:vars={{ collapsedHeight }}>
.collapsed {
height: var(--collapsedHeight);
}
</style>
<script>
class WidgetLayout extends HTMLElement {
constructor() {
super();
if (this.dataset.isCollapsed === undefined || this.dataset.isCollapsed === false)
return;
const id = this.dataset.id;
const btn = this.querySelector('.expand-btn');
const wrapper = this.querySelector(`#${id}`)
btn.addEventListener('click', () => {
wrapper.classList.remove('collapsed');
btn.classList.add('hidden');
})
}
}
customElements.define('widget-layout', WidgetLayout);
</script>

74
src/config.ts Normal file
View file

@ -0,0 +1,74 @@
import type {
LicenseConfig,
NavBarConfig,
ProfileConfig,
SiteConfig,
} from './types/config'
import { LinkPreset } from './types/config'
export const siteConfig: SiteConfig = {
title: 'Hack13 Blog',
subtitle: 'The blog of a enby foxo',
lang: 'en', // 'en', 'zh_CN', 'zh_TW', 'ja'
themeColor: {
hue: 340, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
fixed: true, // Hide the theme color picker for visitors
},
banner: {
enable: false,
src: 'assets/images/demo-banner.png', // Relative to the /src directory. Relative to the /public directory if it starts with '/'
position: 'center', // Equivalent to object-position, defaults center
},
favicon: [
// Leave this array empty to use the default favicon
// {
// src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory
// theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
// }
],
}
export const navBarConfig: NavBarConfig = {
links: [
LinkPreset.Home,
LinkPreset.Archive,
LinkPreset.About,
{
name: 'GitHub',
url: 'https://github.com/hack13', // Internal links should not include the base path, as it is automatically added
external: true, // Show an external link icon and will open in a new tab
},
],
}
export const profileConfig: ProfileConfig = {
avatar: 'assets/images/hack-avatar.png', // Relative to the /src directory. Relative to the /public directory if it starts with '/'
name: 'Hack13',
bio: 'A nerdy enby foxo on the internet.',
links: [
{
name: 'Twitter',
icon: 'fa6-brands:mastodon', // Visit https://icones.js.org/ for icon codes
// You will need to install the corresponding icon set if it's not already included
// `pnpm add @iconify-json/<icon-set-name>`
url: 'https://cyberfurz.social/@hack13',
},
{
name: 'Telegram',
icon: 'fa6-brands:telegram',
url: 'https://t.me/kite5521',
},
{
name: 'Discord',
icon: 'fa6-brands:git',
url: 'https://git.hack13.dev/hack13',
},
],
}
export const licenseConfig: LicenseConfig = {
enable: true,
name: 'CC BY-NC-SA 4.0',
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
}

View file

@ -0,0 +1,6 @@
export const UNCATEGORIZED = '__uncategorized__'
export const PAGE_SIZE = 8
export const LIGHT_MODE = 'light', DARK_MODE = 'dark', AUTO_MODE = 'auto'
export const DEFAULT_THEME = AUTO_MODE

37
src/constants/icon.ts Normal file
View file

@ -0,0 +1,37 @@
import type {Favicon} from "@/types/config.ts";
export const defaultFavicons: Favicon[] = [
{
src: '/favicon/favicon-light-32.png',
theme: 'light',
sizes: '32x32',
}, {
src: '/favicon/favicon-light-128.png',
theme: 'light',
sizes: '128x128',
}, {
src: '/favicon/favicon-light-180.png',
theme: 'light',
sizes: '180x180',
}, {
src: '/favicon/favicon-light-192.png',
theme: 'light',
sizes: '192x192',
}, {
src: '/favicon/favicon-dark-32.png',
theme: 'dark',
sizes: '32x32',
}, {
src: '/favicon/favicon-dark-128.png',
theme: 'dark',
sizes: '128x128',
}, {
src: '/favicon/favicon-dark-180.png',
theme: 'dark',
sizes: '180x180',
}, {
src: '/favicon/favicon-dark-192.png',
theme: 'dark',
sizes: '192x192',
}
]

View file

@ -0,0 +1,18 @@
import { LinkPreset, type NavBarLink } from '@/types/config'
import I18nKey from '@i18n/i18nKey'
import { i18n } from '@i18n/translation'
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
[LinkPreset.Home]: {
name: i18n(I18nKey.home),
url: '/',
},
[LinkPreset.About]: {
name: i18n(I18nKey.about),
url: '/about/',
},
[LinkPreset.Archive]: {
name: i18n(I18nKey.archive),
url: '/archive/',
},
}

16
src/content/config.ts Normal file
View file

@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content'
const postsCollection = defineCollection({
schema: z.object({
title: z.string(),
published: z.date(),
draft: z.boolean().optional(),
description: z.string().optional(),
image: z.string().optional(),
tags: z.array(z.string()).optional(),
category: z.string().optional(),
}),
})
export const collections = {
posts: postsCollection,
}

View file

@ -1,11 +1,11 @@
---
title: Battle of Business vs OpenSource
author: Timothy Rogers
type: post
date: 2012-08-09T04:02:00+00:00
description: Alright lately I have been getting overwhelmed with all the drama of the oldest argument in the world of internet technologies. So who should win? Big business or opensource projects? Well the answer is simple, THE ARE NOT AT WAR!!!!
tags: [OpenSim, Opinion]
published: 2012-08-09
url: /2012/08/battle-of-business-vs-opensource/
categories:
- Archived
category: Archive
draft: false
---
Alright lately I have been getting overwhelmed with all the drama of the oldest argument in the world of internet technologies. So who should win? Big business or opensource projects? Well the answer is simple, THE ARE NOT AT WAR!!!!

View file

@ -1,11 +1,11 @@
---
title: New Years Resolutions
author: Timothy Rogers
type: post
date: 2012-12-31T14:40:00+00:00
description: It is that time of the year, when people start making New Year Resolutions. Well I have never really made any other than once for a class assignment. But this year I really think I need to make some real resolutions. So I thought I would post them here on my personal blog for everyone to read and be aware.
tags: [Personal]
published: 2012-12-31
url: /2012/12/new-years-resolutions/
categories:
- Archived
category: Archive
draft: false
---
It is that time of the year, when people start making New Year Resolutions. Well I have never really made any other than once for a class assignment. But this year I really think I need to make some real resolutions. So I thought I would post them here on my personal blog for everyone to read and be aware.

View file

@ -1,12 +1,12 @@
---
title: Explaining OpenSim Memory Usage
author: Timothy Rogers
type: post
date: 2013-02-11T20:15:00+00:00
description: I have had a couple people ask me to show them some tips and show them how to conserve memory usage in OpenSim. So I am going to be using sim-on-a-stick for today's tips and tricks...
tags: [Resources, OpenSim]
published: 2013-02-11
url: /2013/02/explaining-opensim-memory-usage/
categories:
- Resources
- Archived
category: Archive
draft: false
---
I have had a couple people ask me to show them some tips and show them how to conserve memory usage in OpenSim. So I am going to be using sim-on-a-stick for today's tips and tricks. Now the reason for this is because now with sim-on-a-stick, we have the database running in MySQL which saves a bit of memory, and the fact it is already setup and ready to go out of the box.</p>

View file

@ -1,12 +1,11 @@
---
title: Thank You
author: Timothy Rogers
type: post
date: 2013-02-13T23:32:00+00:00
description: Thank You AuroraScape Community
tags: [AuroraSim]
published: 2013-02-13
url: /2013/02/thank-you/
categories:
- Archived
- Aurora-Sim
category: Archive
draft: false
---
Thank You AuroraScape Community,

View file

@ -1,11 +1,11 @@
---
title: Needing Rest
author: Timothy Rogers
type: post
date: 2013-02-20T05:43:00+00:00
description: It sure has been a long first 20 days, I can't believe how far all of us have come. Yes after the upgrade happened, we lost a nice chunk of regions, but I am sure we will be getting them back soon. All I can say is I am taking a few steps back for the next few days, from grid upgrades.
tags: [AuroraSim]
published: 2013-02-20
url: /2013/02/needing-rest/
categories:
- Archived
category: Archive
draft: false
---
It sure has been a long first 20 days, I can't believe how far all of us have come. Yes after the upgrade happened, we lost a nice chunk of regions, but I am sure we will be getting them back soon. All I can say is I am taking a few steps back for the next few days, from grid upgrades. I will still be around, doing money cash outs, trouble shooting, and processing orders. I just need a break.

View file

@ -1,15 +1,14 @@
---
title: Top 10 Reasons Not To Give Up On Aurora-Sim
author: Timothy Rogers
type: post
date: 2013-02-21T05:48:00+00:00
description: I am very sad to see so many people tell me that I should give up on Aurora-Sim, and that it is a waste. I think it is mostly been placed there because people remember its biggest tank, and the failure of Nova Grid.
tags: [AuroraSim]
published: 2013-02-21
url: /2013/02/top-10-reasons-not-to-give-up-on-aurora-sim/
categories:
- Archived
- Aurora-Sim
category: Archive
draft: false
---
I am very sad to see so many people tell me that I should give up on Aurora-Sim, and that it is a waste. I think it is mostly been placed there because people remember its biggest tank, and the failure of Nova Grid. So I am going to show you the top 10 reasons why I think the metaverse should give it another shot, and get back to helping with its development.</p>
I am very sad to see so many people tell me that I should give up on Aurora-Sim, and that it is a waste. I think it is mostly been placed there because people remember its biggest tank, and the failure of Nova Grid. So I am going to show you the top 10 reasons why I think the metaverse should give it another shot, and get back to helping with its development.
**Ready Out of the Box:** When you download the latest aurora-sim master code, and compile it. First time run, it has a built in fully functional WebUI, Fully Working Groups, Fully Working Profiles, Fully Working Search, and easy to use Region GUI. All of this is built into the code, all running for the core code. This all works even in standalone mode all out of the single Aurora.exe!

View file

@ -1,12 +1,11 @@
---
title: Who Is Timothy Vyper/Hoxley/Rogers
author: Timothy Rogers
type: post
date: 2013-02-23T01:43:00+00:00
description: I just keep reading all this stuff about all these other grids, and the people who have influences there. Now that I am a grid owner, been making myself know more and more over the past year with all the things I have started. Grid-Press, SoftPaw Estates, Zetamex, and my latest AuroraScape.
tags: [Personal]
published: 2013-02-23
url: /2013/02/who-is-timothy-vyperhoxleyrogers/
categories:
- Archived
- Personal
category: Archive
draft: false
---
I just keep reading all this stuff about all these other grids, and the people who have influences there. Now that I am a grid owner, been making myself know more and more over the past year with all the things I have started. Grid-Press, SoftPaw Estates, Zetamex, and my latest AuroraScape. I will admit I am not perfect, hell I never even went to college! Also another zinger for all you people, did you all know that I am only 20 years old? This year I turn 21, on April 28th. Which surprises a lot of people when I tell them all that, but I did take some college classes in High School, because my school was merged with the local college (due to budget cuts) and got 3 certification, that don't mean crap now.

View file

@ -1,15 +1,14 @@
---
title: Why I Closed AuroraScape
author: Timothy Rogers
type: post
date: 2013-03-05T22:20:00+00:00
description: I first want to thank everyone who was on AuroraScape, and I have to apologize to all of you. I know the life of AuroraScape was short but there are multiple reasons to this, and I mean no ill intent to the developers of Aurora by what I have to say in this blog.
tags: [AuroraSim]
published: 2013-03-05
url: /2013/03/why-i-closed-aurorascape/
categories:
- Archived
- Aurora-Sim
category: Archive
draft: false
---
I first want to thank everyone who was on AuroraScape, and I have to apologize to all of you. I know the life of AuroraScape was short but there are multiple reasons to this, and I mean no ill intent to the developers of Aurora by what I have to say in this blog.Firstly, AuroraScape was doing pretty good. But that was how it looked on the outside, there were a lot of problems, things I just cannot work on as I am not a programmer. The software is very stable, very easy to use, and works out of the box. That I am not trying to fight with, but there was just too many people bitching about it.
I first want to thank everyone who was on AuroraScape, and I have to apologize to all of you. I know the life of AuroraScape was short but there are multiple reasons to this, and I mean no ill intent to the developers of Aurora by what I have to say in this blog. Firstly, AuroraScape was doing pretty good. But that was how it looked on the outside, there were a lot of problems, things I just cannot work on as I am not a programmer. The software is very stable, very easy to use, and works out of the box. That I am not trying to fight with, but there was just too many people bitching about it.
I know I shouldn't let people bitching about the software help me close the grid down, but it is not so simple. Many people were just not willing to even give AuroraScape a shot, and many people who joined began to drop out right away. The interest in Aurora-Sim was so limited, and one of the biggest reasons is because the lack of HyperGrid. There were people who literately said things like I will never support an open grid without hypergrid and that type of thing travels and makes other say it and more people see it. People were publicly bitching and complaining about the grid without even giving it a shot. We kept loosing people because of it.

View file

@ -1,12 +1,11 @@
---
title: Closed vs Open? Really?
author: Timothy Rogers
type: post
date: 2013-03-11T12:03:00+00:00
description: I am dealing with a lot of people getting stressed over the debate of if grids should all be open or at least have hypergrid open. I am just going to put this out there, there are tons of people who want everything free in life, and never want to pay for anything.
tags: [OpenSim]
published: 2013-03-11T12:03:00+00:00
url: /2013/03/closed-vs-open-really/
categories:
- Archived
- OpenSimulator
category: Archive
draft: false
---
I am dealing with a lot of people getting stressed over the debate of if grids should all be open or at least have hypergrid open. I am just going to put this out there, there are tons of people who want everything free in life, and never want to pay for anything. I am sorry but the world requires money for us to get things done.

View file

@ -1,12 +1,11 @@
---
title: 'Moving Up & Around'
author: Timothy Rogers
type: post
date: 2013-04-08T13:17:00+00:00
title: Moving Up & Around
description: It has been one hell of a past month, I have had to deal with a lot of drama in my real personal life. Sometimes I wish we could all just live in virtual worlds, but drama would just pile up their too...
tags: [Personal]
published: 2013-04-08
url: /2013/04/moving-up-around/
categories:
- Archived
- Personal
category: Archive
draft: false
---
It has been one hell of a past month, I have had to deal with a lot of drama in my real personal life. Sometimes I wish we could all just live in virtual worlds, but drama would just pile up their too... so what else is new on that. But besides that point, I have been doing some plotting and planning with Zetamex these past few days so going to be talking a bit about that in this blog. I am also going to gloat about my new boy friend, because I can tell you right now, without him this past month would have just been me crying my heart out.

View file

@ -1,12 +1,11 @@
---
title: My Clocks Are Ticking
author: Timothy Rogers
type: post
date: 2013-04-26T01:41:00+00:00
description: Well it has been a while since I have written on my personal blog again. However I have been feeling a little down lately and I think I need to write to pick up my spirits a bit again.
tags: [Personal]
published: 2013-04-26
url: /2013/04/my-clocks-are-ticking/
categories:
- Archived
- Personal
category: Archive
draft: false
---
Well it has been a while since I have written on my personal blog again. However I have been feeling a little down lately and I think I need to write to pick up my spirits a bit again. I just been going through a lot of feelings lately. I guess I should start from the top, which is well I am just so immersed in my work I feel I am loosing more and more touch with my real life. I mean not that is a bad thing, but I am getting to the point that I blow off my boyfriend. I already lost one boyfriend, that I had been with for 2 years, it wasn't only because I was so immersed in my work there were other reasons but this was part of it.

View file

@ -1,12 +1,11 @@
---
title: Upset With Stiffled Innovation
author: Timothy Rogers
type: post
date: 2013-06-19T12:04:00+00:00
description: I am constantly hearing about all these people stiffing innovation, well today I had the last straw I could take. Before I begin, I want to make sure say this boldly and clearly.
tags: [OpenSim]
published: 2013-06-19
url: /2013/06/upset-with-stiffled-innovation/
categories:
- Archived
- OpenSimulator
category: Archive
draft: false
---
I am constantly hearing about all these people stiffing innovation, well today I had the last straw I could take. Before I begin, I want to make sure say this boldly and clearly. **I do not directly represent Zetamex's personal interests, Zetamex represents it's own identity at http://blog.zetamex.com/ and anything said on this blog is unrelated to Zetamex's personal interests.** Now that I got that out of the way, I want to tell you how I was rudely addressed for the last time.

Some files were not shown because too many files have changed in this diff Show more