project baseline

This commit is contained in:
2026-03-01 14:46:22 +01:00
commit 8834a78b54
215 changed files with 105982 additions and 0 deletions

21
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,21 @@
# Contributing
Thank you for your interest in contributing!
## Before You Start
If you plan to make major changes (especially new features or design changes), please open an issue or discussion before starting work. This helps ensure your effort aligns with the project's direction.
## Submitting Code
Please keep each pull request focused on a single purpose. Avoid mixing unrelated changes in one PR, as this can make reviewing and merging code more difficult.
Please use the [Conventional Commits](https://www.conventionalcommits.org/) format for your commit messages whenever possible. This keeps our history clear and consistent.
Before submitting code, please run the appropriate commands to check for errors and format your code.
```bash
pnpm check
pnpm format
```

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.

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
# 🍥Fuwari
![Node.js >= 20](https://img.shields.io/badge/node.js-%3E%3D20-brightgreen)
![pnpm >= 9](https://img.shields.io/badge/pnpm-%3E%3D9-blue)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-saicaca%2Ffuwari-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/saicaca/fuwari)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_shield&issueType=license)
A static blog template built with [Astro](https://astro.build).
[**🖥️ Live Demo (Vercel)**](https://fuwari.vercel.app)
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
🌏 README in
[**中文**](https://github.com/saicaca/fuwari/blob/main/docs/README.zh-CN.md) /
[**日本語**](https://github.com/saicaca/fuwari/blob/main/docs/README.ja.md) /
[**한국어**](https://github.com/saicaca/fuwari/blob/main/docs/README.ko.md) /
[**Español**](https://github.com/saicaca/fuwari/blob/main/docs/README.es.md) /
[**ไทย**](https://github.com/saicaca/fuwari/blob/main/docs/README.th.md) /
[**Tiếng Việt**](https://github.com/saicaca/fuwari/blob/main/docs/README.vi.md) /
[**Bahasa Indonesia**](https://github.com/saicaca/fuwari/blob/main/docs/README.id.md) (Provided by the community and may not always be up-to-date)
## ✨ 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
- [x] Search functionality with [Pagefind](https://pagefind.app/)
- [x] [Markdown extended features](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax)
- [x] Table of contents
- [x] RSS feed
## 🚀 Getting Started
1. Create your blog repository:
- [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template or fork this repository.
- Or run one of the following commands:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. To edit your blog locally, clone your repository, run `pnpm install` 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: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # Set only if the post's language differs from the site's language in `config.ts`
---
```
## 🧩 Markdown Extended Syntax
In addition to Astro's default support for [GitHub Flavored Markdown](https://github.github.com/gfm/), several extra Markdown features are included:
- Admonitions ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- GitHub repository cards ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Enhanced code blocks with Expressive Code ([Preview](https://fuwari.vercel.app/posts/expressive-code/) / [Docs](https://expressive-code.com/))
## ⚡ Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
|:---------------------------|:----------------------------------------------------|
| `pnpm install` | 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 check` | Run checks for errors in your code |
| `pnpm format` | Format your code using Biome |
| `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 |
## ✏️ Contributing
Check out the [Contributing Guide](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) for details on how to contribute to this project.
## 📄 License
This project is licensed under the MIT License.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_large&issueType=license)

178
astro.config.mjs Normal file
View File

@@ -0,0 +1,178 @@
import sitemap from "@astrojs/sitemap";
import svelte from "@astrojs/svelte";
import tailwind from "@astrojs/tailwind";
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
import swup from "@swup/astro";
import expressiveCode from "astro-expressive-code";
import icon from "astro-icon";
import { defineConfig } from "astro/config";
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 remarkSectionize from "remark-sectionize";
import { expressiveCodeConfig } from "./src/config.ts";
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts";
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 { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
// https://astro.build/config
export default defineConfig({
site: "https://fuwari.vercel.app/",
base: "/",
trailingSlash: "always",
integrations: [
tailwind({
nesting: true,
}),
swup({
theme: false,
animationClass: "transition-swup-", // see https://swup.js.org/options/#animationselector
// the default value `transition-` cause transition delay
// when the Tailwind class `transition-all` is used
containers: ["main", "#toc"],
smoothScrolling: true,
cache: true,
preload: true,
accessibility: true,
updateHead: true,
updateBodyClass: false,
globalInstance: true,
}),
icon({
include: {
"preprocess: vitePreprocess(),": ["*"],
"fa6-brands": ["*"],
"fa6-regular": ["*"],
"fa6-solid": ["*"],
},
}),
expressiveCode({
themes: [expressiveCodeConfig.theme, expressiveCodeConfig.theme],
plugins: [
pluginCollapsibleSections(),
pluginLineNumbers(),
pluginLanguageBadge(),
pluginCustomCopyButton()
],
defaultProps: {
wrap: true,
overridesByLang: {
'shellsession': {
showLineNumbers: false,
},
},
},
styleOverrides: {
codeBackground: "var(--codeblock-bg)",
borderRadius: "0.75rem",
borderColor: "none",
codeFontSize: "0.875rem",
codeFontFamily: "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
codeLineHeight: "1.5rem",
frames: {
editorBackground: "var(--codeblock-bg)",
terminalBackground: "var(--codeblock-bg)",
terminalTitlebarBackground: "var(--codeblock-topbar-bg)",
editorTabBarBackground: "var(--codeblock-topbar-bg)",
editorActiveTabBackground: "none",
editorActiveTabIndicatorBottomColor: "var(--primary)",
editorActiveTabIndicatorTopColor: "none",
editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)",
terminalTitlebarBorderBottomColor: "none"
},
textMarkers: {
delHue: 0,
insHue: 180,
markHue: 250
}
},
frames: {
showCopyToClipboardButton: false,
}
}),
svelte(),
sitemap(),
],
markdown: {
remarkPlugins: [
remarkMath,
remarkReadingTime,
remarkExcerpt,
remarkGithubAdmonitionsToDirectives,
remarkDirective,
remarkSectionize,
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: {
server: {
watch: {
ignored: ['**/.obsidian/**', '**/_bases/**', '**/bases/**', '**/_home/**', '**/home/**', '**/_base/**', '**/base/**']
}
},
assetsInclude: ['**/*.base', '**/.obsidian/**', '**/_bases/**'],
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);
},
},
},
},
});

63
biome.json Normal file
View File

@@ -0,0 +1,63 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": [
"**",
"!**/src/**/*.css",
"!**/src/public/**/*",
"!**/dist/**/*",
"!**/node_modules/**/*"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"overrides": [
{
"includes": ["**/*.svelte", "**/*.astro", "**/*.vue"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useImportType": "off"
},
"correctness": {
"noUnusedVariables": "off",
"noUnusedImports": "off"
}
}
}
}
]
}

7580
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

85
docs/README.es.md Normal file
View File

@@ -0,0 +1,85 @@
# 🍥Fuwari
Un tema estático para blogs construido con [Astro](https://astro.build).
[**🖥️ Demostración en Vivo (Vercel)**](https://fuwari.vercel.app)
![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
- [x] TOC (Tabla de Contenidos)
## 👀 requiere
- Node.js <= 22
- pnpm <= 9
## 🚀 Cómo Usar 1
Inicializa el proyecto localmente usando [create-fuwari](https://github.com/L4Ph/create-fuwari).
```sh
# npm
npm create fuwari@latest.
# yarn
yarn create fuwari.
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. Edita el archivo de configuración `src/config.ts` para personalizar tu blog.
2. Ejecuta `pnpm new-post <nombre-de-archivo>` para crear una nueva entrada y edítala en `src/content/posts/`.
3. 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.
## 🚀 Cómo Usar 2
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 |

106
docs/README.id.md Normal file
View File

@@ -0,0 +1,106 @@
# 🍥 Fuwari
Template blog statis yang dibangun dengan [Astro](https://astro.build).
[**🖥️ Demo Langsung (Vercel)**](https://fuwari.vercel.app)
![Gambar Pratinjau](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
🌏 README dalam
[**中文**](https://github.com/saicaca/fuwari/blob/main/docs/README.zh-CN.md) /
[**日本語**](https://github.com/saicaca/fuwari/blob/main/docs/README.ja.md) /
[**한국어**](https://github.com/saicaca/fuwari/blob/main/docs/README.ko.md) /
[**Español**](https://github.com/saicaca/fuwari/blob/main/docs/README.es.md) /
[**ไทย**](https://github.com/saicaca/fuwari/blob/main/docs/README.th.md) /
[**Tiếng Việt**](https://github.com/saicaca/fuwari/blob/main/docs/README.vi.md) /
**Bahasa Indonesia (ini)** (Disediakan oleh komunitas, mungkin tidak selalu paling mutakhir)
## ✨ Fitur
- [x] Dibangun dengan [Astro](https://astro.build) dan [Tailwind CSS](https://tailwindcss.com)
- [x] Animasi dan transisi halaman yang halus
- [x] Mode terang / gelap
- [x] Warna tema & banner yang bisa dikustomisasi
- [x] Desain responsif
- [x] Fitur pencarian dengan [Pagefind](https://pagefind.app/)
- [x] [Fitur markdown tambahan](#-markdown-sintaks-ekstensi)
- [x] Daftar isi (Table of Contents)
- [x] RSS feed
## 🚀 Memulai
1. Buat repositori blog kamu:
- [Generate repositori baru](https://github.com/saicaca/fuwari/generate) dari template ini atau fork repositori ini.
- Atau jalankan salah satu perintah berikut:
```sh
# npm
npm create fuwari@latest.
# yarn
yarn create fuwari.
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
2. Untuk mengedit blog secara lokal, klon repositori kamu, jalankan `pnpm install` untuk instalasi dependensi.
- Install [pnpm](https://pnpm.io) `npm install -g pnpm` jika belum punya.
3. Edit file konfigurasi `src/config.ts` untuk menyesuaikan blog.
4. Jalankan `pnpm new-post <nama-file>` untuk membuat postingan baru dan edit di `src/content/posts/`.
5. Deploy blog ke Vercel, Netlify, GitHub Pages, dll. sesuai [panduan](https://docs.astro.build/en/guides/deploy/). Jangan lupa edit konfigurasi situs di `astro.config.mjs` sebelum deploy.
## 📝 Frontmatter Postingan
```yaml
---
title: Judul Postingan Pertama Saya
published: 2023-09-09
description: Ini adalah postingan pertama blog Astro saya.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: id # Isi hanya jika bahasa postingan berbeda dari bahasa default di `config.ts`
---
```
## 🧩 Markdown Sintaks Ekstensi
Selain dukungan default Astro untuk [GitHub Flavored Markdown](https://github.github.com/gfm/), terdapat beberapa fitur tambahan:
- Admonisi ([Pratinjau & Cara Pakai](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- Kartu repositori GitHub ([Pratinjau & Cara Pakai](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Kode blok ekspresif lewat Expressive Code ([Pratinjau](https://fuwari.vercel.app/posts/expressive-code/) / [Dokumentasi](https://expressive-code.com/))
## ⚡ Perintah
Semua perintah dijalankan dari root proyek, via terminal:
| Perintah | Aksi |
|:-----------------------------|:----------------------------------------------------------|
| `pnpm install` | Instalasi dependensi |
| `pnpm dev` | Menjalankan server dev lokal di `localhost:4321` |
| `pnpm build` | Build untuk produksi ke folder `./dist/` |
| `pnpm preview` | Pratinjau hasil build sebelum deploy |
| `pnpm check` | Cek error atau masalah di kode |
| `pnpm format` | Format kode dengan Biome |
| `pnpm new-post <nama-file>` | Membuat postingan baru |
| `pnpm astro ...` | Jalankan perintah CLI seperti `astro add`, `astro check` |
| `pnpm astro --help` | Bantuan menggunakan Astro CLI |
## ✏️ Kontribusi
Lihat [Panduan Kontribusi](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) untuk detail tentang cara berkontribusi ke proyek ini.
## 📄 Lisensi
Proyek ini dilisensikan di bawah MIT License.
---
> Dokumentasi ini tersedia dalam Bahasa Indonesia. Untuk bahasa lain, lihat README di direktori docs.

85
docs/README.ja.md Normal file
View File

@@ -0,0 +1,85 @@
# 🍥Fuwari
[Astro](https://astro.build) で構築された静的ブログテンプレート
[**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app)
![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] 検索機能
- [x] 目次
## 👀 以下が必要
- Node.js <= 22
- pnpm <= 9
## 🚀 使用方法 1
[create-fuwari](https://github.com/L4Ph/create-fuwari)を使用して、ローカルにプロジェクトを初期化します。
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. `src/config.ts` ファイルを編集する事でブログを自分好みにカスタマイズ出来ます。
2. `pnpm new-post <filename>` で新しい記事を作成し、`src/content/posts/`.フォルダ内で編集します。
3. 作成したブログをVercel、Netlify、GitHub Pagesなどにデプロイするには[ガイド](https://docs.astro.build/ja/guides/deploy/)に従って下さい。加えて、別途デプロイを行う前に `astro.config.mjs` を編集してサイト構成を変更する必要があります。
## 🚀 使用方法 2
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のヘルプを表示 |

82
docs/README.ko.md Normal file
View File

@@ -0,0 +1,82 @@
# 🍥Fuwari
[Astro](https://astro.build)로 구축된 정적 블로그 템플릿입니다.
[**🖥️미리보기 (Vercel)**](https://fuwari.vercel.app)
![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] [Pagefind](https://pagefind.app/)를 이용한 검색 기능
- [x] [Markdown 확장 기능](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax)
- [x] 목차
- [x] RSS 피드
## 🚀 시작하기
1. 블로그 저장소를 생성하세요:
- 이 템플릿에서 [새 저장소를 생성](https://github.com/saicaca/fuwari/generate)하거나 이 저장소를 포크하세요.
- 또는 다음 명령어 중 하나를 실행하세요:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. 로컬에서 블로그를 수정하려면, 저장소를 복제하고 `pnpm install`을 실행하여 종속성을 설치하세요.
- [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/en/guides/deploy/)에 따라 블로그를 Vercel, Netlify, Github Pages 등에 배포하세요. 배포하기 전에 `astro.config.mjs`에서 사이트 구성을 수정해야 합니다.
## ⚙️ 게시물의 머리말 설정
```yaml
---
title: 내 첫 블로그 게시물
published: 2023-09-09
description: 내 새로운 Astro 블로그의 첫 번째 게시물입니다!
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # 게시물의 언어가 `config.ts`의 사이트 언어와 다른 경우에만 설정합니다.
---
```
## 🧩 마크다운 확장 구문
Astro의 기본 [GitHub Flavored Markdown](https://github.github.com/gfm/) 지원 외에도 몇 가지 추가적인 마크다운 기능이 포함되어 있습니다.
- Admonitions ([미리보기 및 사용법](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- GitHub 저장소 카드 ([미리보기 및 사용법](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Expressive Code를 사용한 향상된 코드 블록 ([미리보기](https://fuwari.vercel.app/posts/expressive-code/) / [문서](https://expressive-code.com/))
## ⚡ 명령어
모든 명령어는 프로젝트 최상단, 터미널에서 실행됩니다:
| Command | Action |
|:------------------------------------|:-------------------------------------------------|
| `pnpm install` | 종속성을 설치합니다. |
| `pnpm dev` | `localhost:4321`에서 로컬 개발 서버를 시작합니다. |
| `pnpm build` | `./dist/`에 프로덕션 사이트를 구축합니다. |
| `pnpm check` | 코드에서 오류를 확인합니다. |
| `pnpm format` | Biome을 사용하여 코드를 포멧합니다. |
| `pnpm preview` | 배포하기 전에 로컬에서 빌드 미리보기 |
| `pnpm new-post <filename>` | 새 게시물 작성 |
| `pnpm astro ...` | `astro add`, `astro check`와 같은 CLI 명령어 실행 |
| `pnpm astro --help` | Astro CLI를 사용하여 도움 받기 |
## ✏️ 기여
이 프로젝트에 기여하는 방법에 대한 자세한 내용은 [기여 가이드](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md)를 확인하세요.
## 📄 라이선스
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_large&issueType=license)

84
docs/README.th.md Normal file
View File

@@ -0,0 +1,84 @@
# 🍥Fuwari
แม่แบบสำหรับเว็บบล็อกแบบ static สร้างด้วย [Astro](https://astro.build)
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app)
![ภาพตัวอย่าง](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] ฟังก์ชันการค้นหา ขับเคลื่อนด้วย [Pagefind](https://pagefind.app/)
- [x] [คุณสมบัติเพิ่มเติมสำหรับมาร์กดาวน์](https://github.com/saicaca/fuwari/blob/main/docs/README.th.md#-markdown-extended-syntax)
- [x] สารบัญ
- [x] RSS feed
## 🚀 เริ่มต้นใช้งาน
1. สร้าง repository ใหม่สำหรับบล็อกของคุณ:
- [Generate repository ใหม่](https://github.com/saicaca/fuwari/generate) ขึ้นมาจากแม่แบบนี้ หรือจะ fork repository นี้ก็ได้
- หรือจะสร้างโดยการเลือกรันคำสั่งต่อไปนี้ คำสั่งใดคำสั่งหนึ่ง:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. เริ่มแก้ไขบล็อกของคุณแบบ local โดยการ clone repository ของคุณ (จากข้อ 1) ไว้ในเครื่องของคุณ แล้วรันคำสั่ง `pnpm install` เพื่อติดตั้ง 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` เท่านั้น
---
```
## 🧩 Markdown Extended Syntax
เดิมที Astro มีการสนับสนุน[ภาษามาร์กดาวน์แบบของ GitHub](https://github.github.com/gfm/) ไว้อยู่แล้ว แต่ Fuwari ได้เพิ่มเติมคุณสมบัติพิเศษอื่น ๆ เข้าไปอีก:
- Admonitions หรือ กล่องข้อมูลพิเศษ ([ดูตัวอย่างและการใช้งาน](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- การ์ด GitHub Repository ([ดูตัวอย่างและการใช้งาน](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- บล็อกโค้ดขั้นสูง ด้วย Expressive Code ([ดูตัวอย่าง](https://fuwari.vercel.app/posts/expressive-code/) / [เอกสารประกอบ](https://expressive-code.com/))
## ⚡ คำสั่ง
คำสั่งที่รันได้ใน terminal จาก root ของโปรเจกต์:
| คำสั่ง | การทำงาน |
|:---------------------------|:-------------------------------------------------------|
| `pnpm install` | ติดตั้ง dependencies |
| `pnpm dev` | เปิดเซิร์ฟเวอร์สำหรับการพัฒนาแบบ local ที่ `localhost:4321` |
| `pnpm build` | Build เว็บไซต์สำหรับใช้งานจริงไปยังโฟลเดอร์ `./dist/` |
| `pnpm preview` | ดูตัวอย่าง build ของคุณแบบ local ก่อนที่จะ deploy จริง |
| `pnpm check` | ดำเนินการตรวจสอบหาข้อผิดพลาดในโค้ดของคุณ |
| `pnpm format` | จัดรูปแบบโค้ดของคุณด้วย Biome |
| `pnpm new-post <filename>` | สร้างโพสต์ใหม่ |
| `pnpm astro ...` | รันคำสั่ง CLI เช่น `astro add`, `astro check` |
| `pnpm astro --help` | แสดงวิธีใช้งาน Astro CLI |
## ✏️ การมีส่วนร่วม
กรุณาอ่าน [แนวทางการมีส่วนร่วม](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) สำหรับรายละเอียดวิธีการมีส่วนร่วมในโปรเจกต์นี้
## 📄 สัญญาอนุญาต
โปรเจกต์นี้เผยแพร่ภายใต้สัญญาอนุญาตแบบ MIT License

84
docs/README.vi.md Normal file
View File

@@ -0,0 +1,84 @@
# 🍥Fuwari
Một mẫu blog tĩnh được xây bằng [Astro](https://astro.build).
[**🖥️ Xem bản dùng thử (Vercel)**](https://fuwari.vercel.app)
![Hình ảnh xem trước](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ Tính năng
- [x] Được xây dựng bằng [Astro](https://astro.build) và [Tailwind CSS](https://tailwindcss.com)
- [x] Có hoạt ảnh đổi chuyển trang mượt mà
- [x] Chế độ sáng / tối
- [x] Màu sắc và biểu ngữ có thể tùy chỉnh được
- [x] Thiết kế nhanh nhạy
- [x] Có chức năng tìm kiếm với [Pagefind](https://pagefind.app/)
- [x] [Có các tính năng mở rộng của Markdown](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax)
- [x] Có mục lục
- [x] Nguồn cấp dữ liệu RSS
## 🚀 Bắt đầu
1. Tạo kho lưu trữ blog của bạn:
- [Tạo một kho lưu trữ mới](https://github.com/saicaca/fuwari/generate) từ mẫu này hoặc fork kho lưu trữ này.
- Hoặc chạy một trong các lệnh sau:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. Để chỉnh sửa blog của bạn trên máy cục bộ, hãy clone kho lưu trữ của bạn, chạy lệnh `pnpm install` để cài đặt các phụ thuộc..
- Cài đặt [pnpm](https://pnpm.io) `npm install -g pnpm` nếu chưa có.
3. Chỉnh sửa tệp cấu hình `src/config.ts` để tùy chỉnh blog của bạn.
4. Chạy `pnpm new-post <filename>` để tạo một bài viết mới và chỉnh sửa nó trong `src/content/posts/`.
5. Triển khai blog của bạn lên Vercel, Netlify, GitHub Pages, etc. theo [chỉ dẫn](https://docs.astro.build/en/guides/deploy/). Bạn cần chỉnh sửa cấu hình trang web trong `astro.config.mjs` trước khi triển khai.
## 📝 Tiêu đề đầy đủ của bài viết
```yaml
---
title: Blog đầu tiên của mình
published: 2023-09-09
description: Đây là bài viết đầu tiên vủa mình trên trang blog tạo bằng Astro này.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # Chỉ đặt nếu ngôn ngữ của bài viết khác với ngôn ngữ của trang web trong `config.ts`
---
```
## 🧩 Cú pháp Markdown mở rộng
Ngoài việc Astro đã có hỗ trợ mặc định cho [Markdown vị Github](https://github.github.com/gfm/), một số tính năng Markdown khác cũng đã được bổ sung:
- Chêm xen ([Xem trước và Cách sử dụng](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- Thẻ hiển thị kho lưu trữ GitHub ([Xem trước và Cách sử dụng](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Các khối mã nâng cao với Expressive Code ([Xem trước](https://fuwari.vercel.app/posts/expressive-code/) / [Tài liệu](https://expressive-code.com/))
## ⚡ Lệnh
Tất cả các lệnh được chạy từ thư mục gốc của dự án, từ một bảng điều khiển:
| Lệnh | Mục đích |
|:---------------------------|:----------------------------------------------------|
| `pnpm install` | Cài đặt các phụ thuộc |
| `pnpm dev` | Khởi động máy chủ cục bộ tại `localhost:4321` |
| `pnpm build` | Xây dựng trang web của bạn vào `./dist/` |
| `pnpm preview` | Xem trước bản web cục bộ của bạn, trước khi triển khai |
| `pnpm check` | Chạy kiểm tra lỗi trong mã của bạn |
| `pnpm format` | Định dạng mã của bạn bằng Biome |
| `pnpm new-post <filename>` | Tạo một bài viết mới |
| `pnpm astro ...` | Chạy các lệnh CLI như `astro add`, `astro check` |
| `pnpm astro --help` | Nhận trợ giúp sử dụng Astro CLI |
## ✏️ Đóng góp
Xem [Hướng dẫn đóng góp](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) để biết thêm chi tiết về cách đóng góp cho dự án này.
## 📄 Giấy phép
Dự án này đã được cấp Giấy phép MIT.

86
docs/README.zh-CN.md Normal file
View File

@@ -0,0 +1,86 @@
# 🍥Fuwari
基于 [Astro](https://astro.build) 开发的静态博客模板。
[**🖥在线预览Vercel**](https://fuwari.vercel.app)
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ 功能特性
- [x] 基于 Astro 和 Tailwind CSS 开发
- [x] 流畅的动画和页面过渡
- [x] 亮色 / 暗色模式
- [x] 自定义主题色和横幅图片
- [x] 响应式设计
- [ ] 评论
- [x] 搜索
- [x] 文内目录
## 👀 要求
- Node.js <= 22
- pnpm <= 9
## 🚀 使用方法 1
使用 [create-fuwari](https://github.com/L4Ph/create-fuwari) 在本地初始化项目。
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. 通过配置文件 `src/config.ts` 自定义博客
2. 执行 `pnpm new-post <filename>` 创建新文章,并在 `src/content/posts/` 目录中编辑
3. 参考[官方指南](https://docs.astro.build/zh-cn/guides/deploy/)将博客部署至 Vercel, Netlify, GitHub Pages 等;部署前需编辑 `astro.config.mjs` 中的站点设置。
## 🚀 使用方法 2
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: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # 仅当文章语言与 `config.ts` 中的网站语言不同时需要设置
---
```
## 🧞 指令
下列指令均需要在项目根目录执行:
| 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 帮助 |

67
frontmatter.json Normal file
View File

@@ -0,0 +1,67 @@
{
"$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"
},
{
"title": "language",
"name": "language",
"type": "string"
}
]
}
]
}

76
package.json Normal file
View File

@@ -0,0 +1,76 @@
{
"name": "fuwari",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"check": "astro check",
"build": "astro build && pagefind --site dist",
"preview": "astro preview",
"astro": "astro",
"type-check": "tsc --noEmit --isolatedDeclarations",
"new-post": "node scripts/new-post.js",
"format": "biome format --write ./src",
"lint": "biome check --write ./src",
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/rss": "^4.0.14",
"@astrojs/sitemap": "^3.6.0",
"@astrojs/svelte": "7.2.3",
"@astrojs/tailwind": "^6.0.2",
"@expressive-code/core": "^0.41.4",
"@expressive-code/plugin-collapsible-sections": "^0.41.4",
"@expressive-code/plugin-line-numbers": "^0.41.4",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@fontsource/roboto": "^5.2.9",
"@iconify-json/fa6-brands": "^1.2.6",
"@iconify-json/fa6-regular": "^1.2.4",
"@iconify-json/fa6-solid": "^1.2.4",
"@iconify-json/material-symbols": "^1.2.50",
"@iconify/svelte": "^4.2.0",
"@swup/astro": "^1.7.0",
"@tailwindcss/typography": "^0.5.19",
"astro": "5.13.10",
"astro-expressive-code": "^0.41.4",
"astro-icon": "^1.1.5",
"hastscript": "^9.0.1",
"katex": "^0.16.27",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.12.0",
"pagefind": "^1.4.0",
"photoswipe": "^5.4.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-components": "^0.3.0",
"rehype-katex": "^7.0.1",
"rehype-slug": "^6.0.0",
"remark-directive": "^3.0.1",
"remark-directive-rehype": "^0.4.2",
"remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0",
"sanitize-html": "^2.17.0",
"sharp": "^0.34.5",
"stylus": "^0.64.0",
"svelte": "^5.39.8",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.6",
"@biomejs/biome": "2.2.5",
"@rollup/plugin-yaml": "^4.1.2",
"@types/hast": "^3.0.4",
"@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0",
"postcss-import": "^16.1.1",
"postcss-nesting": "^13.0.2"
},
"packageManager": "pnpm@9.14.4"
}

6
pagefind.yml Normal file
View File

@@ -0,0 +1,6 @@
exclude_selectors:
- "span.katex"
- "span.katex-display"
- "[data-pagefind-ignore]"
- ".search-panel"
- "#search-panel"

11863
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.0 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

59
scripts/new-post.js Normal file
View File

@@ -0,0 +1,59 @@
/* 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(`Error: File ${fullPath} already exists `)
process.exit(1)
}
// recursive mode creates multi-level directories
const dirPath = path.dirname(fullPath)
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
const content = `---
title: ${args[0]}
published: ${getDate()}
description: ''
image: ''
tags: []
category: ''
draft: false
lang: ''
---
`
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

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import { onMount } from "svelte";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getPostUrlBySlug } from "../utils/url-utils";
export let tags: string[];
export let categories: string[];
export let sortedPosts: Post[] = [];
const params = new URLSearchParams(window.location.search);
tags = params.has("tag") ? params.getAll("tag") : [];
categories = params.has("category") ? params.getAll("category") : [];
const uncategorized = params.get("uncategorized");
interface Post {
slug: string;
data: {
title: string;
tags: string[];
category?: string;
published: Date;
};
}
interface Group {
year: number;
posts: Post[];
}
let groups: Group[] = [];
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(tagList: string[]) {
return tagList.map((t) => `#${t}`).join(" ");
}
onMount(async () => {
let filteredPosts: Post[] = sortedPosts;
if (tags.length > 0) {
filteredPosts = filteredPosts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (categories.length > 0) {
filteredPosts = filteredPosts.filter(
(post) => post.data.category && categories.includes(post.data.category),
);
}
if (uncategorized) {
filteredPosts = filteredPosts.filter((post) => !post.data.category);
}
const grouped = filteredPosts.reduce(
(acc, post) => {
const year = post.data.published.getFullYear();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(post);
return acc;
},
{} as Record<number, Post[]>,
);
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
year: Number.parseInt(yearStr, 10),
posts: grouped[Number.parseInt(yearStr, 10)],
}));
groupedPostsArray.sort((a, b) => b.year - a.year);
groups = groupedPostsArray;
});
</script>
<div class="card-base px-8 py-6">
{#each groups as 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(group.posts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)}
</div>
</div>
{#each group.posts as 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>
{/each}
</div>
{/each}
</div>

View File

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

View File

@@ -0,0 +1,21 @@
---
import { profileConfig } from "../config";
import { url } from "../utils/url-utils";
const currentYear = new Date().getFullYear();
---
<!--<div class="border-t border-[var(&#45;&#45;primary)] mx-16 border-dashed py-8 max-w-[var(&#45;&#45;page-width)] flex flex-col items-center justify-center px-6">-->
<div class="transition border-t border-black/10 dark:border-white/15 my-10 border-dashed mx-32"></div>
<!--<div class="transition bg-[oklch(92%_0.01_var(&#45;&#45;hue))] dark:bg-black rounded-2xl py-8 mt-4 mb-8 flex flex-col items-center justify-center px-6">-->
<div class="transition border-dashed border-[oklch(85%_0.01_var(--hue))] dark:border-white/15 rounded-2xl mb-12 flex flex-col items-center justify-center px-6">
<div class="transition text-50 text-sm text-center">
&copy; <span id="copyright-year">{currentYear}</span> {profileConfig.name}. All Rights Reserved. /
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href={url('rss.xml')}>RSS</a> /
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href={url('sitemap-index.xml')}>Sitemap</a><br>
Powered by
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://astro.build">Astro</a> &
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a>
</div>
</div>

View File

@@ -0,0 +1,3 @@
---
---

View File

@@ -0,0 +1,99 @@
<script lang="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";
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE];
let mode: LIGHT_DARK_MODE = $state(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" onmouseleave={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" onclick={toggleScheme} onmouseenter={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}
onclick={() => 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}
onclick={() => 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}
onclick={() => 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>

141
src/components/Navbar.astro Normal file
View File

@@ -0,0 +1,141 @@
---
import { Icon } from "astro-icon/components";
import { navBarConfig, siteConfig } from "../config";
import { LinkPresets } from "../constants/link-presets";
import { LinkPreset, type NavBarLink } from "../types/config";
import { url } from "../utils/url-utils";
import LightDarkSwitch from "./LightDarkSwitch.svelte";
import Search from "./Search.svelte";
import DisplaySettings from "./widget/DisplaySettings.svelte";
import NavMenuPanel from "./widget/NavMenuPanel.astro";
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 id="navbar" class="z-50 onload-animation">
<div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation -->
<div class:list={[
className,
"card-base !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" class="text-[1.75rem] 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 name="fa6-solid:arrow-up-right-from-square" class="text-[0.875rem] 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:only="svelte"></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" class="text-[1.25rem]"></Icon>
</button>
)}
<LightDarkSwitch client:only="svelte"></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" class="text-[1.25rem]"></Icon>
</button>
</div>
<NavMenuPanel links={links}></NavMenuPanel>
<DisplaySettings client:only="svelte"></DisplaySettings>
</div>
</div>
<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");
if (switchBtn) {
switchBtn.onclick = function () {
switchTheme()
};
}
let settingBtn = document.getElementById("display-settings-switch");
if (settingBtn) {
settingBtn.onclick = function () {
let settingPanel = document.getElementById("display-setting");
if (settingPanel) {
settingPanel.classList.toggle("float-panel-closed");
}
};
}
let menuBtn = document.getElementById("nav-menu-switch");
if (menuBtn) {
menuBtn.onclick = function () {
let menuPanel = document.getElementById("nav-menu-panel");
if (menuPanel) {
menuPanel.classList.toggle("float-panel-closed");
}
};
}
}
loadButtonScript();
</script>
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
async function loadPagefind() {
try {
const response = await fetch(scriptUrl, { method: 'HEAD' });
if (!response.ok) {
throw new Error(`Pagefind script not found: ${response.status}`);
}
const pagefind = await import(scriptUrl);
await pagefind.options({
excerptLength: 20
});
window.pagefind = pagefind;
document.dispatchEvent(new CustomEvent('pagefindready'));
console.log('Pagefind loaded and initialized successfully, event dispatched.');
} catch (error) {
console.error('Failed to load Pagefind:', error);
window.pagefind = {
search: () => Promise.resolve({ results: [] }),
options: () => Promise.resolve(),
};
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
console.log('Pagefind load error, event dispatched.');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadPagefind);
} else {
loadPagefind();
}
</script>}

View File

@@ -0,0 +1,110 @@
---
import type { CollectionEntry } from "astro:content";
import path from "node:path";
import { Icon } from "astro-icon/components";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getDir } from "../utils/url-utils";
import ImageWrapper from "./misc/ImageWrapper.astro";
import PostMetadata from "./PostMeta.astro";
interface Props {
class?: string;
entry: CollectionEntry<"posts">;
title: string;
url: string;
published: Date;
updated?: Date;
tags: string[];
category: string | null;
image: string;
description: string;
draft: boolean;
style: string;
}
const {
entry,
title,
url,
published,
updated,
tags,
category,
image,
description,
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-[2rem] text-[var(--primary)] md:hidden translate-y-0.5 absolute" name="material-symbols:chevron-right-rounded" ></Icon>
<Icon class="text-[var(--primary)] text-[2rem] 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"></Icon>
</a>
<!-- metadata -->
<PostMetadata published={published} updated={updated} tags={tags} category={category} hideTagsForMobile={true} hideUpdateDate={true} class="mb-4"></PostMetadata>
<!-- description -->
<div class:list={["transition text-75 mb-3.5 pr-4", {"line-clamp-2 md:line-clamp-1": !description}]}>
{ description || remarkPluginFrontmatter.excerpt }
</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(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)}
</div>
<div>|</div>
<div>
{remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : 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 define:vars={{coverWidth}}>
</style>

View File

@@ -0,0 +1,82 @@
---
import { Icon } from "astro-icon/components";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { formatDateToYYYYMMDD } from "../utils/date-utils";
import { getCategoryUrl, getTagUrl } from "../utils/url-utils";
interface Props {
class: string;
published: Date;
updated?: Date;
tags: string[];
category: string | null;
hideTagsForMobile?: boolean;
hideUpdateDate?: boolean;
}
const {
published,
updated,
tags,
category,
hideTagsForMobile = false,
hideUpdateDate = false,
} = 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>
<!-- update date -->
{!hideUpdateDate && updated && updated.getTime() !== published.getTime() && (
<div class="flex items-center">
<div class="meta-icon"
>
<Icon name="material-symbols:edit-calendar-outline-rounded" class="text-xl"></Icon>
</div>
<span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(updated)}</span>
</div>
)}
<!-- categories -->
<div class="flex items-center">
<div class="meta-icon"
>
<Icon name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row flex-nowrap items-center">
<a href={getCategoryUrl(category)} 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={getTagUrl(tag)} aria-label={`View all posts with the ${tag.trim()} tag`}
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{tag.trim()}
</a>
))}
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
</div>
</div>
</div>

View File

@@ -0,0 +1,28 @@
---
import type { CollectionEntry } from "astro:content";
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: CollectionEntry<"posts">) => (
<PostCard
entry={entry}
title={entry.data.title}
tags={entry.data.tags}
category={entry.data.category}
published={entry.data.published}
updated={entry.data.updated}
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,198 @@
<script lang="ts">
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import Icon from "@iconify/svelte";
import { url } from "@utils/url-utils.ts";
import { onMount } from "svelte";
import type { SearchResult } from "@/global";
let keywordDesktop = "";
let keywordMobile = "";
let result: SearchResult[] = [];
let isSearching = false;
let pagefindLoaded = false;
let initialized = false;
const fakeResult: SearchResult[] = [
{
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.",
},
];
const togglePanel = () => {
const panel = document.getElementById("search-panel");
panel?.classList.toggle("float-panel-closed");
};
const setPanelVisibility = (show: boolean, isDesktop: boolean): void => {
const panel = document.getElementById("search-panel");
if (!panel || !isDesktop) return;
if (show) {
panel.classList.remove("float-panel-closed");
} else {
panel.classList.add("float-panel-closed");
}
};
const search = async (keyword: string, isDesktop: boolean): Promise<void> => {
if (!keyword) {
setPanelVisibility(false, isDesktop);
result = [];
return;
}
if (!initialized) {
return;
}
isSearching = true;
try {
let searchResults: SearchResult[] = [];
if (import.meta.env.PROD && pagefindLoaded && window.pagefind) {
const response = await window.pagefind.search(keyword);
searchResults = await Promise.all(
response.results.map((item) => item.data()),
);
} else if (import.meta.env.DEV) {
searchResults = fakeResult;
} else {
searchResults = [];
console.error("Pagefind is not available in production environment.");
}
result = searchResults;
setPanelVisibility(result.length > 0, isDesktop);
} catch (error) {
console.error("Search error:", error);
result = [];
setPanelVisibility(false, isDesktop);
} finally {
isSearching = false;
}
};
onMount(() => {
const initializeSearch = () => {
initialized = true;
pagefindLoaded =
typeof window !== "undefined" &&
!!window.pagefind &&
typeof window.pagefind.search === "function";
console.log("Pagefind status on init:", pagefindLoaded);
if (keywordDesktop) search(keywordDesktop, true);
if (keywordMobile) search(keywordMobile, false);
};
if (import.meta.env.DEV) {
console.log(
"Pagefind is not available in development mode. Using mock data.",
);
initializeSearch();
} else {
document.addEventListener("pagefindready", () => {
console.log("Pagefind ready event received.");
initializeSearch();
});
document.addEventListener("pagefindloaderror", () => {
console.warn(
"Pagefind load error event received. Search functionality will be limited.",
);
initializeSearch(); // Initialize with pagefindLoaded as false
});
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs
setTimeout(() => {
if (!initialized) {
console.log("Fallback: Initializing search after timeout.");
initializeSearch();
}
}, 2000); // Adjust timeout as needed
}
});
$: if (initialized && keywordDesktop) {
(async () => {
await search(keywordDesktop, true);
})();
}
$: if (initialized && keywordMobile) {
(async () => {
await 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
">
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<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">
<Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon>
</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
">
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<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}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-[var(--primary)]"></Icon>
</div>
<div class="transition text-sm text-50">
{@html item.excerpt}
</div>
</a>
{/each}
</div>
<style>
input:focus {
outline: 0;
}
.search-panel {
max-height: calc(100vh - 100px);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,49 @@
---
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
pointer-events: none
.back-to-top-btn
color: var(--primary)
font-size: 2.25rem
font-weight: bold
border: none
position: fixed
bottom: 10rem
opacity: 1
cursor: pointer
transform: translateX(5rem)
pointer-events: auto
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 is:inline>
function backToTop() {
window.scroll({ top: 0, behavior: 'smooth' });
}
</script>

View File

@@ -0,0 +1,43 @@
---
interface Props {
badge?: string;
url?: string;
label?: string;
}
const { badge, url, label } = Astro.props;
---
<a href={url} aria-label={label}>
<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 px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
text-[var(--btn-content)] dark:text-[var(--deep-text)]
bg-[var(--btn-regular-bg)] 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 { 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,84 @@
---
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;
let 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={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" class="text-[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={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" class="text-[1.75rem]"></Icon>
</a>
</div>

View File

@@ -0,0 +1,54 @@
---
import path from "node: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
// biome-ignore lint/suspicious/noImplicitAnyLet: <check later>
let img;
if (isLocal) {
const files = import.meta.glob<ImageMetadata>("../../**", {
import: "default",
});
let normalizedPath = path
.normalize(path.join("../../", basePath, src))
.replace(/\\/g, "/");
const file = files[normalizedPath];
if (!file) {
console.error(
`\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`,
);
}
img = await file();
}
const imageClass = "w-full h-full object-cover";
const imageStyle = `object-position: ${position}`;
---
<div id={id} class:list={[className, 'overflow-hidden relative']}>
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
{isLocal && img && <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,43 @@
---
import { Icon } from "astro-icon/components";
import { licenseConfig, profileConfig } from "../../config";
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { formatDateToYYYYMMDD } from "../../utils/date-utils";
interface Props {
title: string;
slug: string;
pubDate: Date;
class: string;
}
const { title, 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 line-clamp-2">{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 line-clamp-2">{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)] line-clamp-2">{licenseConf.name}</a>
</div>
</div>
<Icon name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon>
</div>

View File

@@ -0,0 +1,43 @@
---
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>
document.addEventListener("click", function (e: MouseEvent) {
const target = e.target as Element | null;
if (target && target.classList.contains("copy-btn")) {
const preEle = target.closest("pre");
const codeEle = preEle?.querySelector("code");
const code = Array.from(codeEle?.querySelectorAll(".code:not(summary *)") ?? [])
.map(el => el.textContent)
.map(t => t === "\n" ? "" : t)
.join("\n");
navigator.clipboard.writeText(code);
const timeoutId = target.getAttribute("data-timeout-id");
if (timeoutId) {
clearTimeout(parseInt(timeoutId));
}
target.classList.add("success");
// 设置新的timeout并保存ID到按钮的自定义属性中
const newTimeoutId = setTimeout(() => {
target.classList.remove("success");
}, 1000);
target.setAttribute("data-timeout-id", newTimeoutId.toString());
}
});
</script>

View File

@@ -0,0 +1,35 @@
---
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { getCategoryList } from "../../utils/content-utils";
import ButtonLink from "../control/ButtonLink.astro";
import WidgetLayout from "./WidgetLayout.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={c.url}
badge={String(c.count)}
label={`View all posts in the ${c.name.trim()} category`}
>
{c.name.trim()}
</ButtonLink>
)}
</WidgetLayout>

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import Icon from "@iconify/svelte";
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 will-change-transform"
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}>
<div class="text-[var(--btn-content)]">
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
</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 { Icon } from "astro-icon/components";
import { type NavBarLink } from "../../types/config";
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-[1.25rem] text-[var(--primary)]"
>
</Icon>}
{link.external && <Icon name="fa6-solid:arrow-up-right-from-square"
class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1"
>
</Icon>}
</a>
))}
</div>

View File

@@ -0,0 +1,39 @@
---
import { Icon } from "astro-icon/components";
import { profileConfig } from "../../config";
import { url } from "../../utils/url-utils";
import ImageWrapper from "../misc/ImageWrapper.astro";
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-[12rem] 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 flex-wrap 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} class="text-[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} class="text-[1.5rem]"></Icon>
{config.links[0].name}
</a>}
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
---
import type { MarkdownHeading } from "astro";
import Categories from "./Categories.astro";
import Profile from "./Profile.astro";
import Tag from "./Tags.astro";
interface Props {
class?: string;
headings?: MarkdownHeading[];
}
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 id="sidebar-sticky" class="transition-all duration-700 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,268 @@
---
import type { MarkdownHeading } from "astro";
import { siteConfig } from "../../config";
import { url } from "../../utils/url-utils";
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 isPostsRoute = Astro.url.pathname.startsWith(url("/posts/"));
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;
---
{isPostsRoute &&
<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" style="opacity: 0" 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 = -1;
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--;
}
if (min > max) {
this.activeIndicator?.setAttribute("style", `opacity: 0`);
} else {
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.debug('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);
};
}
if (!customElements.get("table-of-contents")) {
customElements.define("table-of-contents", TableOfContents);
}
</script>

View File

@@ -0,0 +1,31 @@
---
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { getTagList } from "../../utils/content-utils";
import { getTagUrl } from "../../utils/url-utils";
import ButtonTag from "../control/ButtonTag.astro";
import WidgetLayout from "./WidgetLayout.astro";
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={getTagUrl(t.name)} label={`View all posts with the ${t.name.trim()} tag`}>
{t.name.trim()}
</ButtonTag>
))}
</div>
</WidgetLayout>

View File

@@ -0,0 +1,60 @@
---
import { Icon } from "astro-icon/components";
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
interface Props {
id: string;
name?: string;
isCollapsed?: boolean;
collapsedHeight?: string;
class?: string;
style?: string;
}
const { id, name, isCollapsed, collapsedHeight, style } = Astro.props;
const className = Astro.props.class;
---
<widget-layout data-id={id} data-is-collapsed={String(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" class="text-[1.75rem]"></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 !== "true")
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');
})
}
}
if (!customElements.get("widget-layout")) {
customElements.define("widget-layout", WidgetLayout);
}
</script>

90
src/config.ts Normal file
View File

@@ -0,0 +1,90 @@
import type {
ExpressiveCodeConfig,
LicenseConfig,
NavBarConfig,
ProfileConfig,
SiteConfig,
} from "./types/config";
import { LinkPreset } from "./types/config";
export const siteConfig: SiteConfig = {
title: "Fuwari",
subtitle: "Demo Site",
lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
themeColor: {
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
fixed: false, // 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, only supports 'top', 'center', 'bottom'. 'center' by default
credit: {
enable: false, // Display the credit text of the banner image
text: "", // Credit text to be displayed
url: "", // (Optional) URL link to the original artwork or artist's page
},
},
toc: {
enable: true, // Display the table of contents on the right side of the post
depth: 2, // Maximum heading depth to show in the table, from 1 to 3
},
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/saicaca/fuwari", // 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/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
name: "Lorem Ipsum",
bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
links: [
{
name: "Twitter",
icon: "fa6-brands:twitter", // 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://twitter.com",
},
{
name: "Steam",
icon: "fa6-brands:steam",
url: "https://store.steampowered.com",
},
{
name: "GitHub",
icon: "fa6-brands:github",
url: "https://github.com/saicaca/fuwari",
},
],
};
export const licenseConfig: LicenseConfig = {
enable: true,
name: "CC BY-NC-SA 4.0",
url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
};
export const expressiveCodeConfig: ExpressiveCodeConfig = {
// Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.
// Please select a dark theme, as this blog theme currently only supports dark background color
theme: "github-dark",
};

View File

@@ -0,0 +1,17 @@
export const PAGE_SIZE = 8;
export const LIGHT_MODE = "light",
DARK_MODE = "dark",
AUTO_MODE = "auto";
export const DEFAULT_THEME = AUTO_MODE;
// Banner height unit: vh
export const BANNER_HEIGHT = 35;
export const BANNER_HEIGHT_EXTEND = 30;
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND;
// The height the main panel overlaps the banner, unit: rem
export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5;
// Page width: rem
export const PAGE_WIDTH = 75;

44
src/constants/icon.ts Normal file
View File

@@ -0,0 +1,44 @@
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 I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { LinkPreset, type NavBarLink } from "@/types/config";
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/",
},
};

44
src/content/.obsidian/app.json vendored Normal file
View File

@@ -0,0 +1,44 @@
{
"showInlineTitle": false,
"promptDelete": true,
"showIndentGuide": false,
"attachmentFolderPath": "./attachments",
"userIgnoreFilters": null,
"focusNewTab": false,
"mobilePullAction": "command-palette:open",
"trashOption": "system",
"alwaysUpdateLinks": true,
"useMarkdownLinks": true,
"mobileToolbarCommands": [
"editor:undo",
"editor:redo",
"editor:insert-wikilink",
"editor:insert-embed",
"editor:insert-tag",
"editor:attach-file",
"editor:set-heading",
"editor:toggle-bold",
"editor:toggle-italics",
"editor:toggle-strikethrough",
"editor:toggle-highlight",
"editor:toggle-code",
"editor:toggle-blockquote",
"editor:insert-link",
"editor:toggle-bullet-list",
"editor:toggle-numbered-list",
"editor:toggle-checklist-status",
"editor:indent-list",
"editor:unindent-list",
"astro-composer:standardize-properties",
"astro-composer:convert-wikilinks-astro",
"seo:seo-run-current",
"editor:configure-toolbar"
],
"newFileLocation": "root",
"newFileFolderPath": "",
"newLinkFormat": "relative",
"mobileQuickRibbonItem": "",
"vimMode": false,
"readableLineLength": true,
"defaultViewMode": "source"
}

10
src/content/.obsidian/appearance.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"cssTheme": "Oxygen",
"showViewHeader": true,
"showRibbon": false,
"enabledCssSnippets": [
"astro-modular-styling"
],
"theme": "system",
"accentColor": ""
}

3
src/content/.obsidian/backlink.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"backlinkInDocument": true
}

10
src/content/.obsidian/bookmarks.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"items": [
{
"type": "file",
"ctime": 1755331588238,
"path": "_formatting-reference.md",
"title": "Formatting Reference"
}
]
}

View File

@@ -0,0 +1,22 @@
[
"seo",
"property-over-file-name",
"oxygen-settings",
"vault-cms",
"astro-composer",
"settings-search",
"editing-toolbar",
"image-manager",
"bases-cms",
"home-base",
"obsidian-git",
"zenmode",
"tag-wrangler",
"explorer-focus",
"ui-tweaker",
"alias-file-name-history",
"obsidian42-brat",
"omnisearch",
"data-files-editor",
"file-name-history"
]

33
src/content/.obsidian/core-plugins.json vendored Normal file
View File

@@ -0,0 +1,33 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": false,
"backlink": false,
"canvas": false,
"outgoing-link": false,
"tag-pane": true,
"footnotes": false,
"properties": false,
"page-preview": false,
"daily-notes": false,
"templates": false,
"note-composer": false,
"command-palette": true,
"slash-command": true,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": false,
"bases": true,
"webviewer": false
}

30
src/content/.obsidian/graph.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"collapse-filter": true,
"search": "path:posts/",
"showTags": false,
"showAttachments": false,
"hideUnresolved": false,
"showOrphans": true,
"collapse-color-groups": true,
"colorGroups": [
{
"query": "",
"color": {
"a": 1,
"rgb": 14701138
}
}
],
"collapse-display": true,
"showArrow": false,
"textFadeMultiplier": 0,
"nodeSizeMultiplier": 1,
"lineSizeMultiplier": 1,
"collapse-forces": true,
"centerStrength": 0.518713248970312,
"repelStrength": 10,
"linkStrength": 1,
"linkDistance": 250,
"scale": 1.0147822288211785,
"close": true
}

274
src/content/.obsidian/hotkeys.json vendored Normal file
View File

@@ -0,0 +1,274 @@
{
"app:go-back": [
{
"modifiers": [
"Mod",
"Alt"
],
"key": "ArrowLeft"
},
{
"modifiers": [
"Alt"
],
"key": "ArrowLeft"
}
],
"app:go-forward": [
{
"modifiers": [
"Mod",
"Alt"
],
"key": "ArrowRight"
},
{
"modifiers": [
"Alt"
],
"key": "ArrowRight"
}
],
"homepage:open-homepage": [
{
"modifiers": [
"Mod"
],
"key": "M"
}
],
"app:toggle-left-sidebar": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "Z"
}
],
"app:toggle-right-sidebar": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "X"
}
],
"obsidian-git:push": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "S"
}
],
"insert-unsplash-image:insert": [
{
"modifiers": [
"Mod"
],
"key": "'"
}
],
"custom-save:save": [],
"editor:insert-callout": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "C"
}
],
"astro-composer:rename-note": [
{
"modifiers": [
"Mod"
],
"key": "R"
}
],
"astro-composer:rename-content": [
{
"modifiers": [
"Mod"
],
"key": "R"
}
],
"editor:toggle-fold-properties": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "P"
}
],
"seo:seo-open-global": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "A"
}
],
"insert-unsplash-image:insert-in-frontmatter": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "'"
}
],
"astro-modular-settings:open-settings": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": ","
}
],
"seo:open-global": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "A"
}
],
"oxygen-settings:toggle-minimal-focus-mode": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "F"
}
],
"zenmode:exit-zen-mode": [],
"oxygen-settings:toggle-zen-mode": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "Z"
}
],
"oxygen-settings:toggle-tab-containers": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "S"
}
],
"app:toggle-ribbon": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "A"
}
],
"zenmode:toggle-zen-mode": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "Z"
}
],
"app:reload": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "R"
}
],
"theme:toggle-light-dark": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "M"
}
],
"astro-composer:open-project-terminal": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "D"
}
],
"editing-toolbar:workplace-fullscreen-focus": [],
"editing-toolbar:fullscreen-focus": [],
"editing-toolbar:hide-show-menu": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "E"
}
],
"astro-composer:edit-astro-config": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": ","
}
],
"image-manager:search-image": [
{
"modifiers": [
"Mod"
],
"key": "'"
}
],
"image-manager:insert-remote-image-to-property": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "'"
}
],
"ui-tweaker:toggle-tab-bar": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "S"
}
],
"omnisearch:show-modal": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "O"
}
]
}

View File

@@ -0,0 +1,20 @@
{
"ignoreRegexes": [
"^_",
"^Untitled$",
"^Untitled \\d+$"
],
"timeoutSeconds": 5,
"caseSensitive": false,
"autoCreateFrontmatter": true,
"includeFolders": [],
"excludeFolders": [],
"fileExtensions": [
"md"
],
"trackFolderRenames": "index",
"excludePropertyName": "",
"propertyName": "",
"propertyValue": true,
"propertyExcludes": false
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"id":"alias-file-name-history","name":"Alias File Name History","version":"0.2.4","minAppVersion":"0.15.0","description":"Store file name history into the aliases property of your notes.","author":"David V. Kimball","authorUrl":"https://davidvkimball.com","fundingUrl":"https://patreon.com/davidvkimball","isDesktopOnly":false}

View File

@@ -0,0 +1,11 @@
/* Group settings compatibility styling for older Obsidian builds (< 1.11.0) */
/* Scoped to only this plugin's settings container to avoid affecting other plugins */
.alias-file-name-history-settings-compat .setting-group-heading h3 {
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
font-size: 1rem;
font-weight: 600;
border-bottom: none !important;
}

View File

@@ -0,0 +1,56 @@
{
"defaultTemplate": "---\ntitle: \"{{title}}\"\ndate: {{date}}\ndescription: \"\"\ntags: []\nimage: \"\"\nimageAlt: \"\"\nimageOG: false\nhideCoverImage: false\nhideTOC: false\ntargetKeyword: \"\"\ndraft: true\n---",
"autoInsertProperties": true,
"dateFormat": "YYYY-MM-DD",
"enableCopyHeadingLink": true,
"copyHeadingLinkFormat": "astro",
"addTrailingSlashToLinks": true,
"enableOpenTerminalCommand": true,
"terminalProjectRootPath": "/home/fzzin/Programming/TS/homesite/homepage",
"terminalApplicationName": "",
"enableTerminalDebugLogging": false,
"enableTerminalRibbonIcon": true,
"enableOpenConfigFileCommand": true,
"configFilePath": "/home/fzzin/Programming/TS/homesite/homepage/src/config.ts",
"enableConfigRibbonIcon": true,
"contentTypes": [
{
"id": "content-type-1772371679330-qy41e2hsz",
"name": "Posts",
"folder": "posts",
"linkBasePath": "/posts/",
"template": "---\ntitle: \"{{title}}\"\nupdated: {{date}}\npublished: \"\"\ntags: []\ncategory: \"\"\ndraft: true\ndescription: \"\"\nimage: \"\"\n---\n",
"enabled": true,
"creationMode": "folder",
"indexFileName": "index",
"ignoreSubfolders": false,
"enableUnderscorePrefix": false
},
{
"id": "content-type-1772371679330-0e6t69ph9",
"name": "Pages",
"folder": "spec",
"linkBasePath": "/",
"template": "---\ntitle:{{title}}\npublished: \"\"\ntags:[]\ncategory: \"\"\ndraft: TRUE\n---\n",
"enabled": true,
"creationMode": "file",
"indexFileName": "index",
"ignoreSubfolders": false,
"enableUnderscorePrefix": false
}
],
"migrationCompleted": true,
"helpButtonReplacement": {
"enabled": true,
"commandId": "astro-composer:edit-astro-config",
"iconId": "rocket"
},
"showMdxFilesInExplorer": false,
"processBackgroundFileChanges": true,
"syncDraftDate": false,
"draftProperty": "",
"draftLogic": "true-is-draft",
"publishDateField": "",
"updateModifiedDate": false,
"modifiedDateField": ""
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"id":"astro-composer","name":"Astro Composer","version":"0.11.4","minAppVersion":"0.15.0","description":"Turn your notes into posts and pages for your Astro blog with automated content management features.","author":"David V. Kimball","authorUrl":"https://davidvkimball.com","fundingUrl":"https://patreon.com/davidvkimball","isDesktopOnly":false}

View File

@@ -0,0 +1,380 @@
.astro-composer-title-input {
width: 100%;
margin-bottom: 16px;
padding: 8px;
}
.astro-composer-button-container {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
}
/* Ensure all buttons in the button container use default cursor */
.astro-composer-button-container button {
cursor: default !important;
}
.astro-composer-button-container button:hover {
cursor: default !important;
}
.astro-composer-cancel-button,
.astro-composer-create-button {
padding: 6px 12px;
border-radius: 4px;
cursor: default !important;
}
.astro-composer-cancel-button:hover,
.astro-composer-create-button:hover {
cursor: default !important;
}
.astro-composer-create-button.mod-cta {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
cursor: default !important;
}
.astro-composer-create-button.mod-cta:hover {
background-color: var(--interactive-accent-hover);
cursor: default !important;
}
.astro-composer-template-textarea {
height: 300px; /* Adjusted for 5-10 lines of properties */
width: 100%;
padding: 8px;
resize: vertical; /* Allow vertical resizing */
margin-bottom: 16px;
}
.astro-composer-modal {
padding: 20px;
max-width: 500px;
margin: 0 auto;
}
.astro-composer-modal h2 {
margin-top: 0;
}
/* Custom Content Types Styling */
.custom-content-types-container {
margin-top: 0;
}
.custom-content-type-item {
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
background-color: var(--background-secondary);
}
.custom-content-type-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-content-type-header .setting-item {
margin-bottom: 0;
}
.custom-content-type-header .setting-item-name {
font-weight: 600;
color: var(--text-normal);
}
.custom-content-type-item .setting-item {
margin-bottom: 12px;
}
.custom-content-type-item .setting-item:last-child {
margin-bottom: 0;
}
/* Add spacing between settings in custom content types since we removed dividers */
.custom-content-type-settings > div {
margin-bottom: 16px;
}
.custom-content-type-settings > div:last-child {
margin-bottom: 0;
}
.custom-content-type-settings {
transition: all 0.2s ease-in-out;
padding-top: 12px;
}
.custom-content-type-settings[style*="none"] {
opacity: 0;
transform: translateY(-10px);
}
/* Mobile-specific improvements for Astro Composer modal */
@media (max-width: 768px) {
/* Force mobile positioning for all modals containing our content */
.modal:has(.astro-composer-title-input) {
position: fixed !important;
top: 10% !important;
left: 50% !important;
transform: translateX(-50%) !important;
max-height: 50vh !important;
overflow-y: auto !important;
width: 90vw !important;
max-width: 500px !important;
}
/* Only target our specific modal content, not all modals */
.modal .astro-composer-title-input {
font-size: 16px; /* Prevents zoom on iOS */
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
}
/* Ensure our modal content is properly sized on mobile */
.modal .astro-composer-button-container {
flex-direction: column;
gap: 12px;
}
.modal .astro-composer-cancel-button,
.modal .astro-composer-create-button {
width: 100%;
padding: 12px;
font-size: 16px;
cursor: default !important;
}
.modal .astro-composer-cancel-button:hover,
.modal .astro-composer-create-button:hover {
cursor: default !important;
}
}
/* Fallback for browsers that don't support :has() */
.astro-composer-mobile-modal {
position: fixed !important;
top: 10% !important;
left: 50% !important;
transform: translateX(-50%) !important;
max-height: 50vh !important;
overflow-y: auto !important;
width: 90vw !important;
max-width: 500px !important;
}
/* Settings UI utility classes */
.astro-composer-setting-container-visible {
display: block;
}
.astro-composer-setting-container-hidden {
display: none;
}
.astro-composer-custom-type-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.astro-composer-header-name {
flex: 1;
min-width: 0;
}
/* Collapse button */
.astro-composer-collapse-button {
background: transparent !important;
border: none !important;
padding: 4px;
margin-right: 4px;
cursor: default;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
box-shadow: none !important;
}
.astro-composer-collapse-button:hover {
background-color: var(--background-modifier-hover);
color: var(--text-normal);
}
.astro-composer-collapse-button:active {
background-color: var(--background-modifier-active);
}
.astro-composer-collapse-button svg {
transition: transform 0.2s ease;
}
.astro-composer-collapse-button.is-collapsed svg {
transform: rotate(-90deg);
}
/* Reorder buttons container */
.astro-composer-reorder-buttons {
display: flex;
flex-direction: row;
gap: 4px;
margin-right: 8px;
align-items: center;
}
/* Reorder buttons - minimal icon-only style */
.astro-composer-reorder-button {
background: transparent !important;
border: none !important;
padding: 4px;
cursor: default;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
line-height: 1;
opacity: 0.6;
box-shadow: none !important;
}
.astro-composer-reorder-button:hover:not(:disabled) {
background-color: var(--background-modifier-hover);
color: var(--text-normal);
opacity: 1;
}
.astro-composer-reorder-button:active:not(:disabled) {
background-color: var(--background-modifier-active);
}
.astro-composer-reorder-button:disabled {
opacity: 0.2;
cursor: default;
}
.astro-composer-reorder-button svg {
width: 16px;
height: 16px;
}
.astro-composer-remove-setting {
border-top: none;
}
.astro-composer-add-button {
border-top: none;
}
/* Floating button container - no settings background, right-aligned */
.astro-composer-add-button-container {
display: flex;
justify-content: flex-end;
margin-top: 16px;
margin-bottom: 0;
padding: 0;
background: transparent;
border: none;
}
.astro-composer-add-button-container button {
margin: 0;
}
/* Settings tab - hidden setting elements */
/* Only hide the setting item info elements, not nested ones inside content types */
.astro-composer-setting-hidden-elements > .setting-item-info > .setting-item-name,
.astro-composer-setting-hidden-elements > .setting-item-info > .setting-item-description,
.astro-composer-setting-hidden-elements > .setting-item-control {
display: none;
}
.astro-composer-setting-hidden-elements {
border-top: none;
padding-top: 0;
padding-bottom: 0;
}
.astro-composer-setting-container-full-width {
display: block;
width: 100%;
}
.astro-composer-custom-types-container-visible {
display: block !important;
width: 100% !important;
visibility: visible !important;
}
/* Ensure custom content types container is always visible even when parent has hidden elements */
.astro-composer-setting-hidden-elements .custom-content-types-container {
display: block !important;
visibility: visible !important;
}
/* Ensure content type items inside the container are visible */
.custom-content-types-container .custom-content-type-item {
display: block !important;
visibility: visible !important;
}
/* Conflict warning styles */
.astro-composer-conflict-warning {
color: var(--text-warning);
font-size: 0.9em;
margin-top: 0.5em;
}
.astro-composer-conflict-warning.hidden {
display: none;
}
/* Help button replacement */
/* No special display needed - inherits from parent flex container */
/* Ribbon context menu hiding - these will be applied via classes */
.astro-composer-hide-terminal-icon .menu-item:has(svg[data-lucide="terminal-square"]),
.astro-composer-hide-terminal-icon .menu-item:has(.lucide-terminal-square),
.astro-composer-hide-terminal-icon .menu-item .menu-item-icon:has(svg[data-lucide="terminal-square"]),
.astro-composer-hide-terminal-icon .menu-item .menu-item-icon:has(.lucide-terminal-square) {
display: none !important;
}
.astro-composer-hide-config-icon .menu-item:has(svg[data-lucide="wrench"]),
.astro-composer-hide-config-icon .menu-item:has(svg[data-lucide="rocket"]),
.astro-composer-hide-config-icon .menu-item:has(.lucide-wrench),
.astro-composer-hide-config-icon .menu-item:has(.lucide-rocket),
.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(svg[data-lucide="wrench"]),
.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(svg[data-lucide="rocket"]),
.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(.lucide-wrench),
.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(.lucide-rocket) {
display: none !important;
}
/* Help button hiding */
.astro-composer-hide-help-button .workspace-drawer-vault-actions .clickable-icon:has(svg.help) {
display: none !important;
}
/* Group settings compatibility styling for older Obsidian builds (< 1.11.0) */
/* Scoped to only this plugin's settings container to avoid affecting other plugins */
.astro-composer-settings-compat .setting-group-heading h3 {
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
font-size: 1rem;
font-weight: 600;
border-bottom: none !important;
}

View File

@@ -0,0 +1,28 @@
{
"confirmBulkOperations": true,
"deleteParentFolder": true,
"deleteParentFolderFilename": "index",
"deleteUniqueAttachments": true,
"confirmDeletions": true,
"useHomeIcon": true,
"enableQuickEdit": true,
"quickEditCommand": "astro-composer:rename-content",
"quickEditCommandName": "Astro Composer: Rename current content",
"quickEditIcon": "lucide-pencil-line",
"quickEditOpenFile": false,
"showToolbarSelectAll": true,
"showToolbarClear": true,
"showToolbarDraft": true,
"showToolbarPublish": true,
"showToolbarTags": true,
"showToolbarSet": true,
"showToolbarRemove": true,
"showToolbarDelete": true,
"forceStaticGifImages": false,
"embeddedViewRefreshDebounceMs": 250,
"virtualScrollThreshold": 100,
"virtualScrollBuffer": 20,
"migrationBasesCmsToCmsDone": true,
"showPropertiesInfoModal": true,
"thumbnailCacheSize": "balanced"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"id":"bases-cms","name":"Bases CMS","version":"0.4.2","minAppVersion":"1.10.2","description":"Manage your notes in bases like a content management system.","author":"David V. Kimball","authorUrl":"https://davidvkimball.com","fundingUrl":"https://patreon.com/davidvkimball","isDesktopOnly":false}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
{
"doLoadTxt": false,
"doCreateTxt": false,
"doLoadXml": false,
"doCreateXml": false,
"doLoadJson": false,
"doCreateJson": false,
"doLoadYaml": false,
"doCreateYaml": false,
"doLoadAstro": false,
"doCreateAstro": false,
"doLoadTs": false,
"doCreateTs": false,
"doLoadCss": false,
"doCreateCss": false,
"doLoadHtml": false,
"doCreateHtml": false,
"doLoadJs": false,
"doCreateJs": false,
"doLoadMjs": false,
"doCreateMjs": false,
"doAutosaveFiles": true,
"lineWrapping": true
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
{
"id": "data-files-editor",
"name": "Data Files Editor",
"version": "1.4.1",
"minAppVersion": "0.15.0",
"description": "Plugin to edit data files like txt, xml, json, and yaml",
"author": "ZukTol",
"authorUrl": "https://github.com/ZukTol",
"fundingUrl": "",
"isDesktopOnly": false
}

View File

@@ -0,0 +1,23 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
.datafile-source-view.mod-cm6 .cm-gutters {
flex: 0 0 auto;
background-color: transparent;
color: var(--text-faint) !important;
border-right: none !important;
margin-inline-end: var(--file-folding-offset);
font-size: var(--font-ui-smaller);
z-index: 1;
font-variant: tabular-nums;
}
.cm-gutterElement.cm-activeLineGutter {
background-color: #aaeeff44;
}

View File

@@ -0,0 +1,169 @@
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => DisableTabsPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian3 = require("obsidian");
// src/settings.ts
var DEFAULT_SETTINGS = {
enabled: true,
hideMobileNewTabIcon: false
};
// src/utils/tab-enforcer.ts
var TabEnforcer = class {
constructor(app) {
this.app = app;
}
enforceSingleTab() {
const leaves = [];
this.app.workspace.iterateRootLeaves((leaf) => {
leaves.push(leaf);
});
if (leaves.length > 1) {
let active = leaves[leaves.length - 1];
for (const leaf of leaves) {
const view = leaf.view;
if (view && "containerEl" in view) {
const containerEl = view.containerEl;
if (containerEl && containerEl.hasClass("is-active")) {
active = leaf;
break;
}
}
}
leaves.forEach((leaf) => {
if (leaf !== active) {
leaf.detach();
}
});
}
}
};
// src/ui/settings-tab.ts
var import_obsidian2 = require("obsidian");
// src/utils/settings-compat.ts
var import_obsidian = require("obsidian");
var ObsidianModule = __toESM(require("obsidian"), 1);
function createSettingsGroup(containerEl, heading, manifestId) {
if ((0, import_obsidian.requireApiVersion)("1.11.0")) {
const SettingGroupClass = ObsidianModule.SettingGroup;
if (SettingGroupClass) {
const group = heading ? new SettingGroupClass(containerEl).setHeading(heading) : new SettingGroupClass(containerEl);
return {
addSetting(cb) {
group.addSetting(cb);
}
};
}
}
if (manifestId) {
containerEl.addClass(`${manifestId}-settings-compat`);
}
{
if (heading) {
const headingEl = containerEl.createDiv("setting-group-heading");
headingEl.createEl("h3", { text: heading });
}
return {
addSetting(cb) {
const setting = new import_obsidian.Setting(containerEl);
cb(setting);
}
};
}
}
// src/ui/settings-tab.ts
var DisableTabsSettingTab = class extends import_obsidian2.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
this.settings = plugin.settings;
}
display() {
const { containerEl } = this;
containerEl.empty();
const generalGroup = createSettingsGroup(containerEl, void 0, "disable-tabs");
generalGroup.addSetting((setting) => {
setting.setName("Hide mobile tabs icon").setDesc("Hide the tabs icon on mobile devices").addToggle(
(toggle) => toggle.setValue(this.settings.hideMobileNewTabIcon).onChange(async (value) => {
this.settings.hideMobileNewTabIcon = value;
await this.plugin.saveSettings();
this.plugin.updateMobileTabIconCSS();
})
);
});
}
};
// src/main.ts
var DisableTabsPlugin = class extends import_obsidian3.Plugin {
async onload() {
const loadedData = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData);
this.tabEnforcer = new TabEnforcer(this.app);
this.updateMobileTabIconCSS();
this.registerEvent(this.app.workspace.on("layout-change", () => {
this.tabEnforcer.enforceSingleTab();
}));
this.app.workspace.onLayoutReady(() => {
this.tabEnforcer.enforceSingleTab();
});
this.addSettingTab(new DisableTabsSettingTab(this.app, this));
}
updateMobileTabIconCSS() {
if (this.settings.hideMobileNewTabIcon) {
document.body.classList.add("disable-tabs-hide-mobile-icon");
} else {
document.body.classList.remove("disable-tabs-hide-mobile-icon");
}
}
async saveSettings() {
await this.saveData(this.settings);
}
onunload() {
document.body.classList.remove("disable-tabs-hide-mobile-icon");
}
};
/* nosourcemap */

View File

@@ -0,0 +1,11 @@
{
"id": "disable-tabs",
"name": "Disable Tabs",
"version": "1.0.9",
"minAppVersion": "0.15.0",
"description": "Disables having more than one tab open at a time.",
"author": "David V. Kimball",
"authorUrl": "https://davidvkimball.com",
"fundingUrl": "https://patreon.com/davidvkimball",
"isDesktopOnly": false
}

View File

@@ -0,0 +1,16 @@
/* Hide mobile new tab icon when the plugin setting is enabled */
body.disable-tabs-hide-mobile-icon .mobile-navbar-action-tabs {
display: none;
}
/* Group settings compatibility styling for older Obsidian builds (< 1.11.0) */
/* Scoped to only this plugin's settings container to avoid affecting other plugins */
.disable-tabs-settings-compat .setting-group-heading h3 {
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
font-size: 1rem;
font-weight: 600;
border-bottom: none !important;
}

View File

@@ -0,0 +1,441 @@
{
"lastVersion": "3.2.7",
"aestheticStyle": "glass",
"positionStyle": "top",
"menuCommands": [
{
"id": "editing-toolbar:editor-undo",
"name": "Undo editor",
"icon": "undo-glyph"
},
{
"id": "editing-toolbar:editor-redo",
"name": "Redo editor",
"icon": "redo-glyph"
},
{
"id": "SubmenuCommands-header",
"name": "submenu",
"icon": "heading-glyph",
"SubmenuCommands": [
{
"id": "editing-toolbar:header2-text",
"name": "Header 2",
"icon": "header-2"
},
{
"id": "editing-toolbar:header3-text",
"name": "Header 3",
"icon": "header-3"
},
{
"id": "editing-toolbar:header4-text",
"name": "Header 4",
"icon": "header-4"
},
{
"id": "editing-toolbar:header5-text",
"name": "Header 5",
"icon": "header-5"
},
{
"id": "editing-toolbar:header6-text",
"name": "Header 6",
"icon": "header-6"
}
]
},
{
"id": "editing-toolbar:toggle-bold",
"name": "Bold",
"icon": "bold-glyph"
},
{
"id": "editing-toolbar:toggle-italics",
"name": "Italics",
"icon": "italic-glyph"
},
{
"id": "editing-toolbar:toggle-strikethrough",
"name": "Strikethrough",
"icon": "strikethrough-glyph"
},
{
"id": "editing-toolbar:underline",
"name": "Underline",
"icon": "underline-glyph"
},
{
"id": "editing-toolbar:toggle-highlight",
"name": "==Highlight==",
"icon": "highlight-glyph"
},
{
"id": "editing-toolbar:toggle-format-brush",
"name": "Format Painter",
"icon": "paintbrush"
},
{
"id": "editing-toolbar:format-eraser",
"name": "Clear text formatting",
"icon": "eraser"
},
{
"id": "editing-toolbar:editor:swap-line-down",
"name": "Swap line down",
"icon": "lucide-corner-right-down"
},
{
"id": "SubmenuCommands-text-tools",
"name": "Text Tools",
"icon": "box",
"menuType": "dropdown",
"SubmenuCommands": [
{
"id": "editing-toolbar:get-plain-text",
"name": "Get Plain Text",
"icon": "lucide-file-text"
},
{
"id": "editing-toolbar:smart-symbols",
"name": "Full Half Converter",
"icon": "lucide-at-sign"
},
{
"id": "editingToolbar-Divider-Line",
"name": "Line Operations",
"icon": "vertical-split"
},
{
"id": "editing-toolbar:insert-blank-lines",
"name": "Insert Blank Lines",
"icon": "lucide-space"
},
{
"id": "editing-toolbar:remove-blank-lines",
"name": "Remove Blank Lines",
"icon": "lucide-minimize-2"
},
{
"id": "editing-toolbar:split-lines",
"name": "Split Lines",
"icon": "lucide-split"
},
{
"id": "editing-toolbar:merge-lines",
"name": "Merge Lines",
"icon": "lucide-merge"
},
{
"id": "editing-toolbar:dedupe-lines",
"name": "Dedupe Lines",
"icon": "lucide-filter"
},
{
"id": "editingToolbar-Divider-Line",
"name": "Text Processing",
"icon": "vertical-split"
},
{
"id": "editing-toolbar:add-wrap",
"name": "Add Prefix/Suffix",
"icon": "lucide-wrap-text"
},
{
"id": "editing-toolbar:number-lines",
"name": "Number Lines (Custom)",
"icon": "lucide-list-ordered"
},
{
"id": "editing-toolbar:remove-whitespace-trim",
"name": "Trim Line Ends",
"icon": "lucide-scissors"
},
{
"id": "editing-toolbar:remove-whitespace-compress",
"name": "Shrink Extra Spaces",
"icon": "lucide-minimize"
},
{
"id": "editing-toolbar:remove-whitespace-all",
"name": "Remove All Whitespace",
"icon": "lucide-eraser"
},
{
"id": "editingToolbar-Divider-Line",
"name": "Advanced Tools",
"icon": "vertical-split"
},
{
"id": "editing-toolbar:list-to-table",
"name": "List to Table",
"icon": "lucide-table"
},
{
"id": "editing-toolbar:table-to-list",
"name": "Table to List",
"icon": "lucide-list"
},
{
"id": "editing-toolbar:extract-between",
"name": "Extract Between Strings",
"icon": "lucide-brackets"
}
]
},
{
"id": "editing-toolbar:editor:swap-line-up",
"name": "Swap line up",
"icon": "lucide-corner-right-up"
},
{
"id": "editing-toolbar:editor:attach-file",
"name": "Attach file",
"icon": "lucide-paperclip"
},
{
"id": "editing-toolbar:editor:insert-table",
"name": "Insert Table",
"icon": "lucide-table"
},
{
"id": "editing-toolbar:editor:toggle-blockquote",
"name": "Blockquote",
"icon": "quote"
},
{
"id": "editing-toolbar:insert-callout",
"name": "Insert Callout ",
"icon": "alert-triangle"
},
{
"id": "SubmenuCommands-list",
"name": "submenu-list",
"icon": "bullet-list-glyph",
"SubmenuCommands": [
{
"id": "editing-toolbar:toggle-bullet-list",
"name": "Bullet list",
"icon": "<svg width=\"18\" height=\"18\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M860 424 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-477 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l477 0 ZM860 756 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-477 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l477 0 ZM860 92 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-477 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l477 0 ZM176 716 l0 0 ZM112 716 q0 -27 18.5 -45.5 q18.5 -18.5 45.5 -18.5 q27 0 45.5 18.5 q18.5 18.5 18.5 45.5 q0 27 -18.5 45.5 q-18.5 18.5 -45.5 18.5 q-27 0 -45.5 -18.5 q-18.5 -18.5 -18.5 -45.5 ZM176 384 l0 0 ZM112 384 q0 -27 18.5 -45.5 q18.5 -18.5 45.5 -18.5 q27 0 45.5 18.5 q18.5 18.5 18.5 45.5 q0 27 -18.5 45.5 q-18.5 18.5 -45.5 18.5 q-27 0 -45.5 -18.5 q-18.5 -18.5 -18.5 -45.5 ZM176 52 l0 0 ZM112 52 q0 -27 18.5 -45.5 q18.5 -18.5 45.5 -18.5 q27 0 45.5 18.5 q18.5 18.5 18.5 45.5 q0 27 -18.5 45.5 q-18.5 18.5 -45.5 18.5 q-27 0 -45.5 -18.5 q-18.5 -18.5 -18.5 -45.5 Z\"></path></g></svg>"
},
{
"id": "editing-toolbar:toggle-numbered-list",
"name": "Numbered list",
"icon": "<svg width=\"18\" height=\"18\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M860 424 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-457 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l457 0 ZM860 756 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-457 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l457 0 ZM860 92 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-457 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l457 0 ZM264 136 l-3 -3 l-51 -57 l56 0 q14 0 24.5 -10 q10.5 -10 11.5 -25 l0 -1 q0 -15 -10.5 -25.5 q-10.5 -10.5 -24.5 -10.5 l-137 0 q-15 0 -25 10 q-10 10 -11 24.5 q-1 14.5 9 25.5 l63 70 l49 54 q7 7 7 16.5 q0 9.5 -7.5 16.5 q-7.5 7 -18.5 7 q-11 0 -18.5 -6.5 q-7.5 -6.5 -8.5 -16.5 l0 0 q0 -15 -10.5 -25.5 q-10.5 -10.5 -25.5 -10.5 q-15 0 -25.5 10.5 q-10.5 10.5 -10.5 25.5 q0 26 13.5 47.5 q13.5 21.5 36 34.5 q22.5 13 49 13 q26.5 0 49.5 -13 q23 -13 36 -34.5 q13 -21.5 13 -47.5 q0 -20 -7.5 -37.5 q-7.5 -17.5 -21.5 -30.5 l-1 -1 ZM173 794 q11 11 25 10.5 q14 -0.5 24.5 -10.5 q10.5 -10 10.5 -25 l0 -293 q0 -15 -10 -25.5 q-10 -10.5 -25 -10.5 q-15 0 -25.5 10 q-10.5 10 -11.5 25 l0 211 q-10 -8 -23.5 -7 q-13.5 1 -22.5 11 l-1 0 q-10 11 -9.5 25.5 q0.5 14.5 10.5 24.5 l58 54 Z\"></path></g></svg>"
},
{
"id": "editing-toolbar:editor:toggle-checklist-status",
"name": "Checklist",
"icon": "checkbox-glyph"
},
{
"id": "editing-toolbar:undent-list",
"name": "Unindent-list",
"icon": "<svg width=\"18\" height=\"18\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M872 302 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-429 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l429 0 ZM872 542 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-429 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l429 0 ZM872 784 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-721 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l721 0 ZM872 62 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-721 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l721 0 ZM244 534 l-123 -122 q-8 -7 -8 -18 q0 -11 8 -18 l123 -122 q8 -7 19 -7 q11 0 18.5 7.5 q7.5 7.5 7.5 18.5 l0 242 q0 11 -7.5 18.5 q-7.5 7.5 -18.5 7.5 q-11 0 -19 -7 Z\"></path></g></svg>"
},
{
"id": "editing-toolbar:indent-list",
"name": "Indent list",
"icon": "<svg width=\"18\" height=\"18\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M872 302 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-429 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l429 0 ZM872 542 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-429 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l429 0 ZM872 784 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-721 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l721 0 ZM872 62 q17 0 28.5 -11.5 q11.5 -11.5 11.5 -28 q0 -16.5 -11.5 -28.5 q-11.5 -12 -27.5 -12 l-721 0 q-17 0 -28.5 11.5 q-11.5 11.5 -11.5 28 q0 16.5 11.5 28.5 q11.5 12 27.5 12 l721 0 ZM158 534 l124 -122 q7 -7 7 -18 q0 -11 -7 -18 l-124 -122 q-7 -7 -18 -7 q-11 0 -19 7.5 q-8 7.5 -8 18.5 l0 242 q0 11 8 18.5 q8 7.5 19 7.5 q11 0 18 -7 Z\"></path></g></svg>"
},
{
"id": "editing-toolbar:renumber-ordered-list",
"name": "Reorder numbered list",
"icon": "list-restart"
}
]
},
{
"id": "SubmenuCommands-mdcmder",
"name": "submenu",
"icon": "<svg width=\"18\" height=\"18\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M464 608 l0 -568 q0 -3 -2.5 -5.5 q-2.5 -2.5 -5.5 -2.5 l-80 0 q-3 0 -5.5 2.5 q-2.5 2.5 -2.5 5.5 l0 568 l-232 0 q-3 0 -5.5 2.5 q-2.5 2.5 -2.5 5.5 l0 80 q0 3 2.5 5.5 q2.5 2.5 5.5 2.5 l560 0 q3 0 5.5 -2.5 q2.5 -2.5 2.5 -5.5 l0 -80 q0 -3 -2.5 -5.5 q-2.5 -2.5 -5.5 -2.5 l-232 0 ZM864 696 q17 0 28.5 11.5 q11.5 11.5 11.5 28.5 q0 17 -11.5 28.5 q-11.5 11.5 -28.5 11.5 q-17 0 -28.5 -11.5 q-11.5 -11.5 -11.5 -28.5 q0 -17 11.5 -28.5 q11.5 -11.5 28.5 -11.5 ZM864 640 q-40 0 -68 28 q-28 28 -28 68 q0 40 28 68 q28 28 68 28 q40 0 68 -28 q28 -28 28 -68 q0 -40 -28 -68 q-28 -28 -68 -28 ZM576 322 l0 -63 q0 -3 2 -5 l89 -70 l-89 -70 q-2 -2 -2 -5 l0 -63 q0 -4 3.5 -5.5 q3.5 -1.5 6.5 0.5 l170 133 q4 3 4.5 8.5 q0.5 5.5 -2.5 9.5 l-2 2 l-170 133 q-3 2 -6.5 0.5 q-3.5 -1.5 -3.5 -5.5 ZM256 322 l0 -63 q0 -3 -2 -5 l-89 -70 l89 -70 q2 -2 2 -5 l0 -63 q0 -4 -3.5 -5.5 q-3.5 -1.5 -6.5 0.5 l-170 133 q-4 3 -4.5 8.5 q-0.5 5.5 2.5 9.5 l2 2 l170 133 q3 2 6.5 0.5 q3.5 -1.5 3.5 -5.5 Z\"></path></g></svg>",
"SubmenuCommands": [
{
"id": "editing-toolbar:editor:toggle-code",
"name": "Inline code",
"icon": "code-glyph"
},
{
"id": "editing-toolbar:codeblock",
"name": "Code block",
"icon": "codeblock-glyph"
},
{
"id": "editing-toolbar:editor:insert-wikilink",
"name": "Insert wikilink [[]]",
"icon": "<svg width=\"15\" height=\"15\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M306 134 l91 0 q1 0 1 -8 l0 -80 q0 -8 -1 -8 l-91 0 q-1 0 -1 7 q0 -8 -5 -8 l-45 0 q-5 0 -5 8 l0 784 q0 8 5 8 l45 0 q5 0 5 -8 q0 8 1 8 l91 0 q1 0 1 -8 l0 -80 q0 -8 -1 -8 l-91 0 q-1 0 -1 8 l0 -623 q0 8 1 8 ZM139 134 l91 0 q1 0 1 -8 l0 -80 q0 -8 -1 -8 l-91 0 q-1 0 -1 7 q0 -8 -5 -8 l-45 0 q-5 0 -5 8 l0 784 q0 8 5 8 l45 0 q5 0 5 -8 q0 8 1 8 l91 0 q1 0 1 -8 l0 -80 q0 -8 -1 -8 l-91 0 q-1 0 -1 8 l0 -623 q0 8 1 8 ZM711 134 q1 0 1 -8 l0 623 q0 -8 -1 -8 l-91 0 q-1 0 -1 8 l0 80 q0 8 1 8 l91 0 q1 0 1 -8 q0 8 4 8 l46 0 q4 0 4 -8 l0 -784 q0 -8 -4 -8 l-46 0 q-4 0 -4 8 q0 -7 -1 -7 l-91 0 q-1 0 -1 8 l0 80 q0 8 1 8 l91 0 ZM878 134 q1 0 1 -8 l0 623 q0 -8 -1 -8 l-91 0 q-1 0 -1 8 l0 80 q0 8 1 8 l91 0 q1 0 1 -8 q0 8 5 8 l45 0 q4 0 4 -8 l0 -784 q0 -8 -4 -8 l-45 0 q-5 0 -5 8 q0 -7 -1 -7 l-91 0 q-1 0 -1 8 l0 80 q0 8 1 8 l91 0 Z\"></path></g></svg>"
},
{
"id": "editing-toolbar:editor:insert-embed",
"name": "Insert embed ![[]]",
"icon": "note-glyph"
},
{
"id": "editing-toolbar:insert-link",
"name": "Insert link []()",
"icon": "link-glyph"
},
{
"id": "editing-toolbar:hrline",
"name": "Horizontal divider",
"icon": "<svg width=\"18\" height=\"18\" focusable=\"false\" fill=\"currentColor\" viewBox=\"0 0 1024 1024\"><g transform=\"scale(1, -1) translate(0, -896) scale(0.9, 0.9) \"><path class=\"path\" d=\"M912 424 l0 -80 q0 -3 -2.5 -5.5 q-2.5 -2.5 -5.5 -2.5 l-784 0 q-3 0 -5.5 2.5 q-2.5 2.5 -2.5 5.5 l0 80 q0 3 2.5 5.5 q2.5 2.5 5.5 2.5 l784 0 q3 0 5.5 -2.5 q2.5 -2.5 2.5 -5.5 Z\"></path></g></svg>"
},
{
"id": "editing-toolbar:custom-summary",
"name": "Summary",
"icon": "chat-bubbles-filled"
},
{
"id": "editing-toolbar:custom-small",
"name": "Small text",
"icon": "header-n"
},
{
"id": "editing-toolbar:superscript",
"name": "Superscript",
"icon": "superscript-glyph"
},
{
"id": "editing-toolbar:subscript",
"name": "Subscript",
"icon": "subscript-glyph"
},
{
"id": "editing-toolbar:toggle-inline-math",
"name": "Inline math",
"icon": "lucide-sigma"
},
{
"id": "editing-toolbar:editor:insert-mathblock",
"name": "MathBlock",
"icon": "lucide-sigma-square"
}
]
},
{
"id": "SubmenuCommands-aligin",
"name": "submenu-aligin",
"icon": "align-left",
"SubmenuCommands": [
{
"id": "editing-toolbar:left",
"name": "<p aligin=\"left\"></p>",
"icon": "align-left"
},
{
"id": "editing-toolbar:center",
"name": "<center>",
"icon": "align-center"
},
{
"id": "editing-toolbar:right",
"name": "<p aligin=\"right\"></p>",
"icon": "align-right"
},
{
"id": "editing-toolbar:justify",
"name": "<p aligin=\"justify\"></p>",
"icon": "align-justify"
}
]
},
{
"id": "editing-toolbar:change-font-color",
"name": "Change font color[html]",
"icon": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" focusable=\"false\" fill=\"currentColor\"><g fill-rule=\"evenodd\"><path id=\"change-font-color-icon\" d=\"M3 18h18v3H3z\" style=\"fill:#2DC26B\"></path><path d=\"M8.7 16h-.8a.5.5 0 01-.5-.6l2.7-9c.1-.3.3-.4.5-.4h2.8c.2 0 .4.1.5.4l2.7 9a.5.5 0 01-.5.6h-.8a.5.5 0 01-.4-.4l-.7-2.2c0-.3-.3-.4-.5-.4h-3.4c-.2 0-.4.1-.5.4l-.7 2.2c0 .3-.2.4-.4.4zm2.6-7.6l-.6 2a.5.5 0 00.5.6h1.6a.5.5 0 00.5-.6l-.6-2c0-.3-.3-.4-.5-.4h-.4c-.2 0-.4.1-.5.4z\"></path></g></svg>"
},
{
"id": "zenmode:toggle-zen-mode",
"name": "Zen Mode: Toggle",
"icon": "expand"
}
],
"followingCommands": [],
"topCommands": [],
"fixedCommands": [],
"mobileCommands": [],
"enableMultipleConfig": false,
"enableTopToolbar": true,
"enableFollowingToolbar": false,
"enableFixedToolbar": false,
"appendMethod": "workspace",
"shouldShowMenuOnSelect": false,
"cMenuVisibility": false,
"cMenuBottomValue": 4.25,
"cMenuNumRows": 12,
"cMenuWidth": 610,
"cMenuFontColor": "#2DC26B",
"cMenuBackgroundColor": "#d3f8b6",
"autohide": false,
"Iscentered": false,
"custom_bg1": "#FFB78B8C",
"custom_bg2": "#CDF4698C",
"custom_bg3": "#A0CCF68C",
"custom_bg4": "#F0A7D88C",
"custom_bg5": "#ADEFEF8C",
"custom_fc1": "#D83931",
"custom_fc2": "#DE7802",
"custom_fc3": "#245BDB",
"custom_fc4": "#6425D0",
"custom_fc5": "#646A73",
"isLoadOnMobile": false,
"horizontalPosition": 0,
"verticalPosition": 0,
"formatBrushes": {},
"customCommands": [
{
"id": "custom-summary",
"name": "Summary",
"prefix": "<details> \n<summary>",
"suffix": "</summary> \nInclude details here.\n</details>",
"char": 0,
"line": 0,
"islinehead": false,
"icon": "chat-bubbles-filled"
},
{
"id": "custom-small",
"name": "Small text",
"prefix": "<small>",
"suffix": "</small>",
"char": 0,
"line": 0,
"islinehead": false,
"icon": "header-n"
}
],
"viewTypeSettings": {
"bases": false
},
"appearanceByStyle": {
"top": {
"toolbarBackgroundColor": "rgba(var(--background-secondary-rgb), 0.7)",
"toolbarIconColor": "var(--text-normal)",
"toolbarIconSize": 18,
"aestheticStyle": "glass"
},
"following": {
"toolbarBackgroundColor": "rgba(var(--background-secondary-rgb), 0.7)",
"toolbarIconColor": "var(--text-normal)",
"toolbarIconSize": 18,
"aestheticStyle": "default"
},
"fixed": {
"toolbarBackgroundColor": "rgba(var(--background-secondary-rgb), 0.7)",
"toolbarIconColor": "var(--text-normal)",
"toolbarIconSize": 18,
"aestheticStyle": "default"
},
"mobile": {
"toolbarBackgroundColor": "rgba(var(--background-secondary-rgb), 0.7)",
"toolbarIconColor": "var(--text-normal)",
"toolbarIconSize": 18,
"aestheticStyle": "default"
}
},
"toolbarBackgroundColor": "rgba(var(--background-secondary-rgb), 0.7)",
"toolbarIconColor": "var(--text-normal)",
"toolbarIconSize": 18,
"useCurrentLineForRegex": false,
"commandIdsFixed": true
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
{
"id": "editing-toolbar",
"name": "Editing Toolbar",
"version": "3.2.7",
"minAppVersion": "0.14.0",
"description": "The Obsidian Editing Toolbar is modified from cmenu, which provides more powerful customization settings and has many built-in editing commands to be a MS Word-like toolbar editing experience.",
"author": "Cuman",
"authorUrl": "https://github.com/cumany/obsidian-editing-toolbar",
"isDesktopOnly": false
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"showRightClickMenu": true,
"showFileExplorerIcon": true,
"focusLevel": "parent",
"customFolderPath": "",
"hideAncestorFolders": false
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"id":"explorer-focus","name":"Explorer Focus","version":"0.1.6","minAppVersion":"0.15.0","description":"Focus on a specific file or folder in the file explorer.","author":"David V. Kimball","authorUrl":"https://davidvkimball.com","fundingUrl":"https://patreon.com/davidvkimball","isDesktopOnly":false}

View File

@@ -0,0 +1,18 @@
{
"historyPropertyName": "aliases",
"ignoreRegexes": [
"^_",
"^Untitled$",
"^Untitled \\d+$"
],
"timeoutSeconds": 5,
"caseSensitive": false,
"autoCreateFrontmatter": true,
"includeFolders": [],
"excludeFolders": [],
"fileExtensions": [
"md"
],
"trackFolderRenames": "",
"excludePropertyName": ""
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"id":"file-name-history","name":"File Name History","version":"0.2.6","minAppVersion":"0.15.0","description":"Store file name or folder name change history into note properties.","author":"David V. Kimball","authorUrl":"https://davidvkimball.com","fundingUrl":"https://patreon.com/davidvkimball","isDesktopOnly":false}

View File

@@ -0,0 +1,11 @@
/* Group settings compatibility styling for older Obsidian builds (< 1.11.0) */
/* Scoped to only this plugin's settings container to avoid affecting other plugins */
.file-name-history-settings-compat .setting-group-heading h3 {
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
font-size: 1rem;
font-weight: 600;
border-bottom: none !important;
}

View File

@@ -0,0 +1,129 @@
{
"syncFolderName": true,
"ctrlKey": true,
"altKey": false,
"hideFolderNote": true,
"templatePath": "",
"autoCreate": false,
"autoCreateFocusFiles": true,
"autoCreateForAttachmentFolder": false,
"autoCreateForFiles": false,
"enableCollapsing": false,
"excludeFolders": [],
"whitelistFolders": [],
"showDeleteConfirmation": true,
"underlineFolder": true,
"stopWhitespaceCollapsing": true,
"underlineFolderInPath": true,
"openFolderNoteOnClickInPath": true,
"openInNewTab": false,
"focusExistingTab": false,
"oldFolderNoteName": "{{folder_name}}",
"folderNoteName": "index",
"folderNoteType": ".md",
"disableFolderHighlighting": false,
"newFolderNoteName": "{{folder_name}}",
"storageLocation": "insideFolder",
"syncDelete": false,
"showRenameConfirmation": true,
"defaultOverview": {
"id": "",
"folderPath": "",
"title": "{{folderName}} overview",
"showTitle": false,
"depth": 3,
"includeTypes": [
"folder",
"markdown"
],
"style": "list",
"disableFileTag": false,
"sortBy": "name",
"sortByAsc": true,
"showEmptyFolders": false,
"onlyIncludeSubfolders": false,
"storeFolderCondition": true,
"showFolderNotes": false,
"disableCollapseIcon": true,
"alwaysCollapse": false,
"autoSync": true,
"allowDragAndDrop": true,
"hideLinkList": true,
"hideFolderOverview": false,
"useActualLinks": false,
"fmtpIntegration": false,
"titleSize": 1,
"isInCallout": false
},
"useSubmenus": true,
"syncMove": true,
"frontMatterTitle": {
"enabled": false,
"explorer": true,
"path": true
},
"settingsTab": "general",
"supportedFileTypes": [
"md",
"canvas",
"base"
],
"boldName": false,
"boldNameInPath": false,
"cursiveName": false,
"cursiveNameInPath": false,
"disableOpenFolderNoteOnClick": false,
"openByClick": true,
"openWithCtrl": false,
"openWithAlt": false,
"excludeFolderDefaultSettings": {
"type": "folder",
"path": "",
"id": "a8a3086d-f63b-409f-be17-9e5676db13b5",
"subFolders": true,
"disableSync": true,
"disableAutoCreate": true,
"disableFolderNote": false,
"enableCollapsing": false,
"position": 0,
"excludeFromFolderOverview": false,
"string": "",
"hideInSettings": false,
"detached": false,
"showFolderNote": false
},
"excludePatternDefaultSettings": {
"type": "pattern",
"path": "",
"id": "d97ab7fb-3b20-49e7-8128-666bf4985a2c",
"subFolders": true,
"disableSync": true,
"disableAutoCreate": true,
"disableFolderNote": false,
"enableCollapsing": false,
"position": 0,
"excludeFromFolderOverview": false,
"string": "",
"hideInSettings": false,
"detached": false,
"showFolderNote": false
},
"hideCollapsingIcon": false,
"hideCollapsingIconForEmptyFolders": false,
"tabManagerEnabled": true,
"ignoreAttachmentFolder": true,
"deleteFilesAction": "trash",
"openSidebar": {
"mobile": false,
"desktop": true
},
"highlightFolder": true,
"persistentSettingsTab": {
"afterRestart": true,
"afterChangingTab": true
},
"firstTimeInsertOverview": true,
"fvGlobalSettings": {
"autoUpdateLinks": false
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
{
"id": "folder-notes",
"name": "Folder notes",
"version": "1.8.18",
"minAppVersion": "0.15.0",
"description": "Create notes within folders that can be accessed without collapsing the folder, similar to the functionality offered in Notion.",
"author": "Lost Paul",
"authorUrl": "https://github.com/LostPaul",
"fundingUrl": "https://ko-fi.com/paul305844",
"helpUrl": "https://lostpaul.github.io/obsidian-folder-notes/",
"isDesktopOnly": false
}

View File

@@ -0,0 +1,349 @@
/* ==========================================================================
General States & Utilities
========================================================================== */
.hide,
.hide-folder .folder-name,
.hide-folder-note .is-folder-note {
display: none;
}
/* make.md plugin integration */
.hide-folder-note .mk-tree-node > .mk-tree-wrapper > .dropzone > .mk-tree-item.is-folder-note {
opacity: 40%;
display: flex;
}
.pointer-cursor,
.has-folder-note .nav-folder-title-content:hover,
.has-folder-note.view-header-breadcrumb:hover,
.nav-folder-collapse-indicator:hover,
.fn-delete-confirmation-modal-buttons span:hover,
.fn-delete-confirmation-modal-buttons input:hover {
cursor: pointer !important;
}
.hide-folder-note :not(.show-folder-note-in-explorer).only-has-folder-note .nav-folder-children {
display: none !important;
}
/* ==========================================================================
Tree Items
========================================================================== */
body:not(.is-grabbing) .tree-item-self.fn-is-active:hover,
body:not(.disable-folder-highlight) .tree-item-self.fn-is-active {
color: var(--nav-item-color-active);
background-color: var(--nav-item-background-active);
font-weight: var(--nav-item-weight-active);
}
/* ==========================================================================
Exclude Folder Settings
========================================================================== */
.fn-excluded-folder-heading {
margin-top: 0 !important;
border-top: 1px solid var(--background-modifier-border);
}
.add-exclude-folder-item,
.fn-exclude-folder-list {
padding-bottom: 0 !important;
}
.fn-exclude-folder-list.setting-item {
border-top: 0 !important;
border-bottom: 0 !important;
}
.fn-exclude-folder-list .setting-item-control {
display: flex;
justify-content: flex-start !important;
}
.fn-exclude-folder-list .setting-item-info {
display: none !important;
}
.fn-exclude-folder-list .search-input-container {
width: 100%;
}
/* ==========================================================================
Modal Styles
========================================================================== */
.fn-backup-warning-modal .fn-modal-button-container {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.fn-confirmation-modal {
padding-bottom: 0;
}
.fn-confirmation-modal .setting-item {
border-top: 0 !important;
padding-top: 0 !important;
}
:not(.is-phone) .fn-confirmation-modal-button {
margin-right: 0.7rem;
}
:not(.is-phone) .fn-delete-confirmation-modal-buttons {
display: flex;
align-items: center;
margin-top: 10px;
}
:not(.is-phone) .fn-delete-confirmation-modal-buttons .fn-confirmation-modal-button {
margin-left: auto;
}
:not(.is-phone) .fn-delete-confirmation-modal-buttons input[type="checkbox"] {
margin-right: 5px;
}
.is-phone .fn-delete-confirmation-modal-buttons {
display: flex;
flex-direction: column;
align-items: center;
}
.is-phone .fn-delete-confirmation-modal-buttons .fn-confirmation-modal-button {
margin-top: 10px;
}
/* ==========================================================================
Folder Overview
========================================================================== */
.folder-overview-container.fv-remove-edit-button .folder-overview-edit-button {
display: none;
}
.cm-line:has(.fv-link-list-item),
li:has(.fv-link-list-item),
.el-ul:has(.fv-link-list-item),
.cm-line:has(.fv-link-list-start),
.cm-line:has(.fv-link-list-end),
.fv-hide-overview {
display: none !important;
}
.folder-overview-list {
margin-top: 0 !important;
margin-bottom: 0 !important;
padding-bottom: 1.200 !important;
padding-top: 1.200 !important;
}
.folder-overview-list-item {
display: flex;
}
.folder-overview-list::marker {
color: var(--text-faint);
}
.folder-list::marker {
color: var(--text-normal) !important;
}
.folder-overview-grid {
display: grid;
grid-gap: 20px;
grid-template-columns: repeat(3, 1fr);
}
.folder-overview-grid-item {
flex: 1 1 auto;
margin: 0 1.2rem 1.2rem 0;
}
.folder-overview-grid-item-article article {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 15px;
flex: 1;
}
.folder-overview-grid-item-article a {
text-decoration: none !important;
}
.folder-overview-grid-item-article h1 {
font-size: 1.2rem;
}
.overview-setting-item-fv {
border-top: 1px solid var(--background-modifier-border);
padding: 0.75em 0;
align-items: center;
}
.overview-setting-item-fv .setting-item {
padding: 0;
}
/* ==========================================================================
File Explorer & Path Styling
========================================================================== */
.folder-note-underline .has-folder-note .nav-folder-title-content {
text-decoration-line: underline;
text-decoration-color: var(--text-faint);
text-decoration-thickness: 2px;
text-underline-offset: 1px;
}
.folder-note-underline-path .has-folder-note.view-header-breadcrumb {
text-decoration-line: underline;
text-decoration-color: var(--text-faint);
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
.folder-note-bold .has-folder-note .nav-folder-title-content,
.folder-note-bold-path .has-folder-note.view-header-breadcrumb {
font-weight: bold;
}
.folder-note-cursive .has-folder-note .nav-folder-title-content,
.folder-note-cursive-path .has-folder-note.view-header-breadcrumb {
font-style: italic;
}
/* Collapse Icon Handling */
.fn-folder-overview-collapse-icon {
display: block !important;
}
.fn-has-no-files .collapse-icon,
.fn-hide-collapse-icon .has-folder-note.only-has-folder-note .tree-item-icon,
body.fn-ignore-attachment-folder.fn-hide-collapse-icon .only-has-folder-note .fn-empty-folder.fn-has-attachment-folder .tree-item-icon,
body.fn-hide-collapse-icon .only-has-folder-note .fn-empty-folder:not(.fn-has-attachment-folder) .tree-item-icon,
body.fn-hide-empty-collapse-icon :not(.only-has-folder-note) > .fn-empty-folder:not(.fn-has-attachment-folder) .tree-item-icon,
body.fn-hide-collapse-icon.only-has-folder-note:not(.is-collapsed):not(.show-folder-note-in-explorer)>.nav-folder-children {
display: none;
}
/* ==========================================================================
Settings Tabs
========================================================================== */
.fn-settings-tab-bar {
display: flex;
flex-direction: row;
padding-bottom: 1rem;
}
.fn-settings-tab {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--size-4-2);
padding: 10px;
border: 1px solid var(--background-modifier-border);
}
.fn-settings-tab-active {
background-color: var(--color-accent);
color: var(--text-on-accent);
}
.fn-settings-tab-name {
font-weight: bold;
}
.fn-settings-tab-icon {
display: flex;
}
/* ==========================================================================
Suggestion Container
========================================================================== */
.fn-suggestion-container {
position: absolute;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--background-primary);
max-width: 500px;
max-height: 300px;
border-radius: var(--radius-m);
border: 1px solid var(--background-modifier-border);
box-shadow: var(--shadow-s);
z-index: var(--layer-notice);
}
/* ==========================================================================
Whitelist Folder Input (Desktop & Mobile)
========================================================================== */
/* Default Desktop Layout */
.fn-whitelist-folder-input-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin: 0;
}
.fn-whitelist-folder-input-container input {
flex-grow: 1;
width: auto;
margin-right: 8px;
height: 40px;
box-sizing: border-box;
}
.fn-whitelist-folder-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
align-items: center;
flex-grow: 0;
flex-shrink: 0;
}
/* Mobile Overrides */
@media (max-width: 768px) {
.fn-whitelist-folder-input-container {
display: block;
width: 100%;
text-align: center;
}
.fn-whitelist-folder-input-container input {
width: 100%;
margin-right: 0;
}
.fn-whitelist-folder-buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
}
.is-phone .fn-overview-folder-path .setting-item-control {
display: block;
}
}

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