Compare commits

..

10 commits

Author SHA1 Message Date
Devon Campbell
3d95b867c0 Add Forgejo-backed editor 2026-03-21 20:22:54 -04:00
wasita
59b5807601
🐛 fix(cli): use 'gray' instead of 'grey' in styleText calls (#2321)
Node.js util.styleText does not accept 'grey' as a format name.
While util.inspect.colors defines 'grey' as a non-enumerable alias
for 'gray', styleText validates against Object.keys(inspect.colors)
which only includes enumerable properties — so 'grey' has never
been a valid styleText format and throws ERR_INVALID_ARG_VALUE.
2026-03-04 16:42:49 +01:00
Andrew G. Dunn
9576701d85
fix: await micromorph() to prevent race condition with nav event handlers (#2323)
micromorph() returns Promise<void> because its internal patch() function
is async and uses Promise.all for recursive child patching. Without
await, the DOM morph may still be in progress when the nav event fires
and downstream handlers attempt to rebuild dynamic content.

Fixes #2322
2026-03-02 08:31:53 -08:00
dependabot[bot]
ec00a40aef
chore(deps): bump the production-dependencies group with 4 updates (#2289)
Bumps the production-dependencies group with 4 updates: [lightningcss](https://github.com/parcel-bundler/lightningcss), [unist-util-visit](https://github.com/syntax-tree/unist-util-visit), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [prettier](https://github.com/prettier/prettier).


Updates `lightningcss` from 1.30.2 to 1.31.1
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/commits)

Updates `unist-util-visit` from 5.0.0 to 5.1.0
- [Release notes](https://github.com/syntax-tree/unist-util-visit/releases)
- [Commits](https://github.com/syntax-tree/unist-util-visit/compare/5.0.0...5.1.0)

Updates `@types/node` from 25.0.9 to 25.0.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `prettier` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.0...3.8.1)

---
updated-dependencies:
- dependency-name: lightningcss
  dependency-version: 1.31.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: unist-util-visit
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: prettier
  dependency-version: 3.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-27 13:27:17 -05:00
dependabot[bot]
25a6747d7d
chore(deps): bump the production-dependencies group across 1 directory with 5 updates (#2282)
Bumps the production-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [esbuild-sass-plugin](https://github.com/glromeo/esbuild-sass-plugin) | `3.3.1` | `3.6.0` |
| [preact](https://github.com/preactjs/preact) | `10.28.1` | `10.28.2` |
| [satori](https://github.com/vercel/satori) | `0.18.3` | `0.19.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.9` |
| [prettier](https://github.com/prettier/prettier) | `3.7.4` | `3.8.0` |



Updates `esbuild-sass-plugin` from 3.3.1 to 3.6.0
- [Release notes](https://github.com/glromeo/esbuild-sass-plugin/releases)
- [Commits](https://github.com/glromeo/esbuild-sass-plugin/commits)

Updates `preact` from 10.28.1 to 10.28.2
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.28.1...10.28.2)

Updates `satori` from 0.18.3 to 0.19.1
- [Release notes](https://github.com/vercel/satori/releases)
- [Commits](https://github.com/vercel/satori/compare/0.18.3...0.19.1)

Updates `@types/node` from 25.0.3 to 25.0.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `prettier` from 3.7.4 to 3.8.0
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.0)

---
updated-dependencies:
- dependency-name: esbuild-sass-plugin
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: preact
  dependency-version: 10.28.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: satori
  dependency-version: 0.19.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: prettier
  dependency-version: 3.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 12:37:59 -05:00
Emile Bangma
b4fb0e6682
fix(explorer): mobile scroll containment (#2283)
* fix(explorer): mobile scroll contaiment

* only apply scroll containment to explorer on mobile
2026-01-20 12:37:40 -05:00
Emile Bangma
f346a01296
feat(explorer): Add active class to current folder in explorer (#2196) 2026-01-08 09:54:41 +08:00
Rahmat Ardiansyah
c2dcc63b5f
feat: add scroll padding for mobile screen (#2249) 2026-01-08 09:36:02 +08:00
derfalx
c2bea8a4c4
fix(citation): Language parameter for non en-US settings (#2075)
* Fix language parameter of the citation plugin for non en-US settings

Per default the rehype-citation project only supports en-US, as
explained here: https://github.com/timlrx/rehype-citation/issues/12
For other languages one can provide a locale-file either by passing
its path or providing an URL. The following repository contains locale
files for multiple languages. So, these are used, in case a non en-US
language is used in quarzt. But this optimistically assumes there is
indeed an according locale file.

In summary this solves the problem only partially, since there are
still some languages which will not work properly.

* Fixing code style by running prettier with --write

* Excluding `en-US` locales from the new behaviour.

* Removing unnecessary `null` und `undefined` check.

* Update quartz/plugins/transformers/citations.ts

* Update quartz/plugins/transformers/citations.ts

* Update quartz/plugins/transformers/citations.ts

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2026-01-08 09:30:42 +08:00
Jacky Zhao
fa8d87a23a docs: link fixes and cleanup 2026-01-08 09:04:56 +08:00
15 changed files with 1615 additions and 266 deletions

View file

@ -3,7 +3,6 @@
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/

View file

@ -5,3 +5,7 @@ You can run the below one-liner to run Quartz in Docker.
```sh
docker run --rm -itp 8080:8080 -p 3001:3001 -v ./content:/usr/src/app/content $(docker build -q .)
```
> [!warning] Not to be used for production
> Serve mode is intended for local previews only.
> For production workloads, see the page on [[hosting]].

View file

@ -8,7 +8,7 @@ By default, Quartz only fetches previews for pages inside your vault due to [COR
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
Similar to Obsidian, [[quartz layout.png|images referenced using wikilinks]] can also be viewed as popups.
Similar to Obsidian, [[quartz-layout-desktop.png|images referenced using wikilinks]] can also be viewed as popups.
## Configuration

1094
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -36,7 +36,13 @@
},
"dependencies": {
"@clack/prompts": "^0.11.0",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/language": "^6.12.2",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.40.0",
"@floating-ui/dom": "^1.7.4",
"@lezer/highlight": "^1.2.3",
"@myriaddreamin/rehype-typst": "^0.6.0",
"@napi-rs/simple-git": "0.1.22",
"@tweenjs/tween.js": "^25.0.0",
@ -44,8 +50,9 @@
"async-mutex": "^0.5.0",
"chokidar": "^5.0.0",
"cli-spinner": "^0.2.10",
"codemirror": "^6.0.2",
"d3": "^7.9.0",
"esbuild-sass-plugin": "^3.3.1",
"esbuild-sass-plugin": "^3.6.0",
"flexsearch": "^0.8.205",
"github-slugger": "^2.0.0",
"globby": "^16.1.0",
@ -55,14 +62,14 @@
"hast-util-to-string": "^3.0.1",
"is-absolute-url": "^5.0.0",
"js-yaml": "^4.1.1",
"lightningcss": "^1.30.2",
"lightningcss": "^1.31.1",
"mdast-util-find-and-replace": "^3.0.2",
"mdast-util-to-hast": "^13.2.1",
"mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5",
"minimatch": "^10.1.1",
"pixi.js": "^8.15.0",
"preact": "^10.28.1",
"preact": "^10.28.2",
"preact-render-to-string": "^6.6.5",
"pretty-bytes": "^7.1.0",
"pretty-time": "^1.1.0",
@ -83,7 +90,7 @@
"remark-rehype": "^11.1.2",
"remark-smartypants": "^3.0.2",
"rfdc": "^1.4.1",
"satori": "^0.18.3",
"satori": "^0.19.1",
"serve-handler": "^6.1.6",
"sharp": "^0.34.5",
"shiki": "^1.26.2",
@ -91,7 +98,7 @@
"to-vfile": "^8.0.0",
"toml": "^3.0.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"unist-util-visit": "^5.1.0",
"vfile": "^6.0.3",
"workerpool": "^10.0.1",
"ws": "^8.19.0",
@ -101,13 +108,13 @@
"@types/d3": "^7.4.3",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.0.3",
"@types/node": "^25.0.10",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.18.1",
"@types/yargs": "^17.0.35",
"esbuild": "^0.27.2",
"prettier": "^3.7.4",
"prettier": "^3.8.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}

View file

@ -89,7 +89,7 @@ const config: QuartzConfig = {
Plugin.Favicon(),
Plugin.NotFoundPage(),
// Comment out CustomOgImages to speed up build time
Plugin.CustomOgImages(),
//Plugin.CustomOgImages(),
],
},
}

View file

@ -1,5 +1,6 @@
import { PageLayout, SharedLayout } from "./quartz/cfg"
import * as Component from "./quartz/components"
import Editor from "./quartz/components/Editor"
// components shared across all pages
export const sharedPageComponents: SharedLayout = {
@ -24,6 +25,7 @@ export const defaultContentPageLayout: PageLayout = {
Component.ArticleTitle(),
Component.ContentMeta(),
Component.TagList(),
Editor(),
],
left: [
Component.PageTitle(),

View file

@ -318,7 +318,7 @@ export async function handleBuild(argv) {
const result = await ctx.rebuild().catch((err) => {
console.error(`${styleText("red", "Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${styleText("grey", err)}`)
console.log(`Reason: ${styleText("gray", err)}`)
process.exit(1)
})
release()
@ -395,7 +395,7 @@ export async function handleBuild(argv) {
status >= 200 && status < 300
? styleText("green", `[${status}]`)
: styleText("red", `[${status}]`)
console.log(statusString + styleText("grey", ` ${argv.baseDir}${req.url}`))
console.log(statusString + styleText("gray", ` ${argv.baseDir}${req.url}`))
release()
}
@ -406,7 +406,7 @@ export async function handleBuild(argv) {
})
console.log(
styleText("yellow", "[302]") +
styleText("grey", ` ${argv.baseDir}${req.url} -> ${newFp}`),
styleText("gray", ` ${argv.baseDir}${req.url} -> ${newFp}`),
)
res.end()
}
@ -482,7 +482,7 @@ export async function handleBuild(argv) {
.on("change", () => build(clientRefresh))
.on("unlink", () => build(clientRefresh))
console.log(styleText("grey", "hint: exit with ctrl+c"))
console.log(styleText("gray", "hint: exit with ctrl+c"))
}
}

View file

@ -0,0 +1,317 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types";
import editorScript from "./scripts/editor.inline";
import path from "path";
const Editor: QuartzComponent = ({ fileData, allFiles, displayClass }: QuartzComponentProps) => {
if (!fileData.filePath) return null;
const dIndex = process.argv.findIndex(arg => arg === "-d" || arg === "--directory");
const baseDir = dIndex !== -1 ? process.argv[dIndex + 1] : "content";
const absoluteBase = path.resolve(process.cwd(), baseDir);
const absoluteFile = path.resolve(process.cwd(), fileData.filePath);
const repoFilePath = path.relative(absoluteBase, absoluteFile);
const directories = new Set<string>();
for (const file of allFiles) {
if (file.slug) {
const parts = file.slug.split("/");
if (parts.length > 1) {
parts.pop();
directories.add(parts.join("/"));
}
}
}
return (
<div class={`editor-container ${displayClass ?? ""}`}>
<div id="fab-container">
<button id="new-button" class="fab" aria-label="New Note" title="New Note">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="18" x2="12" y2="12"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
</button>
<button id="edit-button" class="fab" data-filepath={repoFilePath} aria-label="Edit Note" title="Edit Note">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
</div>
<div id="editor-layout" style="display: none;">
<div id="file-path-controls">
<div class="input-group">
<label for="file-directory">Directory</label>
<input type="text" id="file-directory" placeholder="e.g. thoughts/daily" list="folder-list" />
<datalist id="folder-list">
{Array.from(directories).sort().map(dir => (
<option value={dir}></option>
))}
</datalist>
</div>
<div class="input-group">
<label for="file-name">Note Name</label>
<input type="text" id="file-name" placeholder="e.g. My Idea" />
</div>
</div>
<div id="editor-wrapper"></div>
<div id="editor-controls">
<input type="text" id="commit-subject" placeholder="Commit message..." maxlength={72} />
<details id="commit-details">
<summary>Add extended description...</summary>
<textarea id="commit-body" rows={4} placeholder="Extended description..."></textarea>
</details>
<div class="editor-buttons">
<button id="delete-button">Delete Note</button>
<div class="editor-buttons-right">
<button id="cancel-button">Cancel</button>
<button id="save-button">Save</button>
</div>
</div>
</div>
</div>
</div>
);
};
Editor.afterDOMLoaded = editorScript;
Editor.css = `
#fab-container {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
flex-direction: row;
gap: 1rem;
z-index: 999;
}
.fab {
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
background: var(--secondary);
color: var(--light);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
cursor: pointer;
border: none;
transition: transform 0.2s ease, background-color 0.2s ease;
}
.fab:hover:not(:disabled) {
background: var(--tertiary);
transform: scale(1.05);
}
.fab:disabled {
opacity: 0.7;
cursor: not-allowed;
}
#editor-layout {
width: 100%;
display: flex;
flex-direction: column;
margin-top: 1rem;
margin-bottom: 2rem;
}
#editor-layout.is-new-note {
margin-top: 6rem;
}
#file-path-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 800px) {
#editor-layout.is-new-note {
margin-top: 1rem;
}
}
@media (max-width: 600px) {
#file-path-controls {
flex-direction: column;
gap: 0.5rem;
}
}
.input-group {
display: flex;
flex-direction: column;
flex: 1;
}
.input-group label {
font-size: 0.8rem;
color: var(--gray);
margin-bottom: 0.2rem;
}
.input-group input {
padding: 0.5rem;
border: 1px solid var(--gray);
border-radius: 4px;
background: var(--light);
color: var(--dark);
font-family: inherit;
}
.input-group input:disabled {
opacity: 0.7;
cursor: not-allowed;
}
#editor-wrapper {
width: 100%;
border: 1px solid var(--darkgray);
border-radius: 5px;
overflow: hidden;
background: #2a2a2a;
cursor: text;
}
.cm-editor {
min-height: 60vh;
height: auto;
background-color: #2a2a2a;
}
.cm-scroller {
height: 100% !important;
min-height: 60vh;
}
.cm-content {
min-height: 100%;
}
#editor-controls {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#commit-subject, #commit-body {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--gray);
border-radius: 4px;
background: var(--light);
color: var(--dark);
font-family: inherit;
box-sizing: border-box;
}
#commit-subject:disabled, #commit-body:disabled {
opacity: 0.7;
cursor: not-allowed;
}
#commit-details {
margin-top: 0.2rem;
margin-bottom: 1.5rem; /* Increased vertical spacing above the buttons */
}
#commit-details summary {
cursor: pointer;
color: var(--gray);
font-size: 0.9rem;
user-select: none;
display: inline-block;
}
#commit-details summary:hover {
color: var(--darkgray);
}
#commit-body {
margin-top: 0.5rem;
resize: vertical;
min-height: 4rem;
}
.editor-buttons {
display: flex;
justify-content: space-between; /* Pushes Delete to left, Save/Cancel to right */
align-items: center;
}
.editor-buttons-right {
display: flex;
gap: 0.5rem;
}
.editor-buttons button {
padding: 0.5rem 1rem;
cursor: pointer;
border: none;
border-radius: 4px;
font-weight: bold;
}
.editor-buttons button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#save-button {
background: var(--secondary);
color: var(--light);
}
#save-button:hover:not(:disabled) {
background: var(--tertiary);
}
#cancel-button {
background: var(--lightgray);
color: var(--dark);
}
#cancel-button:hover:not(:disabled) {
background: var(--gray);
}
#delete-button {
background: transparent;
color: #e06c75; /* A distinct red color for destructive actions */
border: 1px solid #e06c75;
}
#delete-button:hover:not(:disabled) {
background: rgba(224, 108, 117, 0.1);
}
#delete-button:disabled {
border-color: var(--gray);
color: var(--gray);
}
@media (max-width: 800px) {
#fab-container {
bottom: 1.5rem;
right: 1.5rem;
}
.fab {
width: 3rem;
height: 3rem;
}
}
`;
export default (() => Editor) satisfies QuartzComponentConstructor;

View file

@ -0,0 +1,395 @@
import { EditorState, Compartment } from "@codemirror/state";
import { EditorView, basicSetup } from "codemirror";
import { markdown } from "@codemirror/lang-markdown";
import { syntaxHighlighting, HighlightStyle } from "@codemirror/language";
import { tags as t } from "@lezer/highlight";
const FORGEJO_URL = "https://code.c.devon.lol";
const REPO_OWNER = "Devon";
const REPO_NAME = "knowledgebase";
const BRANCH = "main";
const customHighlightStyle = HighlightStyle.define([
{ tag: t.heading, color: "#61afef", fontWeight: "bold" },
{ tag: t.keyword, color: "#c678dd" },
{ tag: t.url, color: "#56b6c2" },
{ tag: t.link, color: "#56b6c2", textDecoration: "underline" },
{ tag: t.punctuation, color: "#e06c75" },
{ tag: t.strong, fontWeight: "bold" },
{ tag: t.emphasis, fontStyle: "italic" },
{ tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.meta, color: "#7f848e" },
{ tag: t.comment, color: "#7f848e", fontStyle: "italic" },
{ tag: t.string, color: "#98c379" },
{ tag: t.number, color: "#d19a66" },
{ tag: t.propertyName, color: "#e06c75" },
{ tag: t.variableName, color: "#e06c75" },
]);
const editorTheme = EditorView.theme({
"&": { backgroundColor: "#2a2a2a", color: "#e0e0e0", fontSize: "16px" },
".cm-content": { minHeight: "60vh", padding: "1rem" },
"&.cm-focused .cm-cursor": { borderLeftColor: "#e0e0e0" },
"&.cm-focused .cm-selectionBackground, ::selection": { backgroundColor: "#444444" },
".cm-gutters": { backgroundColor: "#222222", color: "#888888", borderRight: "1px solid #444444" },
".cm-scroller": { fontFamily: "inherit" }
}, { dark: true });
document.addEventListener("nav", () => {
const fabContainer = document.getElementById("fab-container");
const editBtn = document.getElementById("edit-button") as HTMLButtonElement;
const newBtn = document.getElementById("new-button") as HTMLButtonElement;
const saveBtn = document.getElementById("save-button") as HTMLButtonElement;
const cancelBtn = document.getElementById("cancel-button") as HTMLButtonElement;
const deleteBtn = document.getElementById("delete-button") as HTMLButtonElement;
const layout = document.getElementById("editor-layout");
const wrapper = document.getElementById("editor-wrapper");
const commitSubjectInput = document.getElementById("commit-subject") as HTMLInputElement;
const commitBodyInput = document.getElementById("commit-body") as HTMLTextAreaElement;
const commitDetails = document.getElementById("commit-details") as HTMLDetailsElement;
const fileDirectoryInput = document.getElementById("file-directory") as HTMLInputElement;
const fileNameInput = document.getElementById("file-name") as HTMLInputElement;
if (!editBtn || !newBtn || !layout || !wrapper || !fabContainer || !deleteBtn) return;
let editorView: EditorView | null = null;
let currentFileSha = "";
const filePath = editBtn.getAttribute("data-filepath") || "";
const editableCompartment = new Compartment();
let isCreatingNewNote = false;
const encodeBase64 = (str: string) => btoa(unescape(encodeURIComponent(str)));
const decodeBase64 = (str: string) => decodeURIComponent(escape(atob(str)));
const getPat = () => {
let pat = localStorage.getItem("forgejo_pat");
if (!pat) {
pat = prompt("Enter your Forgejo Personal Access Token:");
if (pat) localStorage.setItem("forgejo_pat", pat);
}
return pat;
};
const getCenterColumn = () => document.querySelector(".center");
const onWrapperClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target === wrapper || target.classList.contains("cm-scroller")) {
editorView?.focus();
}
};
const performDOMClose = () => {
const center = getCenterColumn();
if (center) {
Array.from(center.children).forEach(child => {
if (child !== layout) {
(child as HTMLElement).style.display = "";
}
});
}
layout.style.display = "none";
layout.classList.remove("is-new-note");
fabContainer.style.display = "flex";
commitSubjectInput.value = "";
commitBodyInput.value = "";
fileDirectoryInput.value = "";
fileNameInput.value = "";
commitDetails.open = false;
isCreatingNewNote = false;
};
const closeEditor = () => {
if (history.state?.isEditor) {
history.back();
} else {
performDOMClose();
history.replaceState(null, "", window.location.pathname);
}
};
const openEditorUI = (content: string, mode: "edit" | "new", pushHistory = true) => {
const center = getCenterColumn();
if (center) {
if (layout.parentNode !== center) {
center.appendChild(layout);
}
Array.from(center.children).forEach(child => {
if (child !== layout) {
const el = child as HTMLElement;
if (mode === "edit" && el.classList.contains("page-header")) {
el.style.display = "";
} else {
el.style.display = "none";
}
}
});
}
fabContainer.style.display = "none";
layout.style.display = "flex";
if (mode === "new") {
layout.classList.add("is-new-note");
deleteBtn.style.display = "none";
commitSubjectInput.placeholder = `Create new note...`;
} else {
layout.classList.remove("is-new-note");
deleteBtn.style.display = "block";
commitSubjectInput.placeholder = `Update ${filePath}...`;
}
if (pushHistory) {
if (mode === "edit") {
history.pushState({ isEditor: true }, "", "?edit");
} else {
history.pushState({ isEditor: true }, "", "/?new");
}
}
if (editorView) editorView.destroy();
editorView = new EditorView({
state: EditorState.create({
doc: content,
extensions: [
basicSetup,
markdown(),
editorTheme,
syntaxHighlighting(customHighlightStyle),
editableCompartment.of(EditorView.editable.of(true))
]
}),
parent: wrapper
});
};
const onEdit = async (e?: Event, pushHistory = true) => {
const pat = getPat();
if (!pat || !filePath) return;
isCreatingNewNote = false;
editBtn.disabled = true;
const parts = filePath.split("/");
const fName = parts.pop() || "";
fileDirectoryInput.value = parts.join("/");
fileNameInput.value = fName.replace(/\.md$/i, "");
const encodedFilePath = filePath.split('/').map(encodeURIComponent).join('/');
try {
const res = await fetch(`${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/contents/${encodedFilePath}?ref=${BRANCH}`, {
headers: { "Authorization": `token ${pat}` }
});
if (!res.ok) throw new Error("Failed to fetch file. Check your PAT.");
const data = await res.json();
currentFileSha = data.sha;
const content = decodeBase64(data.content);
openEditorUI(content, "edit", pushHistory);
} catch (err) {
alert(err);
fabContainer.style.display = "flex";
} finally {
editBtn.disabled = false;
}
};
const onNewNote = (e?: Event, pushHistory = true) => {
isCreatingNewNote = true;
currentFileSha = "";
fileDirectoryInput.value = localStorage.getItem("forgejo_default_dir") || "";
fileNameInput.value = "";
openEditorUI("", "new", pushHistory);
};
const onCancel = () => closeEditor();
const lockUI = (isLocked: boolean) => {
if (saveBtn) saveBtn.disabled = isLocked;
if (cancelBtn) cancelBtn.disabled = isLocked;
if (deleteBtn) deleteBtn.disabled = isLocked;
commitSubjectInput.disabled = isLocked;
commitBodyInput.disabled = isLocked;
fileDirectoryInput.disabled = isLocked;
fileNameInput.disabled = isLocked;
if (editorView) {
editorView.dispatch({
effects: editableCompartment.reconfigure(EditorView.editable.of(!isLocked))
});
}
};
const onSave = async () => {
if (!editorView) return;
const pat = getPat();
const newContent = editorView.state.doc.toString();
const dir = fileDirectoryInput.value.trim();
let file = fileNameInput.value.trim();
if (!file) {
alert("Please enter a Note Name.");
return;
}
if (file.startsWith(".")) {
alert("Filename must not start with a dot.");
return;
}
if (/[\[\]#\^|\\\/:]/.test(file)) {
alert("Filename cannot contain the following characters: [ ] # ^ | \\ / :");
return;
}
if (!file.toLowerCase().endsWith(".md")) {
file += ".md";
}
const cleanDir = dir.replace(/\/$/, "");
const targetFilePath = cleanDir ? `${cleanDir}/${file}` : file;
const isMovingOrRenaming = !isCreatingNewNote && targetFilePath !== filePath;
let defaultSubject = `Update ${targetFilePath}`;
if (isCreatingNewNote) defaultSubject = `Create ${targetFilePath}`;
if (isMovingOrRenaming) defaultSubject = `Move ${filePath} to ${targetFilePath}`;
const subject = commitSubjectInput.value.trim() || defaultSubject;
const body = commitBodyInput.value.trim();
const fullMessage = body ? `${subject}\n\n${body}` : subject;
lockUI(true);
if (saveBtn) saveBtn.innerText = "Saving...";
const encodedTargetFilePath = targetFilePath.split('/').map(encodeURIComponent).join('/');
try {
const payload: any = {
branch: BRANCH,
content: encodeBase64(newContent),
message: fullMessage
};
if (!isCreatingNewNote) {
payload.sha = currentFileSha;
if (isMovingOrRenaming) {
payload.from_path = filePath;
}
}
const res = await fetch(`${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/contents/${encodedTargetFilePath}`, {
method: isCreatingNewNote ? "POST" : "PUT",
headers: {
"Authorization": `token ${pat}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to save. If creating a new file, ensure it doesn't already exist.");
}
if (isCreatingNewNote) {
localStorage.setItem("forgejo_default_dir", cleanDir);
}
closeEditor();
alert(`Saved successfully!\n\nChanges will be live in about 5 minutes once the site rebuilds.\n\nNote: If you moved or renamed this note, it will be unavailable at its old path after the rebuild. Editing a moved/renamed note before the rebuild completes will fail.`);
} catch (err) {
alert(err);
} finally {
lockUI(false);
if (saveBtn) saveBtn.innerText = "Save";
}
};
const onDelete = async () => {
if (!filePath || !currentFileSha) return;
const confirmDelete = confirm(`Are you sure you want to permanently delete:\n\n${filePath}\n\nThis cannot be undone.`);
if (!confirmDelete) return;
const pat = getPat();
if (!pat) return;
const subject = commitSubjectInput.value.trim() || `Delete ${filePath}`;
const body = commitBodyInput.value.trim();
const fullMessage = body ? `${subject}\n\n${body}` : subject;
lockUI(true);
if (deleteBtn) deleteBtn.innerText = "Deleting...";
const encodedFilePath = filePath.split('/').map(encodeURIComponent).join('/');
try {
const res = await fetch(`${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/contents/${encodedFilePath}`, {
method: "DELETE",
headers: {
"Authorization": `token ${pat}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
branch: BRANCH,
message: fullMessage,
sha: currentFileSha
})
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to delete file.");
}
closeEditor();
alert(`Deleted successfully!\n\nChanges will be live in about 5 minutes once the site rebuilds.`);
} catch (err) {
alert(err);
} finally {
lockUI(false);
if (deleteBtn) deleteBtn.innerText = "Delete Note";
}
};
const query = new URLSearchParams(window.location.search);
if (query.has("edit")) {
onEdit(undefined, false);
} else if (query.has("new")) {
onNewNote(undefined, false);
}
window.addEventListener("popstate", (e) => {
if (layout.style.display !== "none" && !e.state?.isEditor) {
performDOMClose();
}
});
wrapper.addEventListener("click", onWrapperClick);
editBtn.addEventListener("click", onEdit);
newBtn.addEventListener("click", onNewNote);
cancelBtn?.addEventListener("click", onCancel);
saveBtn?.addEventListener("click", onSave);
deleteBtn?.addEventListener("click", onDelete);
window.addCleanup(() => {
wrapper.removeEventListener("click", onWrapperClick);
editBtn.removeEventListener("click", onEdit);
newBtn.removeEventListener("click", onNewNote);
cancelBtn?.removeEventListener("click", onCancel);
saveBtn?.removeEventListener("click", onSave);
deleteBtn?.removeEventListener("click", onDelete);
if (editorView) editorView.destroy();
});
});

View file

@ -111,6 +111,10 @@ function createFolderNode(
const folderPath = node.slug
folderContainer.dataset.folderpath = folderPath
if (currentSlug === folderPath) {
folderContainer.classList.add("active")
}
if (opts.folderClickBehavior === "link") {
// Replace button with link for link behavior
const button = titleContainer.querySelector(".folder-button") as HTMLElement

View file

@ -102,7 +102,7 @@ async function _navigate(url: URL, isBack: boolean = false) {
html.body.appendChild(announcer)
// morph body
micromorph(document.body, html.body)
await micromorph(document.body, html.body)
// scroll into place and add history
if (!isBack) {

View file

@ -6,6 +6,7 @@
& > :not(.sidebar.left:has(.explorer)) {
transition: transform 300ms ease-in-out;
}
&.lock-scroll > :not(.sidebar.left:has(.explorer)) {
transform: translateX(100dvw);
transition: transform 300ms ease-in-out;
@ -33,8 +34,10 @@
min-height: 1.2rem;
flex: 0 1 auto;
&.collapsed {
flex: 0 1 1.2rem;
& .fold {
transform: rotateZ(-90deg);
}
@ -118,7 +121,10 @@ button.desktop-explorer {
list-style: none;
margin: 0;
padding: 0;
&.explorer-ul {
overscroll-behavior: contain;
}
& li > a {
color: var(--dark);
@ -269,6 +275,8 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
.mobile-no-scroll {
@media all and ($mobile) {
overscroll-behavior: none;
.explorer-content > .explorer-ul {
overscroll-behavior: contain;
}
}
}

View file

@ -23,7 +23,16 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
name: "Citations",
htmlPlugins(ctx) {
const plugins: PluggableList = []
// per default, rehype-citations only supports en-US
// see: https://github.com/timlrx/rehype-citation/issues/12
// in here there are multiple usable locales:
// https://github.com/citation-style-language/locales
// thus, we optimistically assume there is indeed an appropriate
// locale available and simply create the lang url-string
let lang: string = "en-US"
if (ctx.cfg.configuration.locale !== "en-US") {
lang = `https://raw.githubusercontent.com/citation-stylelanguage/locales/refs/heads/master/locales-${ctx.cfg.configuration.locale}.xml`
}
// Add rehype-citation to the list of plugins
plugins.push([
rehypeCitation,
@ -32,7 +41,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
suppressBibliography: opts.suppressBibliography,
linkCitations: opts.linkCitations,
csl: opts.csl,
lang: ctx.cfg.configuration.locale ?? "en-US",
lang,
},
])

View file

@ -9,6 +9,10 @@ html {
text-size-adjust: none;
overflow-x: hidden;
width: 100vw;
@media all and ($mobile) {
scroll-padding-top: 4rem;
}
}
body {