mirror of
https://github.com/andwati/andwati.com.git
synced 2026-10-18 09:22:53 +00:00
Compare commits
2
Commits
d958a298f7
...
12dd32ea4f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12dd32ea4f
|
||
|
|
ffc3b6f353
|
@@ -0,0 +1,25 @@
|
||||
name: Lighthouse
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, astro-migration]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lighthouse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile --filter site
|
||||
|
||||
- name: Run Lighthouse CI
|
||||
run: pnpm --filter site run lighthouse
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"snyk.advanced.autoSelectOrganization": true
|
||||
}
|
||||
@@ -60,6 +60,21 @@ CONTENT_GIT_COMMIT_NAME="andwati.com CMS"
|
||||
[email protected]
|
||||
```
|
||||
|
||||
Site analytics (read at build time by `apps/site`, so these must be set on
|
||||
the `site` service before it builds, not just at runtime):
|
||||
|
||||
```dotenv
|
||||
PUBLIC_UMAMI_SCRIPT_URL=https://analytics.andwati.com/script.js
|
||||
PUBLIC_UMAMI_WEBSITE_ID=9eef8fba-c932-4488-ae23-6b84553e8c29
|
||||
```
|
||||
|
||||
`Analytics.astro` silently no-ops if either is unset — safe to deploy without
|
||||
them, but analytics won't record anything until both are present at build
|
||||
time. `sws.toml`'s CSP already allows `analytics.andwati.com` and
|
||||
`utteranc.es` (comments); if the Umami host ever changes, update both that
|
||||
CSP and these two vars together, or the tracker will be silently blocked in
|
||||
production instead of erroring visibly.
|
||||
|
||||
Generate each random secret independently; do not reuse the local development
|
||||
values from `docker-compose.yml`.
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
.PHONY: all build
|
||||
|
||||
BUILD_DIR := public
|
||||
|
||||
all: clean build
|
||||
|
||||
clean:
|
||||
rm -rfd ./$(BUILD_DIR)
|
||||
|
||||
build:
|
||||
node scripts/generate-llms.mjs
|
||||
zola build
|
||||
|
||||
serve:
|
||||
node scripts/generate-llms.mjs
|
||||
zola serve
|
||||
|
||||
update-zola:
|
||||
@./bin/update-zola
|
||||
@@ -134,18 +134,21 @@ approved plan for full rationale on each decision referenced below.
|
||||
- [x] Pagefind integration — indexes the build output (`pnpm build` runs
|
||||
`astro build && pagefind --site dist`), `/search/` page using
|
||||
Pagefind's default UI; verified with a real query in a browser
|
||||
- [ ] Webmentions integration (replacing Utterances) — **plumbing only**:
|
||||
`Base.astro` links `rel="webmention"` to a webmention.io endpoint for
|
||||
`andwati.com`, but nothing displays received mentions yet, and the
|
||||
endpoint won't receive anything until the domain is actually
|
||||
registered/verified on webmention.io (**needs your input**: create an
|
||||
account, verify domain ownership, then I can build the display side)
|
||||
- [x] Comments: kept Utterances (not Webmentions) — `Comments.astro`, wired
|
||||
to `andwati/andwati.com`, with the same dark/light theme-sync-via-
|
||||
postMessage behavior the legacy site had, adapted to this site's
|
||||
`data-theme` attribute. The `rel="webmention"` link was removed since
|
||||
it's unused now. `sws.toml`'s CSP updated to allow `utteranc.es`
|
||||
(script-src, frame-src) — without that the widget is silently blocked
|
||||
in production
|
||||
|
||||
## Analytics
|
||||
|
||||
- [ ] Stand up Plausible or Umami on the Dokploy host, wire up tracking
|
||||
snippet (replacing Google Analytics) — **needs your input**: requires
|
||||
your Dokploy access, not something I can provision from the repo
|
||||
- [x] Umami — self-hosted at `analytics.andwati.com`. `Analytics.astro`
|
||||
renders the tracker script from `PUBLIC_UMAMI_SCRIPT_URL` +
|
||||
`PUBLIC_UMAMI_WEBSITE_ID` (set in `apps/site/.env`, gitignored — copy
|
||||
into the production environment's env vars too); `sws.toml`'s CSP
|
||||
updated to allow that host in `script-src`/`connect-src`
|
||||
|
||||
## Migration from the legacy Zola site
|
||||
|
||||
@@ -192,5 +195,16 @@ approved plan for full rationale on each decision referenced below.
|
||||
## Cleanup
|
||||
|
||||
- [ ] Remove Zola-specific files (`zola.toml`, `templates/`, `sass/`,
|
||||
`themes/`, `content/posts/`, legacy OG/llms scripts) once each has a
|
||||
working Astro/Strapi equivalent — progressively, not all at once
|
||||
`themes/`, `static/`, `Makefile`, `bin/update-zola`, `content/posts/`,
|
||||
`content/archive/_index.md`, `content/about.md`, `content/_index.md`,
|
||||
legacy OG/llms scripts) — verified safe: root `Dockerfile` already
|
||||
builds `apps/site` (not `zola build`), nothing in `apps/site` reads
|
||||
those content files, and `content/writings/` has all 20 posts from
|
||||
`content/posts/`. Root `package.json`'s `legacy:*` scripts already
|
||||
removed. **Blocked**: the actual `git rm` was refused by the
|
||||
permission system as irreversible destruction — run it yourself (fully
|
||||
reversible via git history/revert since these are tracked files):
|
||||
`git rm -r templates/ sass/ themes/ static/ zola.toml Makefile bin/
|
||||
content/posts/ content/archive/_index.md content/about.md
|
||||
content/_index.md scripts/generate-llms.mjs
|
||||
scripts/generate-og-images.mjs`
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Self-hosted Umami analytics (https://umami.is/docs/tracker-configuration).
|
||||
# Leave unset to disable analytics entirely — Analytics.astro no-ops until
|
||||
# both of these are present. Fill in once the Umami instance is deployed.
|
||||
PUBLIC_UMAMI_SCRIPT_URL=
|
||||
PUBLIC_UMAMI_WEBSITE_ID=
|
||||
|
||||
# Optional: restrict tracking to these comma-separated domains (Umami's
|
||||
# data-domains attribute), useful if the same build is deployed to a
|
||||
# staging domain and you only want production traffic counted.
|
||||
PUBLIC_UMAMI_DOMAINS=
|
||||
@@ -17,6 +17,9 @@ pnpm-debug.log*
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# Lighthouse CI report artifacts
|
||||
.lighthouseci/
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
// @ts-check
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import { defineConfig } from "astro/config";
|
||||
import { buildLastmodMap, resolveContentDir } from "./lib-config/sitemap-lastmod.mjs";
|
||||
import { siteConfig } from "./src/site.config.ts";
|
||||
|
||||
const lastmodMap = buildLastmodMap(resolveContentDir(import.meta.url));
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: siteConfig.baseUrl,
|
||||
integrations: [sitemap()],
|
||||
integrations: [
|
||||
sitemap({
|
||||
// Every build re-crawls the actual rendered output, so the sitemap's
|
||||
// URL list always matches whatever pages/content exist right now — no
|
||||
// separate "regenerate the sitemap" step needed when content changes.
|
||||
// On top of that, <lastmod> is set from each entry's own
|
||||
// date/updated field (falling back to build time when unmapped),
|
||||
// instead of every URL just getting "whenever the site was last
|
||||
// built".
|
||||
serialize(item) {
|
||||
const lastmod = lastmodMap.get(new URL(item.url).pathname);
|
||||
return lastmod ? { ...item, lastmod } : item;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Builds a Map<pathname, ISO date string> from content/ frontmatter so the
|
||||
// sitemap's <lastmod> reflects real edit dates instead of just "whenever the
|
||||
// site was last built". Runs from astro.config.mjs, which executes as plain
|
||||
// Node outside Vite/Astro's content-layer pipeline (same reason
|
||||
// scripts/generate-llms.astro-site.mjs parses frontmatter by hand instead of
|
||||
// importing astro:content) — duplicated here rather than shared because the
|
||||
// two scripts' output shapes don't overlap enough to be worth a shared module.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
|
||||
|
||||
function walk(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return walk(full);
|
||||
return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function slugFor(dir, file) {
|
||||
const rel = path.relative(dir, file).split(path.sep).join("/");
|
||||
return rel.endsWith("/index.md") ? rel.slice(0, -"/index.md".length) : rel.slice(0, -".md".length);
|
||||
}
|
||||
|
||||
function readEntries(typeDir) {
|
||||
return walk(typeDir)
|
||||
.map((file) => {
|
||||
const raw = fs.readFileSync(file, "utf8");
|
||||
const match = raw.match(FRONTMATTER_RE);
|
||||
if (!match) return null;
|
||||
return { slug: slugFor(typeDir, file), frontmatter: parseToml(match[1]) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function isoOrNull(value) {
|
||||
if (!value) return null;
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(d.valueOf()) ? null : d.toISOString();
|
||||
}
|
||||
|
||||
export function buildLastmodMap(contentDir) {
|
||||
const map = new Map();
|
||||
|
||||
for (const entry of readEntries(path.join(contentDir, "writings"))) {
|
||||
if (entry.frontmatter.draft) continue;
|
||||
const lastmod = isoOrNull(entry.frontmatter.updated ?? entry.frontmatter.date);
|
||||
if (lastmod) map.set(`/writings/${entry.slug}/`, lastmod);
|
||||
}
|
||||
|
||||
for (const entry of readEntries(path.join(contentDir, "bookshelf"))) {
|
||||
const lastmod = isoOrNull(entry.frontmatter.date_finished ?? entry.frontmatter.date_started);
|
||||
if (lastmod) map.set(`/bookshelf/${entry.slug}/`, lastmod);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export function resolveContentDir(configFileUrl) {
|
||||
return path.join(path.dirname(fileURLToPath(configFileUrl)), "..", "..", "content");
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Lighthouse CI — runs the same audits PageSpeed Insights reports on
|
||||
* (performance, accessibility, best-practices, SEO) against the built site,
|
||||
* so results can be tracked over time / per-commit without depending on the
|
||||
* hosted PSI API. `staticDistDir` lets lhci serve `dist/` itself, no dev
|
||||
* server juggling needed. Reporting only for now — no `assert` thresholds,
|
||||
* so a regression shows up in the report without failing the build.
|
||||
*/
|
||||
module.exports = {
|
||||
ci: {
|
||||
collect: {
|
||||
staticDistDir: "./dist",
|
||||
url: [
|
||||
"/index.html",
|
||||
"/writings/index.html",
|
||||
"/writings/from-c-to-machine-code/index.html",
|
||||
"/archive/index.html",
|
||||
"/portfolio/index.html",
|
||||
"/bookshelf/index.html",
|
||||
"/tags/index.html",
|
||||
"/about/index.html",
|
||||
],
|
||||
numberOfRuns: 3,
|
||||
},
|
||||
upload: {
|
||||
// Free, no account/server needed — each run prints a shareable report
|
||||
// link (expires after 7 days). Point this at a real LHCI server later
|
||||
// if longer-lived historical tracking is wanted.
|
||||
target: "temporary-public-storage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Desktop form-factor pass — same pages/config as lighthouserc.cjs, but
|
||||
// Lighthouse's default (and PageSpeed Insights' default) is mobile: throttled
|
||||
// CPU/network and a narrow viewport. Auditing desktop too means neither form
|
||||
// factor's numbers are a blind spot.
|
||||
const mobileConfig = require("./lighthouserc.cjs");
|
||||
|
||||
module.exports = {
|
||||
ci: {
|
||||
...mobileConfig.ci,
|
||||
collect: {
|
||||
...mobileConfig.ci.collect,
|
||||
settings: { preset: "desktop" },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -12,7 +12,8 @@
|
||||
"build": "pnpm run generate:llms && pnpm run generate:og && astro build && pagefind --site dist",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"astro": "astro"
|
||||
"astro": "astro",
|
||||
"lighthouse": "pnpm run build && lhci autorun && lhci autorun --config=lighthouserc.desktop.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^4.0.19",
|
||||
@@ -22,7 +23,9 @@
|
||||
"@fontsource/jetbrains-mono": "^5.3.0",
|
||||
"astro": "^7.2.9",
|
||||
"highlight.js": "^11.12.0",
|
||||
"katex": "^0.18.7",
|
||||
"marked": "^18.0.11",
|
||||
"marked-katex-extension": "^5.1.12",
|
||||
"pagefind": "^1.5.2",
|
||||
"smol-toml": "^1.8.0",
|
||||
"three": "^0.185.1"
|
||||
@@ -32,6 +35,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@types/three": "^0.185.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 2.9 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 749 B |
@@ -26,7 +26,6 @@
|
||||
- [The One Thing Nobody Tells You About WiFi Hotspots on Arch Linux](https://andwati.com/writings/wifi-hotspot-arch/): A minimal Arch install is great until NetworkManager silently fails to find a binary you didn't know you needed
|
||||
- [Setting up Obsidian sync in Windows](https://andwati.com/writings/obsidian-sync/): Syncing Obsidian in Windows with Windows Task Scheduler with git
|
||||
- [The Single Byte That Kills Your Exploit: Understanding Endianness](https://andwati.com/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/): Problem: Computers speak different languages, like people
|
||||
- [Why This Site Exists](https://andwati.com/writings/example-first-post/): A short note on what this new site is, and why it's more than a tech blog.
|
||||
- [mmuctf 2025 writeup](https://andwati.com/writings/mmuctf2025/): mmuctf 2025 challenges writeup
|
||||
- [Configure Vue.js app With Tailwind CSS](https://andwati.com/writings/vuejs-tailwind/): Tailwindcss in vuejs
|
||||
- [Subresource Integrity](https://andwati.com/writings/subresource-integrity/): Understanding Subresource Integrity
|
||||
|
||||
@@ -1,4 +1,45 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# AI assistants / agents / LLM training crawlers — explicitly welcome.
|
||||
# See /llms.txt for a crawler-friendly summary of this site's content.
|
||||
User-agent: GPTBot
|
||||
Allow: /
|
||||
|
||||
User-agent: ChatGPT-User
|
||||
Allow: /
|
||||
|
||||
User-agent: OAI-SearchBot
|
||||
Allow: /
|
||||
|
||||
User-agent: ClaudeBot
|
||||
Allow: /
|
||||
|
||||
User-agent: Claude-Web
|
||||
Allow: /
|
||||
|
||||
User-agent: anthropic-ai
|
||||
Allow: /
|
||||
|
||||
User-agent: PerplexityBot
|
||||
Allow: /
|
||||
|
||||
User-agent: Perplexity-User
|
||||
Allow: /
|
||||
|
||||
User-agent: Google-Extended
|
||||
Allow: /
|
||||
|
||||
User-agent: Applebot-Extended
|
||||
Allow: /
|
||||
|
||||
User-agent: CCBot
|
||||
Allow: /
|
||||
|
||||
User-agent: Bytespider
|
||||
Allow: /
|
||||
|
||||
User-agent: Amazonbot
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://andwati.com/sitemap-index.xml
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
/**
|
||||
* Self-hosted Umami analytics. No-ops until PUBLIC_UMAMI_SCRIPT_URL and
|
||||
* PUBLIC_UMAMI_WEBSITE_ID are set (see .env.example) — fill those in once
|
||||
* the Umami instance is deployed, no code changes needed.
|
||||
*/
|
||||
const scriptUrl = import.meta.env.PUBLIC_UMAMI_SCRIPT_URL;
|
||||
const websiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID;
|
||||
const domains = import.meta.env.PUBLIC_UMAMI_DOMAINS;
|
||||
---
|
||||
|
||||
{
|
||||
scriptUrl && websiteId && (
|
||||
<script
|
||||
defer
|
||||
src={scriptUrl}
|
||||
data-website-id={websiteId}
|
||||
data-domains={domains || undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -136,11 +136,16 @@ const { items, activeSlug } = Astro.props;
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll<HTMLElement>("[data-spines]").forEach((root) => {
|
||||
const track = root.querySelector<HTMLElement>("[data-track]");
|
||||
const next = root.querySelector<HTMLButtonElement>("[data-next]");
|
||||
next?.addEventListener("click", () => {
|
||||
track?.scrollBy({ left: 320, behavior: "smooth" });
|
||||
// astro:page-load fires on first load and after every client-side
|
||||
// navigation (bookshelf index <-> detail pages both render this
|
||||
// component), so the button keeps working once view transitions swap it in.
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
document.querySelectorAll<HTMLElement>("[data-spines]").forEach((root) => {
|
||||
const track = root.querySelector<HTMLElement>("[data-track]");
|
||||
const next = root.querySelector<HTMLButtonElement>("[data-next]");
|
||||
next?.addEventListener("click", () => {
|
||||
track?.scrollBy({ left: 320, behavior: "smooth" });
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
const repo = "andwati/andwati.com";
|
||||
const themeDark = "gruvbox-dark";
|
||||
const themeLight = "github-light";
|
||||
---
|
||||
|
||||
<div
|
||||
class="comments"
|
||||
id="comments"
|
||||
data-utterances-theme-dark={themeDark}
|
||||
data-utterances-theme-light={themeLight}
|
||||
>
|
||||
<script
|
||||
src="https://utteranc.es/client.js"
|
||||
repo={repo}
|
||||
issue-term="pathname"
|
||||
theme={themeDark}
|
||||
crossorigin="anonymous"
|
||||
async
|
||||
></script>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.comments {
|
||||
margin-top: var(--space-8);
|
||||
padding-top: var(--space-6);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Utterances only reads its `theme` attribute once at load, so later
|
||||
// theme-toggle clicks must be relayed through postMessage instead.
|
||||
function isDark() {
|
||||
const explicit = document.documentElement.getAttribute("data-theme");
|
||||
if (explicit === "dark" || explicit === "light") return explicit === "dark";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
}
|
||||
|
||||
function syncUtterancesTheme() {
|
||||
const commentsEl = document.getElementById("comments");
|
||||
const frame = document.querySelector<HTMLIFrameElement>(".utterances-frame");
|
||||
if (!commentsEl || !frame || !frame.contentWindow) return;
|
||||
const theme = isDark()
|
||||
? commentsEl.dataset.utterancesThemeDark
|
||||
: commentsEl.dataset.utterancesThemeLight;
|
||||
try {
|
||||
frame.contentWindow.postMessage({ type: "set-theme", theme }, "https://utteranc.es");
|
||||
} catch {
|
||||
// The iframe briefly shares our origin before navigating to
|
||||
// utteranc.es, so posting can throw until that navigation finishes.
|
||||
}
|
||||
}
|
||||
|
||||
// Each navigation to a new article gets a fresh observer/poller — without
|
||||
// disconnecting the previous page's, they'd accumulate across client-side
|
||||
// navigations since document.documentElement persists across view
|
||||
// transitions.
|
||||
let observer: MutationObserver | undefined;
|
||||
let poll: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
function initComments() {
|
||||
observer?.disconnect();
|
||||
if (poll) clearInterval(poll);
|
||||
|
||||
if (!document.getElementById("comments")) return;
|
||||
|
||||
observer = new MutationObserver(syncUtterancesTheme);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
|
||||
let tries = 0;
|
||||
poll = setInterval(() => {
|
||||
if (document.querySelector(".utterances-frame") || ++tries > 20) {
|
||||
clearInterval(poll);
|
||||
syncUtterancesTheme();
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
document.addEventListener("astro:page-load", initComments);
|
||||
</script>
|
||||
@@ -52,19 +52,46 @@ const graph = await buildConstellationGraph();
|
||||
|
||||
<script>
|
||||
import type { ConstellationGraph } from "../lib/constellation-data";
|
||||
import { mountConstellation, supportsWebGL } from "../lib/constellation-scene";
|
||||
import { supportsWebGL } from "../lib/webgl-support";
|
||||
|
||||
const root = document.querySelector<HTMLElement>("[data-constellation]");
|
||||
const canvas = root?.querySelector<HTMLElement>("[data-canvas]");
|
||||
const tooltip = root?.querySelector<HTMLElement>("[data-tooltip]");
|
||||
const dataEl = document.getElementById("constellation-data");
|
||||
// A plain top-level mount only ever runs once per module URL, so
|
||||
// navigating away from "/" and back via a view transition would leave the
|
||||
// canvas blank. astro:page-load fires on first load and every subsequent
|
||||
// client-side navigation to "/", and the stored dispose() tears down the
|
||||
// previous mount's WebGL context/listeners/rAF loop first.
|
||||
let dispose: (() => void) | undefined;
|
||||
|
||||
if (root && canvas && tooltip && dataEl?.textContent && supportsWebGL()) {
|
||||
const graph: ConstellationGraph = JSON.parse(dataEl.textContent);
|
||||
if (graph.nodes.length > 0) {
|
||||
mountConstellation(canvas, tooltip, graph);
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
dispose?.();
|
||||
dispose = undefined;
|
||||
|
||||
const root = document.querySelector<HTMLElement>("[data-constellation]");
|
||||
const canvas = root?.querySelector<HTMLElement>("[data-canvas]");
|
||||
const tooltip = root?.querySelector<HTMLElement>("[data-tooltip]");
|
||||
const dataEl = document.getElementById("constellation-data");
|
||||
if (!root) return;
|
||||
|
||||
if (!(canvas && tooltip && dataEl?.textContent && supportsWebGL())) {
|
||||
root.style.display = "none";
|
||||
return;
|
||||
}
|
||||
} else if (root) {
|
||||
root.style.display = "none";
|
||||
}
|
||||
const graph: ConstellationGraph = JSON.parse(dataEl.textContent);
|
||||
if (graph.nodes.length === 0) {
|
||||
root.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
// three.js (~130KB gzip) is dynamically imported here instead of at the
|
||||
// top of this module, and only once the browser is idle — this is
|
||||
// decorative, so it must never compete with first paint / LCP for main-
|
||||
// thread time. Falls back to a short timeout on browsers without
|
||||
// requestIdleCallback (Safari).
|
||||
const schedule =
|
||||
window.requestIdleCallback ?? ((cb: () => void) => window.setTimeout(cb, 200));
|
||||
schedule(async () => {
|
||||
const { mountConstellation } = await import("../lib/constellation-scene");
|
||||
dispose = mountConstellation(canvas, tooltip, graph);
|
||||
root.style.display = "";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div class="minimap" aria-hidden="true">
|
||||
<div class="minimap-content"></div>
|
||||
<div class="minimap-viewport"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.minimap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 90rem) {
|
||||
.minimap {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: var(--space-6);
|
||||
bottom: var(--space-6);
|
||||
right: var(--space-5);
|
||||
width: 9rem;
|
||||
z-index: 10;
|
||||
cursor: grab;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.minimap:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.minimap-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
filter: saturate(0.65);
|
||||
}
|
||||
|
||||
.minimap-content :is(h1, h2, h3) {
|
||||
text-decoration-thickness: 0.18em;
|
||||
}
|
||||
|
||||
.minimap-viewport {
|
||||
position: absolute;
|
||||
left: -0.4rem;
|
||||
right: -0.4rem;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent-muted);
|
||||
border-radius: var(--radius-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function initMinimap(signal: AbortSignal) {
|
||||
const minimap = document.querySelector(".minimap");
|
||||
const source = document.querySelector(".article-main");
|
||||
if (!(minimap instanceof HTMLElement) || !(source instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = minimap.querySelector(".minimap-content");
|
||||
const viewport = minimap.querySelector(".minimap-viewport");
|
||||
if (!(content instanceof HTMLElement) || !(viewport instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clone = source.cloneNode(true) as HTMLElement;
|
||||
clone
|
||||
.querySelectorAll("script, [id]")
|
||||
.forEach((el) => el.removeAttribute("id"));
|
||||
clone.querySelectorAll("script").forEach((el) => el.remove());
|
||||
clone
|
||||
.querySelectorAll("a, button, input, select, textarea")
|
||||
.forEach((el) => el.setAttribute("tabindex", "-1"));
|
||||
content.replaceChildren(clone);
|
||||
content.style.width = `${source.offsetWidth}px`;
|
||||
|
||||
let scale = 1;
|
||||
let sourceTop = 0;
|
||||
|
||||
function layout() {
|
||||
sourceTop = source.getBoundingClientRect().top + window.scrollY;
|
||||
const sourceHeight = source.scrollHeight;
|
||||
const mapHeight = minimap.clientHeight;
|
||||
scale = Math.min(mapHeight / sourceHeight, 1);
|
||||
content.style.transform = `scale(${scale})`;
|
||||
update();
|
||||
}
|
||||
|
||||
function update() {
|
||||
const maxTop = minimap.clientHeight - viewport.offsetHeight;
|
||||
const top = (window.scrollY - sourceTop) * scale;
|
||||
viewport.style.height = `${window.innerHeight * scale}px`;
|
||||
viewport.style.top = `${Math.max(0, Math.min(top, Math.max(maxTop, 0)))}px`;
|
||||
}
|
||||
|
||||
function seek(clientY: number) {
|
||||
const rect = minimap.getBoundingClientRect();
|
||||
const y = clientY - rect.top;
|
||||
const target = sourceTop + y / scale - window.innerHeight / 2;
|
||||
window.scrollTo({ top: target, behavior: "auto" });
|
||||
}
|
||||
|
||||
let dragging = false;
|
||||
|
||||
minimap.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
dragging = true;
|
||||
minimap.setPointerCapture(e.pointerId);
|
||||
seek(e.clientY);
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
minimap.addEventListener(
|
||||
"pointermove",
|
||||
(e) => {
|
||||
if (dragging) seek(e.clientY);
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
minimap.addEventListener("pointerup", () => (dragging = false), { signal });
|
||||
|
||||
let ticking = false;
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
update();
|
||||
ticking = false;
|
||||
});
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
window.addEventListener("resize", layout, { signal });
|
||||
|
||||
layout();
|
||||
}
|
||||
|
||||
// Each navigation gets a fresh controller so the previous page's
|
||||
// window-level scroll/resize listeners are torn down before the new
|
||||
// page's initMinimap() adds its own — otherwise they'd accumulate across
|
||||
// client-side navigations between article pages.
|
||||
let controller: AbortController | undefined;
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
controller?.abort();
|
||||
controller = new AbortController();
|
||||
initMinimap(controller.signal);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div class="reading-progress-container" aria-hidden="true">
|
||||
<div class="reading-progress-bar" id="reading-progress-bar"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.reading-progress-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.reading-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: var(--accent);
|
||||
transition: width 0.1s var(--ease-out);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function initReadingProgress(signal: AbortSignal) {
|
||||
const bar = document.getElementById("reading-progress-bar");
|
||||
if (!(bar instanceof HTMLElement)) return;
|
||||
|
||||
const update = () => {
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
|
||||
const clientHeight = document.documentElement.clientHeight || window.innerHeight;
|
||||
const total = scrollHeight - clientHeight;
|
||||
bar.style.width = `${total > 0 ? (scrollTop / total) * 100 : 0}%`;
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
let ticking = false;
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
update();
|
||||
ticking = false;
|
||||
});
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
|
||||
let controller: AbortController | undefined;
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
controller?.abort();
|
||||
controller = new AbortController();
|
||||
initReadingProgress(controller.signal);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
---
|
||||
|
||||
<button id="scroll-to-top" class="scroll-to-top" aria-label="Scroll to top" type="button">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
bottom: var(--space-6);
|
||||
right: var(--space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--accent);
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(0.75rem);
|
||||
transition:
|
||||
transform 0.25s var(--ease-out),
|
||||
opacity 0.25s var(--ease-out),
|
||||
visibility 0.25s var(--ease-out),
|
||||
background-color 0.2s var(--ease-out),
|
||||
color 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.scroll-to-top.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.scroll-to-top:hover {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-to-top:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
.scroll-to-top {
|
||||
bottom: var(--space-5);
|
||||
right: var(--space-5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function initScrollToTop(signal: AbortSignal) {
|
||||
const btn = document.getElementById("scroll-to-top");
|
||||
if (!(btn instanceof HTMLElement)) return;
|
||||
|
||||
const toggleVisibility = () => {
|
||||
btn.classList.toggle("visible", window.scrollY > 400);
|
||||
};
|
||||
|
||||
btn.addEventListener(
|
||||
"click",
|
||||
() => window.scrollTo({ top: 0, behavior: "smooth" }),
|
||||
{ signal },
|
||||
);
|
||||
|
||||
let ticking = false;
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
toggleVisibility();
|
||||
ticking = false;
|
||||
});
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
toggleVisibility();
|
||||
}
|
||||
|
||||
// astro:page-load fires on first load and after every client-side
|
||||
// navigation. Each firing gets a fresh AbortController so the previous
|
||||
// page's window-level scroll listener is torn down first, instead of
|
||||
// accumulating across navigations.
|
||||
let controller: AbortController | undefined;
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
controller?.abort();
|
||||
controller = new AbortController();
|
||||
initScrollToTop(controller.signal);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div id="search-overlay" class="search-overlay" aria-hidden="true">
|
||||
<div class="search-modal" role="dialog" aria-modal="true" aria-label="Search">
|
||||
<div id="search-mount"></div>
|
||||
<p class="search-empty-hint meta">
|
||||
Search only works against a built site (`pnpm build` runs Pagefind after `astro build`) — it
|
||||
has no index to query in `astro dev`.
|
||||
</p>
|
||||
<div class="search-footer">
|
||||
<span>Navigate: <kbd>↑</kbd> <kbd>↓</kbd></span>
|
||||
<span>Select: <kbd>Enter</kbd></span>
|
||||
<span>Close: <kbd>Esc</kbd></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: var(--space-7) var(--space-5);
|
||||
background: color-mix(in srgb, var(--bg) 70%, transparent);
|
||||
backdrop-filter: blur(4px);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.2s var(--ease-out),
|
||||
visibility 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.search-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
width: 100%;
|
||||
max-width: 38rem;
|
||||
max-height: calc(100vh - 2 * var(--space-7));
|
||||
overflow-y: auto;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-5);
|
||||
transform: translateY(-0.5rem);
|
||||
transition: transform 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.search-overlay.active .search-modal {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.search-empty-hint {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-footer {
|
||||
display: none;
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border);
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-overlay.active .search-footer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-footer kbd {
|
||||
font-family: inherit;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0 0.35em;
|
||||
}
|
||||
|
||||
/* Pagefind UI theming, mapped onto this site's tokens. */
|
||||
#search-mount {
|
||||
--pagefind-ui-scale: 0.9;
|
||||
--pagefind-ui-primary: var(--text);
|
||||
--pagefind-ui-text: var(--text);
|
||||
--pagefind-ui-background: transparent;
|
||||
--pagefind-ui-border: var(--border);
|
||||
--pagefind-ui-tag: var(--bg);
|
||||
--pagefind-ui-border-width: 1px;
|
||||
--pagefind-ui-border-radius: 6px;
|
||||
--pagefind-ui-font: var(--font-body);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let pagefindLoaded = false;
|
||||
let pagefindLoading: Promise<void> | undefined;
|
||||
|
||||
function loadPagefind(): Promise<void> {
|
||||
if (pagefindLoaded) return Promise.resolve();
|
||||
if (pagefindLoading) return pagefindLoading;
|
||||
|
||||
pagefindLoading = new Promise((resolve) => {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/pagefind/pagefind-ui.css";
|
||||
document.head.appendChild(link);
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "/pagefind/pagefind-ui.js";
|
||||
script.onload = () => {
|
||||
const mount = document.getElementById("search-mount");
|
||||
const hint = document.querySelector(".search-empty-hint");
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: loaded from /pagefind/pagefind-ui.js
|
||||
if (mount && typeof PagefindUI !== "undefined") {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: loaded from /pagefind/pagefind-ui.js
|
||||
new PagefindUI({ element: mount, showSubResults: true, showImages: false });
|
||||
hint?.remove();
|
||||
}
|
||||
pagefindLoaded = true;
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => resolve();
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
return pagefindLoading;
|
||||
}
|
||||
|
||||
function initSearchModal(signal: AbortSignal) {
|
||||
const overlay = document.getElementById("search-overlay");
|
||||
const toggle = document.getElementById("search-toggle");
|
||||
if (!overlay || !toggle) return;
|
||||
|
||||
const isOpen = () => overlay.classList.contains("active");
|
||||
|
||||
const open = () => {
|
||||
overlay.classList.add("active");
|
||||
overlay.setAttribute("aria-hidden", "false");
|
||||
document.body.style.overflow = "hidden";
|
||||
loadPagefind().then(() => {
|
||||
overlay.querySelector<HTMLInputElement>(".pagefind-ui__search-input")?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
overlay.classList.remove("active");
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
|
||||
toggle.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
isOpen() ? close() : open();
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
overlay.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
if (e.target === overlay) close();
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
isOpen() ? close() : open();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape" && isOpen()) close();
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
|
||||
let controller: AbortController | undefined;
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
controller?.abort();
|
||||
controller = new AbortController();
|
||||
initSearchModal(controller.signal);
|
||||
});
|
||||
</script>
|
||||
+88
-48
@@ -1,45 +1,44 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const { title, url } = Astro.props;
|
||||
const encodedTitle = encodeURIComponent(title);
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
---
|
||||
|
||||
<div class="social-share">
|
||||
<span class="share-label">{{ config.extra.share_label | default(value="Share:") }}</span>
|
||||
<span class="share-label">Share:</span>
|
||||
<a
|
||||
href="https://twitter.com/intent/tweet?text={{ page.title | urlencode }}&url={{ page.permalink | urlencode }}"
|
||||
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-btn x"
|
||||
aria-label="Share on X"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="currentColor"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||
<path
|
||||
d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"
|
||||
/>
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://bsky.app/intent/compose?text={{ page.title | urlencode }}%20{{ page.permalink | urlencode }}"
|
||||
href={`https://bsky.app/intent/compose?text=${encodedTitle}%20${encodedUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-btn bluesky"
|
||||
aria-label="Share on Bluesky"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="currentColor"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||
<path
|
||||
d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"
|
||||
/>
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/shareArticle?mini=true&url={{ page.permalink | urlencode }}&title={{ page.title | urlencode }}"
|
||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-btn linkedin"
|
||||
@@ -56,56 +55,36 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
|
||||
></path>
|
||||
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
|
||||
<rect x="2" y="9" width="4" height="12"></rect>
|
||||
<circle cx="4" cy="4" r="2"></circle>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://reddit.com/submit?url={{ page.permalink | urlencode }}&title={{ page.title | urlencode }}"
|
||||
href={`https://reddit.com/submit?url=${encodedUrl}&title=${encodedTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-btn reddit"
|
||||
aria-label="Share on Reddit"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
|
||||
/>
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://news.ycombinator.com/submitlink?u={{ page.permalink | urlencode }}&t={{ page.title | urlencode }}"
|
||||
href={`https://news.ycombinator.com/submitlink?u=${encodedUrl}&t=${encodedTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-btn hackernews"
|
||||
aria-label="Share on Hacker News"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M0 24V0h24v24H0zM6.95 5.55L11 13v5h2v-5l4.05-7.45h-2.25L12 10.65 9.2 5.55H6.95z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||
<path d="M0 24V0h24v24H0zM6.95 5.55L11 13v5h2v-5l4.05-7.45h-2.25L12 10.65 9.2 5.55H6.95z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="mailto:?subject={{ page.title | urlencode }}&body={{ page.permalink | urlencode }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-btn email"
|
||||
aria-label="Share via Email"
|
||||
>
|
||||
<a href={`mailto:?subject=${encodedTitle}&body=${encodedUrl}`} class="share-btn email" aria-label="Share via Email">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -122,3 +101,64 @@
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.social-share {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.share-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
transition:
|
||||
color 0.2s var(--ease-out),
|
||||
background-color 0.2s var(--ease-out),
|
||||
border-color 0.2s var(--ease-out),
|
||||
transform 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.share-btn:hover {
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.share-btn.x:hover {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.share-btn.bluesky:hover {
|
||||
background: #0285ff;
|
||||
}
|
||||
|
||||
.share-btn.linkedin:hover {
|
||||
background: #0077b5;
|
||||
}
|
||||
|
||||
.share-btn.reddit:hover {
|
||||
background: #ff4500;
|
||||
}
|
||||
|
||||
.share-btn.hackernews:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
.share-btn.email:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@ const writings = defineCollection({
|
||||
canonical_url: z.string().optional(),
|
||||
wordCount: z.number().optional(),
|
||||
readingMinutes: z.number().optional(),
|
||||
youtubeIds: z.array(z.string()).default([]),
|
||||
taxonomies,
|
||||
extra: z
|
||||
.object({
|
||||
|
||||
@@ -3,23 +3,64 @@ import "@fontsource-variable/fraunces";
|
||||
import "@fontsource-variable/newsreader";
|
||||
import "@fontsource/jetbrains-mono";
|
||||
import "../styles/global.css";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
import { copyrightYearRange } from "../lib/copyright-year";
|
||||
import { siteConfig } from "../site.config";
|
||||
import Analytics from "../components/Analytics.astro";
|
||||
import ScrollToTop from "../components/ScrollToTop.astro";
|
||||
import SearchModal from "../components/SearchModal.astro";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
/** Widens .container for dashboard-style layouts (currently just the homepage). */
|
||||
/** Narrows .container to the prose measure — opt in for pages with no wide layout needs. */
|
||||
wide?: boolean;
|
||||
/** Site-root-relative path to a per-page OG image, e.g. /images/og/writings/foo.png */
|
||||
ogImage?: string;
|
||||
/** Overrides the self-referential canonical (e.g. a writing first published elsewhere). */
|
||||
canonicalUrl?: string;
|
||||
/** Page type for the og:type meta — "article" for writings, "website" otherwise. */
|
||||
ogType?: "website" | "article";
|
||||
/** Extra JSON-LD object(s) beyond the sitewide WebSite/Person graph. */
|
||||
structuredData?: object | object[];
|
||||
}
|
||||
const { title, description, wide = false, ogImage } = Astro.props;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
wide = true,
|
||||
ogImage,
|
||||
canonicalUrl,
|
||||
ogType = "website",
|
||||
structuredData,
|
||||
} = Astro.props;
|
||||
const metaDescription = description ?? siteConfig.description;
|
||||
const currentPath = Astro.url.pathname;
|
||||
const copyrightRange = await copyrightYearRange();
|
||||
const pageTitle = `${title} — ${siteConfig.title}`;
|
||||
const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
|
||||
const ogImageUrl = new URL(ogImage ?? "/favicon.ico", siteConfig.baseUrl).href;
|
||||
const pageCanonicalUrl = canonicalUrl ?? new URL(currentPath, siteConfig.baseUrl).href;
|
||||
|
||||
const siteJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": `${siteConfig.baseUrl}/#website`,
|
||||
url: siteConfig.baseUrl,
|
||||
name: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
author: { "@id": `${siteConfig.baseUrl}/#person` },
|
||||
},
|
||||
{
|
||||
"@type": "Person",
|
||||
"@id": `${siteConfig.baseUrl}/#person`,
|
||||
name: siteConfig.author,
|
||||
url: siteConfig.baseUrl,
|
||||
sameAs: [siteConfig.social.github, siteConfig.social.twitter, siteConfig.social.youtube],
|
||||
},
|
||||
],
|
||||
};
|
||||
const extraJsonLd = structuredData ? (Array.isArray(structuredData) ? structuredData : [structuredData]) : [];
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
@@ -28,10 +69,13 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={metaDescription} />
|
||||
<link rel="canonical" href={pageCanonicalUrl} />
|
||||
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/rss.xml" />
|
||||
<link rel="alternate" type="application/atom+xml" title={siteConfig.title} href="/atom.xml" />
|
||||
<link rel="alternate" type="application/feed+json" title={siteConfig.title} href="/feed.json" />
|
||||
<link rel="webmention" href="https://webmention.io/andwati.com/webmention" />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:site_name" content={siteConfig.title} />
|
||||
<meta property="og:title" content={pageTitle} />
|
||||
<meta property="og:description" content={metaDescription} />
|
||||
<meta property="og:url" content={Astro.url.href} />
|
||||
@@ -40,8 +84,18 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={metaDescription} />
|
||||
<meta name="twitter:image" content={ogImageUrl} />
|
||||
<script is:inline>
|
||||
// Runs before paint to avoid a flash of the wrong theme.
|
||||
<script type="application/ld+json" set:html={JSON.stringify(siteJsonLd)} />
|
||||
{
|
||||
extraJsonLd.map((data) => (
|
||||
<script type="application/ld+json" set:html={JSON.stringify(data)} />
|
||||
))
|
||||
}
|
||||
<Analytics />
|
||||
<ClientRouter />
|
||||
<script is:inline data-astro-rerun>
|
||||
// Runs before paint to avoid a flash of the wrong theme. Also reruns
|
||||
// after every view-transition navigation, since Astro's head swap can
|
||||
// otherwise clobber the data-theme attribute this sets on <html>.
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem("theme");
|
||||
@@ -86,27 +140,46 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
|
||||
nav a[aria-current="page"] {
|
||||
color: var(--accent);
|
||||
}
|
||||
#theme-toggle {
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
transition:
|
||||
border-color 0.2s var(--ease-out),
|
||||
color 0.2s var(--ease-out);
|
||||
transition: color 0.2s var(--ease-out);
|
||||
}
|
||||
#theme-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
.icon-btn:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
.icon-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.moon-icon {
|
||||
display: block;
|
||||
}
|
||||
.sun-icon {
|
||||
display: none;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .sun-icon {
|
||||
display: block;
|
||||
}
|
||||
:root:not([data-theme="light"]) .moon-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] .sun-icon {
|
||||
display: block;
|
||||
}
|
||||
:root[data-theme="dark"] .moon-icon {
|
||||
display: none;
|
||||
}
|
||||
main {
|
||||
min-height: 60vh;
|
||||
}
|
||||
@@ -138,8 +211,28 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
|
||||
))
|
||||
}
|
||||
<a href="/tags/" aria-current={currentPath.startsWith("/tags/") ? "page" : undefined}>Tags</a>
|
||||
<a href="/search/" aria-current={currentPath.startsWith("/search/") ? "page" : undefined}>Search</a>
|
||||
<button id="theme-toggle" type="button" aria-label="Toggle color theme">◐</button>
|
||||
<button id="search-toggle" class="icon-btn" type="button" aria-label="Search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-btn" type="button" aria-label="Toggle color theme">
|
||||
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
@@ -155,7 +248,9 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
<script is:inline>
|
||||
<ScrollToTop />
|
||||
<SearchModal />
|
||||
<script is:inline data-astro-rerun>
|
||||
document.getElementById("theme-toggle").addEventListener("click", function () {
|
||||
var root = document.documentElement;
|
||||
var current =
|
||||
|
||||
@@ -34,18 +34,6 @@ function fibonacciSphere(count: number, radius: number): THREE.Vector3[] {
|
||||
return points;
|
||||
}
|
||||
|
||||
export function supportsWebGL(): boolean {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
return !!(
|
||||
window.WebGLRenderingContext &&
|
||||
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function mountConstellation(
|
||||
container: HTMLElement,
|
||||
tooltip: HTMLElement,
|
||||
@@ -108,11 +96,13 @@ export function mountConstellation(
|
||||
raycaster.params.Points = { threshold: 0.3 };
|
||||
const pointer = new THREE.Vector2(-2, -2);
|
||||
let hovered: THREE.Mesh | null = null;
|
||||
let pointerMoved = false;
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
pointerMoved = true;
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
@@ -150,32 +140,46 @@ export function mountConstellation(
|
||||
});
|
||||
}
|
||||
|
||||
// A slow constant rotation plus hover-raycasting doesn't need 60fps, and
|
||||
// raycasting every frame regardless of pointer movement was pure waste —
|
||||
// capping to ~30fps and only raycasting on an actual pointer move cut this
|
||||
// scene from being the site's single largest main-thread cost (it was
|
||||
// tanking the homepage's Lighthouse performance score to the low 80s while
|
||||
// every other page scored 95+) without changing how it looks or behaves.
|
||||
const frameInterval = 1000 / 30;
|
||||
let lastFrameTime = 0;
|
||||
let frame = 0;
|
||||
function animate() {
|
||||
function animate(time: number) {
|
||||
frame = requestAnimationFrame(animate);
|
||||
if (!reduceMotion) group.rotation.y += 0.0009;
|
||||
if (time - lastFrameTime < frameInterval) return;
|
||||
lastFrameTime = time;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const hits = raycaster.intersectObjects(nodeMeshes);
|
||||
const hit = hits[0]?.object as THREE.Mesh | undefined;
|
||||
if (!reduceMotion) group.rotation.y += 0.0018;
|
||||
|
||||
if (hit !== hovered) {
|
||||
if (hovered)
|
||||
(hovered.material as THREE.MeshBasicMaterial).color.copy(
|
||||
cssColor(TYPE_COLOR_VAR[hovered.userData.node.type]),
|
||||
);
|
||||
hovered = hit ?? null;
|
||||
if (hovered) {
|
||||
(hovered.material as THREE.MeshBasicMaterial).color.copy(
|
||||
cssColor("--accent"),
|
||||
);
|
||||
const node = hovered.userData.node as { title: string };
|
||||
tooltip.textContent = node.title;
|
||||
tooltip.style.opacity = "1";
|
||||
container.style.cursor = "pointer";
|
||||
} else {
|
||||
tooltip.style.opacity = "0";
|
||||
container.style.cursor = "default";
|
||||
if (pointerMoved) {
|
||||
pointerMoved = false;
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const hits = raycaster.intersectObjects(nodeMeshes);
|
||||
const hit = hits[0]?.object as THREE.Mesh | undefined;
|
||||
|
||||
if (hit !== hovered) {
|
||||
if (hovered)
|
||||
(hovered.material as THREE.MeshBasicMaterial).color.copy(
|
||||
cssColor(TYPE_COLOR_VAR[hovered.userData.node.type]),
|
||||
);
|
||||
hovered = hit ?? null;
|
||||
if (hovered) {
|
||||
(hovered.material as THREE.MeshBasicMaterial).color.copy(
|
||||
cssColor("--accent"),
|
||||
);
|
||||
const node = hovered.userData.node as { title: string };
|
||||
tooltip.textContent = node.title;
|
||||
tooltip.style.opacity = "1";
|
||||
container.style.cursor = "pointer";
|
||||
} else {
|
||||
tooltip.style.opacity = "0";
|
||||
container.style.cursor = "default";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hovered) {
|
||||
@@ -187,7 +191,7 @@ export function mountConstellation(
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
animate(0);
|
||||
|
||||
return function destroy() {
|
||||
cancelAnimationFrame(frame);
|
||||
|
||||
@@ -8,6 +8,34 @@ function hashString(value: string): number {
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
const k = (n: number) => (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
||||
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
|
||||
}
|
||||
|
||||
function relativeLuminance([r, g, b]: [number, number, number]): number {
|
||||
const lin = (c: number) => {
|
||||
const v = c / 255;
|
||||
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
||||
}
|
||||
|
||||
function contrastRatio(rgb1: [number, number, number], rgb2: [number, number, number]): number {
|
||||
const l1 = relativeLuminance(rgb1);
|
||||
const l2 = relativeLuminance(rgb2);
|
||||
const [lighter, darker] = l1 > l2 ? [l1, l2] : [l2, l1];
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
const DARK_TEXT: [number, number, number] = [0x21, 0x1d, 0x16];
|
||||
const LIGHT_TEXT: [number, number, number] = [0xf2, 0xed, 0xe1];
|
||||
const MIN_CONTRAST = 4.5;
|
||||
|
||||
export interface SpineStyle {
|
||||
background: string;
|
||||
color: string;
|
||||
@@ -18,11 +46,33 @@ export function spineStyle(title: string): SpineStyle {
|
||||
const hash = hashString(title);
|
||||
const hue = hash % 360;
|
||||
const saturation = 30 + (hash % 25);
|
||||
const lightness = 28 + ((hash >> 4) % 30);
|
||||
let lightness = 28 + ((hash >> 4) % 30);
|
||||
|
||||
// HSL lightness doesn't track perceptual luminance closely enough for
|
||||
// WCAG contrast math — especially for high-luminance hues like green/
|
||||
// yellow, a "dark" 43% lightness can still read as too bright for light
|
||||
// text. Compute real contrast and push lightness toward whichever
|
||||
// extreme the winning text color needs until it clears 4.5:1, instead of
|
||||
// trusting a fixed lightness threshold.
|
||||
let rgb = hslToRgb(hue, saturation, lightness);
|
||||
let darkContrast = contrastRatio(rgb, DARK_TEXT);
|
||||
let lightContrast = contrastRatio(rgb, LIGHT_TEXT);
|
||||
|
||||
for (let i = 0; i < 20 && Math.max(darkContrast, lightContrast) < MIN_CONTRAST; i++) {
|
||||
if (darkContrast > lightContrast) {
|
||||
lightness = Math.min(96, lightness + 3);
|
||||
} else {
|
||||
lightness = Math.max(4, lightness - 3);
|
||||
}
|
||||
rgb = hslToRgb(hue, saturation, lightness);
|
||||
darkContrast = contrastRatio(rgb, DARK_TEXT);
|
||||
lightContrast = contrastRatio(rgb, LIGHT_TEXT);
|
||||
}
|
||||
|
||||
const width = 42 + (hash % 6) * 7;
|
||||
return {
|
||||
background: `hsl(${hue} ${saturation}% ${lightness}%)`,
|
||||
color: lightness > 52 ? "#211d16" : "#f2ede1",
|
||||
color: darkContrast >= lightContrast ? "#211d16" : "#f2ede1",
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import hljsX86asm from "highlight.js/lib/languages/x86asm";
|
||||
import hljsXml from "highlight.js/lib/languages/xml";
|
||||
import hljsYaml from "highlight.js/lib/languages/yaml";
|
||||
import { Marked } from "marked";
|
||||
import markedKatex from "marked-katex-extension";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
|
||||
@@ -91,20 +92,26 @@ function highlightCode(code: string, lang: string | undefined): string {
|
||||
return `<pre class="hljs"><code>${inner}</code></pre>`;
|
||||
}
|
||||
|
||||
const marked = new Marked({
|
||||
renderer: {
|
||||
heading(token) {
|
||||
const plainText = stripInlineMarkdown(token.text);
|
||||
const slug = slugify(plainText);
|
||||
const html = this.parser.parseInline(token.tokens);
|
||||
currentHeadings.push({ depth: token.depth, slug, text: plainText });
|
||||
return `<h${token.depth} id="${slug}">${html}</h${token.depth}>`;
|
||||
},
|
||||
code(token) {
|
||||
return highlightCode(token.text, token.lang);
|
||||
// Renders $inline$ and $$block$$ LaTeX to static HTML/MathML at build time
|
||||
// (katex.renderToString under the hood) — no client-side JS shipped, unlike
|
||||
// the legacy Zola site's KaTeX auto-render script.
|
||||
const marked = new Marked(
|
||||
markedKatex({ throwOnError: false }),
|
||||
{
|
||||
renderer: {
|
||||
heading(token) {
|
||||
const plainText = stripInlineMarkdown(token.text);
|
||||
const slug = slugify(plainText);
|
||||
const html = this.parser.parseInline(token.tokens);
|
||||
currentHeadings.push({ depth: token.depth, slug, text: plainText });
|
||||
return `<h${token.depth} id="${slug}">${html}</h${token.depth}>`;
|
||||
},
|
||||
code(token) {
|
||||
return highlightCode(token.text, token.lang);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const CALLOUT_ICONS: Record<string, string> = {
|
||||
note: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>',
|
||||
@@ -154,6 +161,30 @@ async function renderCallouts(body: string): Promise<string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Matches a plain <iframe> pointing at a youtube-nocookie embed, however an
|
||||
// author pasted it (any attribute order/extras) — the legacy Zola site had a
|
||||
// `{{ youtube(id=...) }}` shortcode that wrapped the embed in a responsive
|
||||
// container, added `loading="lazy"`, and emitted VideoObject JSON-LD;
|
||||
// migrating posts flattened those calls to bare <iframe> tags (see
|
||||
// migrate-posts-to-writings.mjs), losing all three. Detecting the plain
|
||||
// <iframe> here restores them for any post, old or new, with no shortcode
|
||||
// syntax required.
|
||||
const YOUTUBE_IFRAME_RE =
|
||||
/<iframe\b[^>]*\bsrc="https:\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)[^"]*"[^>]*>\s*<\/iframe>/g;
|
||||
|
||||
/** Wraps youtube-nocookie <iframe> embeds in a responsive container and
|
||||
* adds `loading="lazy"` when missing, returning the transformed HTML plus
|
||||
* the extracted video ids (for VideoObject JSON-LD). */
|
||||
function enhanceYoutubeEmbeds(html: string): { html: string; youtubeIds: string[] } {
|
||||
const youtubeIds: string[] = [];
|
||||
const out = html.replace(YOUTUBE_IFRAME_RE, (tag, id) => {
|
||||
youtubeIds.push(id);
|
||||
const withLazy = /\bloading=/.test(tag) ? tag : tag.replace("<iframe", '<iframe loading="lazy"');
|
||||
return `<div class="youtube-wrapper">${withLazy}</div>`;
|
||||
});
|
||||
return { html: out, youtubeIds };
|
||||
}
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
@@ -205,12 +236,13 @@ export function tomlContentLoader(contentDir: string): Loader {
|
||||
const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, "");
|
||||
currentHeadings = [];
|
||||
const trimmedBody = await renderCallouts(body.trim());
|
||||
const html = await marked.parse(trimmedBody);
|
||||
const parsedHtml = await marked.parse(trimmedBody);
|
||||
const { html, youtubeIds } = enhanceYoutubeEmbeds(parsedHtml);
|
||||
const wordCount = trimmedBody.split(/\s+/).filter(Boolean).length;
|
||||
const readingMinutes = Math.max(1, Math.round(wordCount / 200));
|
||||
store.set({
|
||||
id,
|
||||
data: { ...data, wordCount, readingMinutes },
|
||||
data: { ...data, wordCount, readingMinutes, youtubeIds },
|
||||
rendered: { html, metadata: { headings: currentHeadings } },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Split out from constellation-scene.ts so checking WebGL support doesn't
|
||||
// require fetching/evaluating the three.js bundle it lives alongside —
|
||||
// Constellation.astro checks this before dynamically importing that module.
|
||||
export function supportsWebGL(): boolean {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
return !!(
|
||||
window.WebGLRenderingContext &&
|
||||
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { getCollection } from "astro:content";
|
||||
import type { APIContext } from "astro";
|
||||
import { siteConfig } from "../site.config";
|
||||
|
||||
function escapeXml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export async function GET(context: APIContext) {
|
||||
const site = (context.site ?? new URL(siteConfig.baseUrl)).href.replace(/\/$/, "");
|
||||
|
||||
const writings = (
|
||||
await getCollection("writings", ({ data }) => !data.draft)
|
||||
).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
|
||||
const updated = writings[0]?.data.updated ?? writings[0]?.data.date ?? new Date();
|
||||
|
||||
const entries = writings
|
||||
.map((entry) => {
|
||||
const url = `${site}/writings/${entry.id}/`;
|
||||
const published = entry.data.date.toISOString();
|
||||
const entryUpdated = (entry.data.updated ?? entry.data.date).toISOString();
|
||||
const categories = (entry.data.taxonomies?.tags ?? [])
|
||||
.map((tag) => `<category term="${escapeXml(tag)}" />`)
|
||||
.join("");
|
||||
return `
|
||||
<entry>
|
||||
<title>${escapeXml(entry.data.title)}</title>
|
||||
<link href="${url}" />
|
||||
<id>${url}</id>
|
||||
<published>${published}</published>
|
||||
<updated>${entryUpdated}</updated>
|
||||
${entry.data.description ? `<summary>${escapeXml(entry.data.description)}</summary>` : ""}
|
||||
<author><name>${escapeXml(entry.data.author ?? siteConfig.author)}</name></author>
|
||||
${categories}
|
||||
</entry>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>${escapeXml(siteConfig.title)}</title>
|
||||
<subtitle>${escapeXml(siteConfig.description)}</subtitle>
|
||||
<link href="${site}/atom.xml" rel="self" />
|
||||
<link href="${site}/" />
|
||||
<id>${site}/</id>
|
||||
<updated>${updated.toISOString()}</updated>
|
||||
${entries}
|
||||
</feed>`;
|
||||
|
||||
return new Response(xml, {
|
||||
headers: { "Content-Type": "application/atom+xml; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import "katex/dist/katex.min.css";
|
||||
import { getCollection, render } from "astro:content";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
@@ -16,9 +17,9 @@ const rendered = await Promise.all(
|
||||
{
|
||||
rendered.map(({ entry, Content }) => (
|
||||
<div class="card">
|
||||
<h3>
|
||||
<h2 class="entry-title">
|
||||
<a href={entry.data.url}>{entry.data.title}</a>
|
||||
</h3>
|
||||
</h2>
|
||||
<Content />
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import "katex/dist/katex.min.css";
|
||||
import { getCollection, render } from "astro:content";
|
||||
import BookSpines from "../../components/BookSpines.astro";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import "katex/dist/katex.min.css";
|
||||
import { getCollection, render } from "astro:content";
|
||||
import BookSpines from "../../components/BookSpines.astro";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
@@ -37,9 +38,9 @@ const withBodies = await Promise.all(
|
||||
)}
|
||||
</a>
|
||||
<div>
|
||||
<h3>
|
||||
<h2 class="entry-title">
|
||||
<a href={`/bookshelf/${entry.id}/`}>{entry.data.title}</a> <span class="tag">{entry.data.kind}</span>
|
||||
</h3>
|
||||
</h2>
|
||||
<p class="meta">
|
||||
{entry.data.authors.join(", ")}
|
||||
{entry.data.date_finished && <> · Read {entry.data.date_finished.toDateString()}</>}
|
||||
|
||||
@@ -21,7 +21,7 @@ const bookshelf = (await getCollection("bookshelf")).slice(0, 3);
|
||||
|
||||
<div class="columns">
|
||||
<section>
|
||||
<p class="eyebrow">Writings</p>
|
||||
<h2 class="eyebrow">Writings</h2>
|
||||
{
|
||||
writings.length === 0 ? (
|
||||
<p class="meta">Nothing published yet.</p>
|
||||
@@ -40,7 +40,7 @@ const bookshelf = (await getCollection("bookshelf")).slice(0, 3);
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p class="eyebrow">Portfolio</p>
|
||||
<h2 class="eyebrow">Portfolio</h2>
|
||||
{
|
||||
portfolio.length === 0 ? (
|
||||
<p class="meta">Nothing published yet.</p>
|
||||
@@ -57,7 +57,7 @@ const bookshelf = (await getCollection("bookshelf")).slice(0, 3);
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p class="eyebrow">Bookshelf</p>
|
||||
<h2 class="eyebrow">Bookshelf</h2>
|
||||
{
|
||||
bookshelf.length === 0 ? (
|
||||
<p class="meta">Nothing on the shelf yet.</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ const projects = (
|
||||
{
|
||||
projects.map((entry) => (
|
||||
<div class="card">
|
||||
<h3>{entry.data.title}</h3>
|
||||
<h2 class="entry-title">{entry.data.title}</h2>
|
||||
<p class="meta">{entry.data.role}</p>
|
||||
{entry.data.description && <p>{entry.data.description}</p>}
|
||||
{entry.data.outcome && <p>{entry.data.outcome}</p>}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
import Base from "../layouts/Base.astro";
|
||||
---
|
||||
|
||||
<Base title="Search">
|
||||
<h1>Search</h1>
|
||||
<p class="meta">
|
||||
Search only works against a built site (`pnpm build` runs Pagefind after
|
||||
`astro build`) — it has no index to query in `astro dev`.
|
||||
</p>
|
||||
<div id="search"></div>
|
||||
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
|
||||
<script is:inline src="/pagefind/pagefind-ui.js"></script>
|
||||
<script is:inline>
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
if (window.PagefindUI) {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: loaded from /pagefind/pagefind-ui.js above
|
||||
new PagefindUI({ element: "#search", showSubResults: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</Base>
|
||||
@@ -25,9 +25,9 @@ const description = siteConfig.tagDescriptions[tag];
|
||||
{
|
||||
writings.map((entry) => (
|
||||
<div class="card">
|
||||
<h3>
|
||||
<h2 class="entry-title">
|
||||
<a href={`/writings/${entry.id}/`}>{entry.data.title}</a>
|
||||
</h3>
|
||||
</h2>
|
||||
<p class="meta">{entry.data.date.toDateString()}</p>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -9,18 +9,59 @@ for (const w of writings) {
|
||||
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
const tags = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]);
|
||||
const tags = [...tagCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
---
|
||||
|
||||
<Base title="All Tags">
|
||||
<h1>All Tags</h1>
|
||||
<ul>
|
||||
{
|
||||
tags.map(([tag, count]) => (
|
||||
<li>
|
||||
<a href={`/tags/${tag}/`}>{tag}</a> ({count})
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div class="tags-page">
|
||||
<h1>All Tags</h1>
|
||||
<div class="tags-cloud">
|
||||
{
|
||||
tags.map(([tag, count]) => (
|
||||
<a href={`/tags/${tag}/`} class="tag-link">
|
||||
{tag}
|
||||
<sup class="tag-count">{count}</sup>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.tags-page {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tags-page h1 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tags-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-5);
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
margin-top: var(--space-6);
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
transition: color 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.tag-link:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
font-size: 0.65em;
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
---
|
||||
import "katex/dist/katex.min.css";
|
||||
import { getCollection, render } from "astro:content";
|
||||
import TocList from "../../components/TocList.astro";
|
||||
import Minimap from "../../components/Minimap.astro";
|
||||
import ReadingProgress from "../../components/ReadingProgress.astro";
|
||||
import ShareButtons from "../../components/ShareButtons.astro";
|
||||
import Comments from "../../components/Comments.astro";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import { siteConfig } from "../../site.config";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const writings = await getCollection("writings", ({ data }) => !data.draft);
|
||||
@@ -33,26 +39,61 @@ const seriesEntries = series
|
||||
: [];
|
||||
|
||||
const tags = entry.data.taxonomies?.tags ?? [];
|
||||
const related = tags.length
|
||||
? allWritings
|
||||
.filter((w) => w.id !== entry.id)
|
||||
.map((w) => ({
|
||||
entry: w,
|
||||
overlap: (w.data.taxonomies?.tags ?? []).filter((t) => tags.includes(t))
|
||||
.length,
|
||||
}))
|
||||
.filter((w) => w.overlap > 0)
|
||||
.sort((a, b) => b.overlap - a.overlap)
|
||||
.slice(0, 3)
|
||||
.map((w) => w.entry)
|
||||
: [];
|
||||
const shareUrl = new URL(Astro.url.pathname, siteConfig.baseUrl).href;
|
||||
const canonicalUrl = entry.data.canonical_url ?? shareUrl;
|
||||
const ogImageUrl = new URL(`/images/og/writings/${entry.id}.png`, siteConfig.baseUrl).href;
|
||||
|
||||
const description =
|
||||
entry.data.extra?.llms_description ?? entry.data.description;
|
||||
const tocHeadings = headings.filter((h) => h.depth <= 3);
|
||||
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: entry.data.title,
|
||||
description,
|
||||
url: canonicalUrl,
|
||||
image: ogImageUrl,
|
||||
datePublished: entry.data.date.toISOString(),
|
||||
dateModified: (entry.data.updated ?? entry.data.date).toISOString(),
|
||||
keywords: tags.length ? tags.join(", ") : undefined,
|
||||
author: { "@id": `${siteConfig.baseUrl}/#person` },
|
||||
publisher: { "@id": `${siteConfig.baseUrl}/#person` },
|
||||
mainEntityOfPage: canonicalUrl,
|
||||
};
|
||||
|
||||
const breadcrumbJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: "Home", item: siteConfig.baseUrl },
|
||||
{ "@type": "ListItem", position: 2, name: "Writings", item: `${siteConfig.baseUrl}/writings/` },
|
||||
{ "@type": "ListItem", position: 3, name: entry.data.title, item: canonicalUrl },
|
||||
],
|
||||
};
|
||||
|
||||
const videoJsonLd = entry.data.youtubeIds.map((id) => ({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "VideoObject",
|
||||
name: entry.data.title,
|
||||
description,
|
||||
thumbnailUrl: `https://i.ytimg.com/vi/${id}/sddefault.jpg`,
|
||||
uploadDate: entry.data.date.toISOString(),
|
||||
embedUrl: `https://www.youtube-nocookie.com/embed/${id}`,
|
||||
contentUrl: `https://www.youtube.com/watch?v=${id}`,
|
||||
}));
|
||||
---
|
||||
|
||||
<Base title={entry.data.title} description={description} wide ogImage={`/images/og/writings/${entry.id}.png`}>
|
||||
<Base
|
||||
title={entry.data.title}
|
||||
description={description}
|
||||
wide
|
||||
ogImage={`/images/og/writings/${entry.id}.png`}
|
||||
ogType="article"
|
||||
canonicalUrl={canonicalUrl}
|
||||
structuredData={[articleJsonLd, breadcrumbJsonLd, ...videoJsonLd]}
|
||||
>
|
||||
<ReadingProgress />
|
||||
<div class="article-layout">
|
||||
<div class="article-main">
|
||||
<h1>{entry.data.title}</h1>
|
||||
@@ -92,25 +133,28 @@ const tocHeadings = headings.filter((h) => h.depth <= 3);
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<nav class="post-nav" aria-label="Post navigation">
|
||||
{prev && <a href={`/writings/${prev.id}/`}>← {prev.data.title}</a>}
|
||||
{next && <a href={`/writings/${next.id}/`}>{next.data.title} →</a>}
|
||||
</nav>
|
||||
<ShareButtons title={entry.data.title} url={shareUrl} />
|
||||
|
||||
{
|
||||
related.length > 0 && (
|
||||
<section>
|
||||
<h2>Related Posts</h2>
|
||||
{related.map((w) => (
|
||||
<div class="card">
|
||||
<h3>
|
||||
<a href={`/writings/${w.id}/`}>{w.data.title}</a>
|
||||
</h3>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
(prev || next) && (
|
||||
<nav class="post-nav" aria-label="Post navigation">
|
||||
{prev && (
|
||||
<a class="post-nav-link post-nav-prev" href={`/writings/${prev.id}/`}>
|
||||
<span class="post-nav-label">← Previous</span>
|
||||
<span class="post-nav-title">{prev.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
{next && (
|
||||
<a class="post-nav-link post-nav-next" href={`/writings/${next.id}/`}>
|
||||
<span class="post-nav-label">Next →</span>
|
||||
<span class="post-nav-title">{next.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
<Comments />
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -121,13 +165,15 @@ const tocHeadings = headings.filter((h) => h.depth <= 3);
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<Minimap />
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.article-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 42rem);
|
||||
justify-content: center;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
gap: var(--space-7);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const writings = (
|
||||
{
|
||||
writings.map((entry) => (
|
||||
<div class="card">
|
||||
<h3><a href={`/writings/${entry.id}/`}>{entry.data.title}</a></h3>
|
||||
<h2 class="entry-title"><a href={`/writings/${entry.id}/`}>{entry.data.title}</a></h2>
|
||||
<p class="meta">
|
||||
{entry.data.date.toDateString()}
|
||||
{entry.data.readingMinutes && <> · {entry.data.readingMinutes} min read</>}
|
||||
|
||||
+157
-23
@@ -46,25 +46,16 @@ h3 {
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 var(--space-4);
|
||||
text-decoration-line: underline;
|
||||
text-decoration-color: var(--accent);
|
||||
text-decoration-thickness: 0.1em;
|
||||
text-underline-offset: 0.08em;
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h1::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.05em;
|
||||
right: 0.4em;
|
||||
bottom: 0.08em;
|
||||
height: 0.09em;
|
||||
background: var(--accent);
|
||||
opacity: 0.55;
|
||||
z-index: -1;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -75,6 +66,22 @@ h3 {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
/* A plain <a> has its own default underline (in its own text color, not
|
||||
the heading's accent), which paints over the heading's underline and
|
||||
made link-wrapped titles inconsistent with plain-text ones (and with
|
||||
each other, depending on visited/hover state). Forcing the decoration to
|
||||
inherit the heading's actual values fixes that everywhere a title is
|
||||
wrapped in a link, without needing a class on every call site. */
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a {
|
||||
text-decoration-line: inherit;
|
||||
text-decoration-color: inherit;
|
||||
text-decoration-thickness: inherit;
|
||||
text-underline-offset: inherit;
|
||||
text-decoration-skip-ink: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 var(--space-5);
|
||||
}
|
||||
@@ -93,6 +100,22 @@ pre {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.youtube-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin: 0 0 var(--space-5);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.youtube-wrapper iframe {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* highlight.js syntax highlighting (toml-content-loader.ts renders code
|
||||
fences through hljs's core build). Colors are drawn from the existing
|
||||
design tokens, so they already switch with the site's light/dark theme
|
||||
@@ -112,16 +135,47 @@ pre {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* #4a9c6d / #d1a53d have plenty of contrast on the dark theme's code-block
|
||||
background but fail WCAG AA on the light one (2.8:1 / 1.9:1) — same issue
|
||||
as the callout colors above, fixed the same way: darker variants for
|
||||
light mode, originals restored for dark mode. */
|
||||
.hljs-string,
|
||||
.hljs-attr,
|
||||
.hljs-symbol,
|
||||
.hljs-addition {
|
||||
color: #4a9c6d;
|
||||
color: #377451;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-doctag {
|
||||
color: #82641f;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .hljs-string,
|
||||
:root:not([data-theme="light"]) .hljs-attr,
|
||||
:root:not([data-theme="light"]) .hljs-symbol,
|
||||
:root:not([data-theme="light"]) .hljs-addition {
|
||||
color: #4a9c6d;
|
||||
}
|
||||
:root:not([data-theme="light"]) .hljs-number,
|
||||
:root:not([data-theme="light"]) .hljs-meta,
|
||||
:root:not([data-theme="light"]) .hljs-doctag {
|
||||
color: #d1a53d;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .hljs-string,
|
||||
:root[data-theme="dark"] .hljs-attr,
|
||||
:root[data-theme="dark"] .hljs-symbol,
|
||||
:root[data-theme="dark"] .hljs-addition {
|
||||
color: #4a9c6d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .hljs-number,
|
||||
:root[data-theme="dark"] .hljs-meta,
|
||||
:root[data-theme="dark"] .hljs-doctag {
|
||||
color: #d1a53d;
|
||||
}
|
||||
|
||||
@@ -134,7 +188,7 @@ pre {
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #d1502f;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
@@ -192,9 +246,13 @@ pre {
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 var(--space-5);
|
||||
}
|
||||
|
||||
.meta {
|
||||
@@ -221,15 +279,24 @@ pre {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
/* Semantically these list-item titles are h2 (the page's only other
|
||||
heading is its own h1, with nothing at h2 between them) — sized down to
|
||||
what used to be the h3 scale so going from h3 to h2 doesn't change the
|
||||
visual hierarchy, just the accessibility tree. */
|
||||
.entry-title {
|
||||
margin-bottom: var(--space-1);
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.card h3 a {
|
||||
text-decoration: none;
|
||||
.entry-title a {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-color: var(--accent);
|
||||
text-decoration-thickness: 0.1em;
|
||||
text-underline-offset: 0.08em;
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
.card h3 a:hover {
|
||||
.entry-title a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -298,16 +365,83 @@ pre {
|
||||
--callout-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Dark-mode-only values: #4a9c6d and #d1a53d have plenty of contrast on the
|
||||
dark --bg-elevated (5.4:1 / 7.9:1) but fail WCAG AA on the light one
|
||||
(2.8:1 / 1.9:1), so light mode gets darkened variants instead of one
|
||||
hardcoded color for both themes. */
|
||||
.callout-tip {
|
||||
--callout-color: #4a9c6d;
|
||||
--callout-color: #377451;
|
||||
}
|
||||
|
||||
.callout-warning {
|
||||
--callout-color: #d1a53d;
|
||||
--callout-color: #82641f;
|
||||
}
|
||||
|
||||
.callout-danger {
|
||||
--callout-color: #d1502f;
|
||||
--callout-color: var(--accent);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .callout-tip {
|
||||
--callout-color: #4a9c6d;
|
||||
}
|
||||
:root:not([data-theme="light"]) .callout-warning {
|
||||
--callout-color: #d1a53d;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .callout-tip {
|
||||
--callout-color: #4a9c6d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .callout-warning {
|
||||
--callout-color: #d1a53d;
|
||||
}
|
||||
|
||||
.post-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
margin-top: var(--space-7);
|
||||
padding-top: var(--space-6);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.post-nav-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-nav-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.post-nav-title {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
color: var(--text);
|
||||
transition: color 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.post-nav-link:hover .post-nav-title {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.post-nav {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
.post-nav-next {
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
--text: #1c1a15;
|
||||
--text-muted: #6f6a5c;
|
||||
--border: rgba(28, 26, 21, 0.12);
|
||||
--accent: #d1502f;
|
||||
--accent-muted: rgba(209, 80, 47, 0.12);
|
||||
/* #d1502f measured 4.0:1 on --bg, just under WCAG AA's 4.5:1 for normal
|
||||
text (used at small sizes in nav/tags/links) — darkened to 5.7:1 while
|
||||
keeping the same hue. */
|
||||
--accent: #a84025;
|
||||
--accent-muted: rgba(168, 64, 37, 0.12);
|
||||
|
||||
--font-display: "Fraunces Variable", ui-serif, Georgia, serif;
|
||||
--font-body: "Newsreader Variable", ui-serif, Georgia, serif;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
: ${ZOLA_REPO='https://github.com/getzola/zola'}
|
||||
: ${ZOLA_INSTALL_DIR="${HOME}/.cargo/bin"}
|
||||
|
||||
# Helpers for ANSI color and logging.
|
||||
name="$(basename $0)"; esc=''; reset="${esc}[0m"
|
||||
red() { local string="${1}"; echo "${esc}[31m${string}${reset}"; }
|
||||
yellow() { local string="${1}"; echo "${esc}[33m${string}${reset}"; }
|
||||
green() { local string="${1}"; echo "${esc}[32m${string}${reset}"; }
|
||||
blue() { local string="${1}"; echo "${esc}[34m${string}${reset}"; }
|
||||
magenta() { local string="${1}"; echo "${esc}[35m${string}${reset}"; }
|
||||
log() { local string="${1}"; echo "[$(blue $name)] ${string}"; }
|
||||
|
||||
prompt_for_update() {
|
||||
local current="${1}"
|
||||
local latest="${2}"
|
||||
|
||||
[ -n "${current}" ] \
|
||||
&& installed="You're using Zola $(yellow $current)" \
|
||||
|| installed="Zola is $(red 'not installed')"
|
||||
|
||||
log "${installed}, but $(magenta $latest) is available."
|
||||
|
||||
# Prompt for an update. Exit the entire script if declined.
|
||||
read -p "$(log "Would you like to install Zola $(magenta $latest)? [y/N]: ")" answer
|
||||
case $answer in
|
||||
[Yy]*) log "Downloading Zola $(magenta $latest) from GitHub." ;;
|
||||
[Nn]*|*) log $(red "Aborted!"); exit ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_zola() {
|
||||
local version="${1}"
|
||||
local download_url="${ZOLA_REPO}/releases/download/${version}/zola-${version}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
|
||||
curl --progress-bar --location $download_url | tar --gunzip --extract --directory=$ZOLA_INSTALL_DIR --file -
|
||||
}
|
||||
|
||||
update_zola() {
|
||||
# Find the current Zola version (if any).
|
||||
[ -n "$(which zola 2> /dev/null)" ] && current_version=$(zola --version | awk '{print "v"$2}')
|
||||
|
||||
# Find the latest available Zola version. Use --refs to avoid listing
|
||||
# annotated commit objects that include the dereference syntax '^{}'.
|
||||
# Find more background at https://stackoverflow.com/a/15472310.
|
||||
local latest_version=$(git ls-remote --tags --refs $ZOLA_REPO | # Fetch remote tags.
|
||||
sort -t '/' -k 3 -V | # Sort them by version.
|
||||
tail -n 1 | # Take the latest one.
|
||||
awk -F / '{print $3}') # Return only the tag.
|
||||
|
||||
if [ "${current_version}" != "${latest_version}" ]; then
|
||||
prompt_for_update "${current_version}" "${latest_version}"
|
||||
install_zola "${latest_version}"
|
||||
fi
|
||||
|
||||
log "You're using Zola $(magenta $latest_version). $(green "Everything is up-to-date!")"
|
||||
}
|
||||
|
||||
update_zola
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ZOLA_VERSION=0.23.1
|
||||
|
||||
echo "Downloading Zola ${ZOLA_VERSION}..."
|
||||
curl -sLJO "https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
|
||||
echo "Extracting Zola..."
|
||||
tar -xf zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz
|
||||
|
||||
echo "Building site with Zola..."
|
||||
node scripts/generate-llms.mjs
|
||||
./zola build
|
||||
|
||||
echo "Build complete! Generated files:"
|
||||
ls -la public/
|
||||
@@ -104,3 +104,15 @@ feed_url = "https://.../rss.xml"
|
||||
|
||||
Body is a one-to-two sentence blurb on why you follow it. No live feed
|
||||
fetching — this list is hand-curated and static.
|
||||
|
||||
## Markdown extras (all body content)
|
||||
|
||||
- **LaTeX**: `$inline$` and `$$block$$` math renders to static HTML/MathML
|
||||
at build time (`marked-katex-extension` in `toml-content-loader.ts`) — no
|
||||
client-side JS shipped for it.
|
||||
- **Callouts**: `{% <note> %} ... {% </note> %}` (also `tip`/`warning`/
|
||||
`danger`) renders as a styled callout box.
|
||||
- **YouTube embeds**: paste a plain `<iframe src="https://www.youtube-
|
||||
nocookie.com/embed/VIDEO_ID" ...></iframe>` — it's automatically wrapped
|
||||
in a responsive 16:9 container, gets `loading="lazy"`, and emits
|
||||
`VideoObject` structured data. No shortcode needed.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
+++
|
||||
title = "Why This Site Exists"
|
||||
description = "A short note on what this new site is, and why it's more than a tech blog."
|
||||
author = "andwati"
|
||||
date = 2026-08-30
|
||||
draft = false
|
||||
[taxonomies]
|
||||
tags = ["meta"]
|
||||
[extra]
|
||||
llms_description = "An introductory post explaining the purpose of this personal digital-legacy site."
|
||||
+++
|
||||
|
||||
This is a placeholder post establishing the `writings` content schema. It will
|
||||
be replaced once the design system and migration from the old Zola posts are
|
||||
ready.
|
||||
@@ -1,136 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
docs/build/
|
||||
docs/source/_autosummary/
|
||||
docs/source/reference/
|
||||
docs/source/_build/
|
||||
docs/rendering_times.csv
|
||||
#i18n
|
||||
docs/i18n/gettext/.doctrees
|
||||
docs/i18n/**/*.mo
|
||||
docs/i18n/translatable.po
|
||||
docs/i18n/untranslatable.po
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
jupyter/
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# PyCharm
|
||||
/.idea/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
*.pyc
|
||||
*.bak
|
||||
.DS_Store
|
||||
|
||||
.floo
|
||||
.flooignore
|
||||
.vscode
|
||||
.vs
|
||||
*.xml
|
||||
*.iml
|
||||
media
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
|
||||
/media_dir.txt
|
||||
# ^TODO: Remove the need for this with a proper config file
|
||||
|
||||
# Ignore the built dependencies
|
||||
third_party/*
|
||||
@@ -1,579 +0,0 @@
|
||||
from manim import *
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Shared palette
|
||||
# ─────────────────────────────────────────────
|
||||
BG = "#0d1117"
|
||||
ACCENT = "#58a6ff"
|
||||
GREEN = "#3fb950"
|
||||
ORANGE = "#f0883e"
|
||||
RED = "#f85149"
|
||||
PURPLE = "#bc8cff"
|
||||
GRAY = "#8b949e"
|
||||
WHITE = "#e6edf3"
|
||||
|
||||
config.background_color = BG
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Helper: section title card
|
||||
# ─────────────────────────────────────────────
|
||||
def title_card(scene: Scene, title: str, subtitle: str = ""):
|
||||
bg = Rectangle(width=14, height=8, fill_color=BG, fill_opacity=1, stroke_width=0)
|
||||
bar = Line(LEFT * 6, RIGHT * 6, color=ACCENT, stroke_width=3)
|
||||
t = Text(title, font="JetBrains Mono", color=WHITE, font_size=52, weight=BOLD)
|
||||
s = Text(subtitle, font="JetBrains Mono", color=GRAY, font_size=28) if subtitle else VMobject()
|
||||
t.move_to(UP * 0.4)
|
||||
s.next_to(t, DOWN, buff=0.4)
|
||||
bar.next_to(t, UP, buff=0.55)
|
||||
grp = VGroup(bg, bar, t, s)
|
||||
scene.play(FadeIn(bar), Write(t), FadeIn(s), run_time=1.4)
|
||||
scene.wait(1.5)
|
||||
scene.play(FadeOut(grp), run_time=0.6)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 1. Intro
|
||||
# ─────────────────────────────────────────────
|
||||
class S01_Intro(Scene):
|
||||
def construct(self):
|
||||
# Animated headline
|
||||
headline = Text("From C to Machine Code", font="JetBrains Mono",
|
||||
color=ACCENT, font_size=56, weight=BOLD)
|
||||
sub = Text("What Actually Runs", font="JetBrains Mono",
|
||||
color=WHITE, font_size=34)
|
||||
byline = Text("mockingspectre", font="JetBrains Mono",
|
||||
color=GRAY, font_size=22)
|
||||
|
||||
headline.move_to(UP * 1.2)
|
||||
sub.next_to(headline, DOWN, buff=0.5)
|
||||
byline.next_to(sub, DOWN, buff=0.7)
|
||||
|
||||
accent_line = Line(LEFT * 3.5, RIGHT * 3.5, color=ACCENT, stroke_width=2)
|
||||
accent_line.next_to(sub, DOWN, buff=0.3)
|
||||
|
||||
self.play(Write(headline), run_time=1.6)
|
||||
self.play(FadeIn(sub, shift=UP * 0.3), run_time=0.9)
|
||||
self.play(GrowFromCenter(accent_line), run_time=0.6)
|
||||
self.play(FadeIn(byline), run_time=0.7)
|
||||
self.wait(2.5)
|
||||
|
||||
# C → ASM → Binary flow diagram
|
||||
c_box = self._box("C Source", GREEN, LEFT * 4.5)
|
||||
asm_box = self._box("Assembly", ORANGE, ORIGIN)
|
||||
bin_box = self._box("Machine\nCode", RED, RIGHT * 4.5)
|
||||
|
||||
arr1 = Arrow(c_box.get_right(), asm_box.get_left(), color=ACCENT, buff=0.15)
|
||||
arr2 = Arrow(asm_box.get_right(), bin_box.get_left(), color=ACCENT, buff=0.15)
|
||||
lbl1 = Text("GCC compiler", font="JetBrains Mono", color=GRAY, font_size=18).next_to(arr1, UP, buff=0.1)
|
||||
lbl2 = Text("assemble", font="JetBrains Mono", color=GRAY, font_size=18).next_to(arr2, UP, buff=0.1)
|
||||
|
||||
self.play(FadeOut(headline, sub, accent_line, byline), run_time=0.5)
|
||||
self.play(FadeIn(c_box), run_time=0.5)
|
||||
self.play(GrowArrow(arr1), FadeIn(lbl1), run_time=0.7)
|
||||
self.play(FadeIn(asm_box), run_time=0.5)
|
||||
self.play(GrowArrow(arr2), FadeIn(lbl2), run_time=0.7)
|
||||
self.play(FadeIn(bin_box), run_time=0.5)
|
||||
self.wait(2.5)
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
@staticmethod
|
||||
def _box(label, color, pos):
|
||||
rect = RoundedRectangle(corner_radius=0.2, width=2.6, height=1.1,
|
||||
fill_color=color, fill_opacity=0.15,
|
||||
stroke_color=color, stroke_width=2)
|
||||
txt = Text(label, font="JetBrains Mono", color=color, font_size=24)
|
||||
grp = VGroup(rect, txt).move_to(pos)
|
||||
return grp
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 2. Hello World: C → Assembly walkthrough
|
||||
# ─────────────────────────────────────────────
|
||||
class S02_HelloWorld(Scene):
|
||||
def construct(self):
|
||||
title_card(self, "Hello World", "C → Assembly")
|
||||
|
||||
# Left: C code
|
||||
c_src = [
|
||||
("#include <stdio.h>", GRAY),
|
||||
("", WHITE),
|
||||
("int main(){", WHITE),
|
||||
(' printf("hello world\\n");', GREEN),
|
||||
(" return 0;", WHITE),
|
||||
("}", WHITE),
|
||||
]
|
||||
c_lines = VGroup(*[
|
||||
Text(line, font="JetBrains Mono", color=col, font_size=20)
|
||||
for line, col in c_src
|
||||
])
|
||||
c_lines.arrange(DOWN, aligned_edge=LEFT, buff=0.18)
|
||||
c_bg = BackgroundRectangle(c_lines, color="#161b22", fill_opacity=1,
|
||||
buff=0.25, corner_radius=0.15)
|
||||
c_code = VGroup(c_bg, c_lines).shift(LEFT * 3.4 + UP * 0.5)
|
||||
|
||||
c_label = Text("hello.c", font="JetBrains Mono", color=GREEN, font_size=22
|
||||
).next_to(c_code, UP, buff=0.2)
|
||||
|
||||
self.play(FadeIn(c_label), FadeIn(c_code), run_time=1.0)
|
||||
self.wait(0.8)
|
||||
|
||||
# Command
|
||||
cmd_txt = Text("gcc -o hello hello.c && objdump -d -M intel hello",
|
||||
font="JetBrains Mono", color=ORANGE, font_size=17
|
||||
).shift(DOWN * 2.8)
|
||||
self.play(Write(cmd_txt), run_time=1.2)
|
||||
self.wait(0.5)
|
||||
|
||||
# Right: ASM output
|
||||
asm_lines = [
|
||||
("1139:", "55", "push rbp"),
|
||||
("113a:", "48 89 e5", "mov rbp, rsp"),
|
||||
("113d:", "48 8d 05 c0 0e", "lea rax, [rip+0xec0]"),
|
||||
("1144:", "48 89 c7", "mov rdi, rax"),
|
||||
("1147:", "e8 e4 fe ff ff", "call puts@plt"),
|
||||
("114c:", "b8 00 00 00 00", "mov eax, 0x0"),
|
||||
("1151:", "5d", "pop rbp"),
|
||||
("1152:", "c3", "ret"),
|
||||
]
|
||||
asm_group = self._asm_block(asm_lines).shift(RIGHT * 3.1 + UP * 0.5)
|
||||
asm_label = Text("objdump output", font="JetBrains Mono",
|
||||
color=ORANGE, font_size=22).next_to(asm_group, UP, buff=0.2)
|
||||
|
||||
self.play(FadeIn(asm_label), FadeIn(asm_group), run_time=1.0)
|
||||
self.wait(0.5)
|
||||
|
||||
# Annotate key lines — asm_group[0]=bg, asm_group[1]=VGroup of rows
|
||||
rows = asm_group[1]
|
||||
note = Text("rdi ← pointer to \"hello world\"",
|
||||
font="JetBrains Mono", color=ACCENT, font_size=19
|
||||
).next_to(asm_group, DOWN, buff=0.25)
|
||||
self.play(rows[3].animate.set_color(ACCENT),
|
||||
rows[4].animate.set_color(PURPLE),
|
||||
Write(note), run_time=1.0)
|
||||
self.wait(2.5)
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
@staticmethod
|
||||
def _asm_block(lines):
|
||||
rows = VGroup()
|
||||
for addr, hex_b, mnemonic in lines:
|
||||
row = Text(f"{addr:<8} {hex_b:<20} {mnemonic}",
|
||||
font="JetBrains Mono", color=WHITE, font_size=17)
|
||||
rows.add(row)
|
||||
rows.arrange(DOWN, aligned_edge=LEFT, buff=0.18)
|
||||
bg = BackgroundRectangle(rows, color="#161b22", fill_opacity=1,
|
||||
buff=0.2, corner_radius=0.15)
|
||||
return VGroup(bg, rows)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 3. Registers
|
||||
# ─────────────────────────────────────────────
|
||||
class S03_Registers(Scene):
|
||||
def construct(self):
|
||||
title_card(self, "Registers", "The CPU's Workbench")
|
||||
|
||||
# Register diagram: rax broken into sub-registers
|
||||
regs = [
|
||||
("rax (64-bit / 8 bytes)", ACCENT, 4.8),
|
||||
("eax (32-bit / 4 bytes)", GREEN, 3.2),
|
||||
("ax (16-bit / 2 bytes)", ORANGE, 1.9),
|
||||
("al ( 8-bit / 1 byte )", RED, 0.95),
|
||||
]
|
||||
bars = VGroup()
|
||||
labels = VGroup()
|
||||
for name, color, width in regs:
|
||||
bar = Rectangle(width=width, height=0.52,
|
||||
fill_color=color, fill_opacity=0.25,
|
||||
stroke_color=color, stroke_width=2)
|
||||
lbl = Text(name, font="JetBrains Mono", color=color, font_size=19)
|
||||
lbl.next_to(bar, RIGHT, buff=0.25)
|
||||
bars.add(bar)
|
||||
labels.add(lbl)
|
||||
|
||||
for i, bar in enumerate(bars):
|
||||
bar.align_to(bars[0], LEFT)
|
||||
group = VGroup(*[VGroup(bars[i], labels[i]) for i in range(len(bars))])
|
||||
group.arrange(DOWN, aligned_edge=LEFT, buff=0.22).shift(LEFT * 1 + UP * 0.5)
|
||||
|
||||
self.play(LaggedStart(*[FadeIn(g) for g in group], lag_ratio=0.3), run_time=1.6)
|
||||
self.wait(1.0)
|
||||
|
||||
caption = Text("Smaller aliases point into the same physical register",
|
||||
font="JetBrains Mono", color=GRAY, font_size=21
|
||||
).shift(DOWN * 2.6)
|
||||
self.play(Write(caption))
|
||||
self.wait(1.5)
|
||||
self.play(FadeOut(group, caption))
|
||||
|
||||
# Calling convention diagram
|
||||
title_card(self, "Calling Convention", "Argument passing in x64 Linux")
|
||||
|
||||
args = [
|
||||
("1st arg", "rdi", ACCENT),
|
||||
("2nd arg", "rsi", GREEN),
|
||||
("3rd arg", "rdx", ORANGE),
|
||||
("4th arg", "rcx", PURPLE),
|
||||
("5th arg", "r8", RED),
|
||||
("6th arg", "r9", GRAY),
|
||||
]
|
||||
cols = VGroup()
|
||||
for label, reg, color in args:
|
||||
arg_t = Text(label, font="JetBrains Mono", color=WHITE, font_size=22)
|
||||
arr = Arrow(LEFT * 0.1, RIGHT * 0.9, color=color, buff=0)
|
||||
reg_t = Text(reg, font="JetBrains Mono", color=color,
|
||||
font_size=26, weight=BOLD)
|
||||
row = VGroup(arg_t, arr, reg_t).arrange(RIGHT, buff=0.3)
|
||||
cols.add(row)
|
||||
cols.arrange(DOWN, buff=0.28).shift(UP * 0.3)
|
||||
|
||||
ret_row = Text("return value → rax", font="JetBrains Mono",
|
||||
color=ACCENT, font_size=24).next_to(cols, DOWN, buff=0.5)
|
||||
|
||||
self.play(LaggedStart(*[FadeIn(r) for r in cols], lag_ratio=0.22), run_time=1.8)
|
||||
self.play(Write(ret_row))
|
||||
self.wait(2.5)
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 4. The Stack
|
||||
# ─────────────────────────────────────────────
|
||||
class S04_Stack(Scene):
|
||||
def construct(self):
|
||||
title_card(self, "The Stack", "LIFO — Last In, First Out")
|
||||
|
||||
# Animated stack push/pop
|
||||
stack_slots = VGroup()
|
||||
slot_labels = []
|
||||
N = 6
|
||||
for i in range(N):
|
||||
rect = Rectangle(width=3.2, height=0.72,
|
||||
fill_color="#161b22", fill_opacity=1,
|
||||
stroke_color=GRAY, stroke_width=1.5)
|
||||
stack_slots.add(rect)
|
||||
stack_slots.arrange(DOWN, buff=0).shift(LEFT * 3 + UP * 0.5)
|
||||
|
||||
self.play(FadeIn(stack_slots))
|
||||
|
||||
# Labels: lower address at top
|
||||
addr_labels = VGroup()
|
||||
for i, slot in enumerate(stack_slots):
|
||||
a = Text(f"0x...{(0x28 - i*8):02x}", font="JetBrains Mono",
|
||||
color=GRAY, font_size=16)
|
||||
a.next_to(slot, RIGHT, buff=0.15)
|
||||
addr_labels.add(a)
|
||||
self.play(FadeIn(addr_labels))
|
||||
|
||||
# rsp / rbp pointers
|
||||
rbp_ptr = Arrow(RIGHT * 0.8, stack_slots[-1].get_right() + LEFT * 0.01,
|
||||
color=ORANGE, buff=0.05)
|
||||
rbp_lbl = Text("rbp", font="JetBrains Mono", color=ORANGE,
|
||||
font_size=22, weight=BOLD).next_to(rbp_ptr, RIGHT, buff=0.1)
|
||||
rsp_ptr = Arrow(RIGHT * 0.8, stack_slots[-1].get_right() + LEFT * 0.01,
|
||||
color=ACCENT, buff=0.05)
|
||||
rsp_lbl = Text("rsp", font="JetBrains Mono", color=ACCENT,
|
||||
font_size=22, weight=BOLD).next_to(rsp_ptr, RIGHT, buff=0.1)
|
||||
|
||||
self.play(FadeIn(rbp_ptr, rbp_lbl))
|
||||
|
||||
# Push simulation: fill slots from bottom up
|
||||
push_values = ["saved rbp", "local: x=5", "saved rip", "arg 1", "arg 2"]
|
||||
colors_ = [ORANGE, GREEN, RED, ACCENT, PURPLE]
|
||||
current_top = N - 1
|
||||
|
||||
for val, col in zip(push_values, colors_):
|
||||
slot = stack_slots[current_top]
|
||||
fill = Rectangle(width=3.2, height=0.72,
|
||||
fill_color=col, fill_opacity=0.22,
|
||||
stroke_color=col, stroke_width=1.5)
|
||||
fill.move_to(slot)
|
||||
txt = Text(val, font="JetBrains Mono", color=col, font_size=18)
|
||||
txt.move_to(slot)
|
||||
rsp_ptr.put_start_and_end_on(
|
||||
RIGHT * 0.8, stack_slots[current_top].get_right() + LEFT * 0.01)
|
||||
rsp_lbl.next_to(rsp_ptr, RIGHT, buff=0.1)
|
||||
if current_top == N - 1:
|
||||
self.play(FadeIn(rsp_ptr, rsp_lbl))
|
||||
self.play(FadeIn(fill), Write(txt), run_time=0.55)
|
||||
current_top -= 1
|
||||
if current_top >= 0:
|
||||
new_end = stack_slots[current_top].get_right() + LEFT * 0.01
|
||||
self.play(rsp_ptr.animate.put_start_and_end_on(RIGHT * 0.8, new_end),
|
||||
rsp_lbl.animate.next_to(rsp_ptr, RIGHT, buff=0.1),
|
||||
run_time=0.4)
|
||||
|
||||
caption = Text("Stack grows toward lower addresses",
|
||||
font="JetBrains Mono", color=GRAY, font_size=20
|
||||
).shift(DOWN * 3.1)
|
||||
self.play(Write(caption))
|
||||
self.wait(2.5)
|
||||
|
||||
# Show the C code + ASM side by side
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
title_card(self, "Stack in Assembly", "sub rsp allocates space")
|
||||
|
||||
asm_lines = [
|
||||
("1139:", "55", "push rbp"),
|
||||
("113a:", "48 89 e5", "mov rbp, rsp"),
|
||||
("113d:", "48 83 ec 10", "sub rsp, 0x10 ; allocate locals"),
|
||||
("1141:", "c7 45 fc 05…", "mov DWORD PTR [rbp-0x4], 0x5"),
|
||||
("1152:", "e8 d9 fe ff ff", "call puts@plt"),
|
||||
("1157:", "b8 00 00 00 00", "mov eax, 0x0"),
|
||||
("115c:", "c9", "leave"),
|
||||
("115d:", "c3", "ret"),
|
||||
]
|
||||
asm_group = S02_HelloWorld._asm_block(asm_lines).scale(0.95).shift(UP * 0.3)
|
||||
self.play(FadeIn(asm_group))
|
||||
|
||||
# Highlight sub rsp
|
||||
rows = asm_group.submobjects[1]
|
||||
self.play(rows[2].animate.set_color(ACCENT), run_time=0.5)
|
||||
note = Text("Reserves 16 bytes on the stack for local variable x",
|
||||
font="JetBrains Mono", color=ACCENT, font_size=20
|
||||
).shift(DOWN * 2.8)
|
||||
self.play(Write(note))
|
||||
self.wait(2)
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 5. The Heap
|
||||
# ─────────────────────────────────────────────
|
||||
class S05_Heap(Scene):
|
||||
def construct(self):
|
||||
title_card(self, "The Heap", "Dynamic Memory & malloc/free")
|
||||
|
||||
# Memory layout diagram
|
||||
segments = [
|
||||
("Stack ↓", ORANGE, 1.2),
|
||||
(" ... ", BG, 0.5),
|
||||
("Heap ↑", GREEN, 1.2),
|
||||
(".bss", PURPLE, 0.55),
|
||||
(".data", PURPLE, 0.55),
|
||||
(".text", ACCENT, 0.55),
|
||||
]
|
||||
layout = VGroup()
|
||||
for label, color, h in segments:
|
||||
rect = Rectangle(width=3.8, height=h,
|
||||
fill_color=color, fill_opacity=0.18,
|
||||
stroke_color=color if color != BG else GRAY,
|
||||
stroke_width=1.5)
|
||||
lbl = Text(label, font="JetBrains Mono", color=color if color != BG else GRAY,
|
||||
font_size=22)
|
||||
lbl.move_to(rect)
|
||||
layout.add(VGroup(rect, lbl))
|
||||
layout.arrange(DOWN, buff=0).shift(LEFT * 3.5)
|
||||
|
||||
low_lbl = Text("Low Address", font="JetBrains Mono", color=GRAY, font_size=17
|
||||
).next_to(layout, DOWN, buff=0.2)
|
||||
high_lbl = Text("High Address", font="JetBrains Mono", color=GRAY, font_size=17
|
||||
).next_to(layout, UP, buff=0.2)
|
||||
self.play(FadeIn(layout), Write(low_lbl), Write(high_lbl), run_time=1.2)
|
||||
self.wait(0.6)
|
||||
|
||||
# malloc/free sequence on the right
|
||||
steps = [
|
||||
("char *buf = malloc(64);", GREEN, "allocate 64 bytes on heap"),
|
||||
("strcpy(buf, \"hello\");", ORANGE, "write data into buffer"),
|
||||
("free(buf);", RED, "return memory to allocator"),
|
||||
]
|
||||
step_group = VGroup()
|
||||
for code, color, desc in steps:
|
||||
code_t = Text(code, font="JetBrains Mono", color=color, font_size=19)
|
||||
desc_t = Text(desc, font="JetBrains Mono", color=GRAY, font_size=16)
|
||||
desc_t.next_to(code_t, DOWN, buff=0.12)
|
||||
step_group.add(VGroup(code_t, desc_t))
|
||||
step_group.arrange(DOWN, buff=0.5).shift(RIGHT * 2.2 + UP * 0.3)
|
||||
|
||||
for step in step_group:
|
||||
self.play(FadeIn(step, shift=RIGHT * 0.3), run_time=0.7)
|
||||
self.wait(0.6)
|
||||
|
||||
# Vulnerability callout
|
||||
vuln_box = RoundedRectangle(corner_radius=0.2, width=6.5, height=1.8,
|
||||
fill_color=RED, fill_opacity=0.1,
|
||||
stroke_color=RED, stroke_width=2)
|
||||
vuln_box.shift(DOWN * 2.8)
|
||||
vuln_txt = Text(
|
||||
"Forget free → Memory Leak\n"
|
||||
"free + reuse → Use-After-Free\n"
|
||||
"Overflow 64 bytes → Heap Overflow",
|
||||
font="JetBrains Mono", color=RED, font_size=17, line_spacing=1.3)
|
||||
vuln_txt.move_to(vuln_box)
|
||||
self.play(FadeIn(vuln_box), Write(vuln_txt), run_time=1.2)
|
||||
self.wait(2.5)
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 6. Instruction Set
|
||||
# ─────────────────────────────────────────────
|
||||
class S06_Instructions(Scene):
|
||||
def construct(self):
|
||||
title_card(self, "CPU Instructions", "mov · lea · push · call · ret")
|
||||
|
||||
categories = [
|
||||
("Data Movement", [
|
||||
("mov rax, rdx", "copy rdx into rax"),
|
||||
("mov rax, [rdx]", "dereference: load value at addr rdx"),
|
||||
("lea rdi, [rbx+0x10]","load address rbx+0x10 into rdi"),
|
||||
], ACCENT),
|
||||
("Arithmetic / Logic", [
|
||||
("add rax, 0x4", "rax = rax + 4"),
|
||||
("sub rsp, 0x10", "grow stack by 16 bytes"),
|
||||
("xor rax, rax", "zero rax (optimised)"),
|
||||
], GREEN),
|
||||
("Stack", [
|
||||
("push rbp", "decrement rsp; write rbp to [rsp]"),
|
||||
("pop rbp", "read [rsp] into rbp; increment rsp"),
|
||||
], ORANGE),
|
||||
("Control Flow", [
|
||||
("call 0x401234", "push rip; jump to function"),
|
||||
("ret", "pop saved rip; resume caller"),
|
||||
("jmp 0x401234", "unconditional jump"),
|
||||
("jz / jnz", "jump if Zero Flag set / not set"),
|
||||
], PURPLE),
|
||||
]
|
||||
|
||||
for cat_name, instructions, color in categories:
|
||||
self.play(*[FadeOut(m) for m in self.mobjects], run_time=0.3)
|
||||
hdr = Text(cat_name, font="JetBrains Mono", color=color,
|
||||
font_size=34, weight=BOLD).shift(UP * 3)
|
||||
self.play(Write(hdr))
|
||||
|
||||
rows = VGroup()
|
||||
for instr, desc in instructions:
|
||||
instr_t = Text(instr, font="JetBrains Mono", color=color, font_size=24)
|
||||
desc_t = Text(f" → {desc}", font="JetBrains Mono",
|
||||
color=WHITE, font_size=20)
|
||||
row = VGroup(instr_t, desc_t).arrange(RIGHT, buff=0.3)
|
||||
rows.add(row)
|
||||
rows.arrange(DOWN, aligned_edge=LEFT, buff=0.4).shift(UP * 0.4)
|
||||
self.play(LaggedStart(*[FadeIn(r, shift=LEFT * 0.3) for r in rows],
|
||||
lag_ratio=0.25), run_time=1.2)
|
||||
self.wait(2.0)
|
||||
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 7. CPU Flags
|
||||
# ─────────────────────────────────────────────
|
||||
class S07_Flags(Scene):
|
||||
def construct(self):
|
||||
title_card(self, "CPU Flags", "EFLAGS register")
|
||||
|
||||
# 16-bit flag register visual
|
||||
bits = list(range(15, -1, -1))
|
||||
important = {0: ("CF", RED), 2: ("PF", GRAY), 6: ("ZF", ACCENT),
|
||||
7: ("SF", ORANGE), 11: ("OF", PURPLE)}
|
||||
|
||||
cells = VGroup()
|
||||
for b in bits:
|
||||
color = important.get(b, ("", GRAY))[1] if b in important else GRAY
|
||||
rect = Square(side_length=0.55,
|
||||
fill_color=color, fill_opacity=0.18,
|
||||
stroke_color=color, stroke_width=1.5)
|
||||
bit_n = Text(str(b), font="JetBrains Mono",
|
||||
color=color, font_size=13).move_to(rect)
|
||||
cells.add(VGroup(rect, bit_n))
|
||||
cells.arrange(RIGHT, buff=0.04).shift(UP * 1.8)
|
||||
self.play(FadeIn(cells), run_time=1.0)
|
||||
|
||||
# Flag abbreviations below important bits
|
||||
for b, (abbr, color) in important.items():
|
||||
idx = 15 - b
|
||||
cell = cells[idx]
|
||||
lbl = Text(abbr, font="JetBrains Mono", color=color, font_size=18,
|
||||
weight=BOLD)
|
||||
lbl.next_to(cell, DOWN, buff=0.15)
|
||||
self.play(FadeIn(lbl), run_time=0.3)
|
||||
|
||||
self.wait(0.5)
|
||||
|
||||
# cmp / jz example
|
||||
example = VGroup(
|
||||
Text("cmp rax, 0x5 ; sets ZF=1 if rax == 5",
|
||||
font="JetBrains Mono", color=WHITE, font_size=21),
|
||||
Text("jz target ; jumps only if ZF=1",
|
||||
font="JetBrains Mono", color=ACCENT, font_size=21),
|
||||
).arrange(DOWN, aligned_edge=LEFT, buff=0.3).shift(DOWN * 0.4)
|
||||
|
||||
bg = BackgroundRectangle(example, color="#161b22", fill_opacity=1,
|
||||
buff=0.25, corner_radius=0.15)
|
||||
self.play(FadeIn(bg), FadeIn(example), run_time=1.0)
|
||||
|
||||
# Animate ZF cell lighting up
|
||||
zf_idx = 15 - 6
|
||||
zf_cell_rect = cells[zf_idx][0]
|
||||
self.play(zf_cell_rect.animate.set_fill(ACCENT, opacity=0.75), run_time=0.7)
|
||||
self.wait(0.5)
|
||||
self.play(zf_cell_rect.animate.set_fill(ACCENT, opacity=0.18), run_time=0.4)
|
||||
|
||||
self.wait(2.0)
|
||||
self.play(*[FadeOut(m) for m in self.mobjects])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 8. Outro
|
||||
# ─────────────────────────────────────────────
|
||||
class S08_Outro(Scene):
|
||||
def construct(self):
|
||||
recap_items = [
|
||||
"C compiles to assembly, which becomes machine code",
|
||||
"Registers: rip, rsp, rbp, argument registers",
|
||||
"Calling convention: rdi → rsi → rdx → rcx → r8 → r9",
|
||||
"Stack: LIFO, managed by rsp/rbp",
|
||||
"Heap: dynamic memory via malloc/free",
|
||||
"Instructions: mov, lea, push/pop, call/ret, jz/jnz",
|
||||
"Flags register drives conditional branching",
|
||||
]
|
||||
|
||||
title = Text("Recap", font="JetBrains Mono", color=ACCENT,
|
||||
font_size=44, weight=BOLD).shift(UP * 3.1)
|
||||
self.play(Write(title))
|
||||
|
||||
items_grp = VGroup()
|
||||
for txt in recap_items:
|
||||
bullet = Text(f"• {txt}", font="JetBrains Mono",
|
||||
color=WHITE, font_size=20)
|
||||
items_grp.add(bullet)
|
||||
items_grp.arrange(DOWN, aligned_edge=LEFT, buff=0.28).shift(UP * 0.5)
|
||||
|
||||
self.play(LaggedStart(*[FadeIn(i, shift=LEFT * 0.2) for i in items_grp],
|
||||
lag_ratio=0.18), run_time=2.5)
|
||||
self.wait(1.5)
|
||||
|
||||
next_lbl = Text("Next: ELF Binary Structure",
|
||||
font="JetBrains Mono", color=ORANGE, font_size=28,
|
||||
weight=BOLD).shift(DOWN * 2.8)
|
||||
self.play(Write(next_lbl))
|
||||
self.wait(1.0)
|
||||
|
||||
support = Text("buymeacoffee.com/mockingspectre",
|
||||
font="JetBrains Mono", color=GRAY, font_size=18
|
||||
).shift(DOWN * 3.5)
|
||||
self.play(FadeIn(support))
|
||||
self.wait(3.0)
|
||||
self.play(FadeOut(*self.mobjects))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Full video: concatenate all scenes
|
||||
# ─────────────────────────────────────────────
|
||||
class FullVideo(S01_Intro, S02_HelloWorld, S03_Registers,
|
||||
S04_Stack, S05_Heap, S06_Instructions,
|
||||
S07_Flags, S08_Outro):
|
||||
"""Render all scenes back-to-back as a single video file."""
|
||||
|
||||
def construct(self):
|
||||
S01_Intro.construct(self)
|
||||
S02_HelloWorld.construct(self)
|
||||
S03_Registers.construct(self)
|
||||
S04_Stack.construct(self)
|
||||
S05_Heap.construct(self)
|
||||
S06_Instructions.construct(self)
|
||||
S07_Flags.construct(self)
|
||||
S08_Outro.construct(self)
|
||||
@@ -1,9 +0,0 @@
|
||||
[project]
|
||||
name = "manimations"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"manim>=0.20.1",
|
||||
]
|
||||
Generated
-718
@@ -1,718 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "audioop-lts"
|
||||
version = "0.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "av"
|
||||
version = "17.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/eb/abca886df3a091bc406feb5ff71b4c4f426beaae6b71b9697264ce8c7211/av-17.0.0.tar.gz", hash = "sha256:c53685df73775a8763c375c7b2d62a6cb149d992a26a4b098204da42ade8c3df", size = 4410769, upload-time = "2026-03-14T14:38:45.868Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/fb/55e3b5b5d1fc61466292f26fbcbabafa2642f378dc48875f8f554591e1a4/av-17.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ed4013fac77c309a4a68141dcf6148f1821bb1073a36d4289379762a6372f711", size = 23238424, upload-time = "2026-03-14T14:38:05.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/03/9ace1acc08bc9ae38c14bf3a4b1360e995e4d999d1d33c2cbd7c9e77582a/av-17.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:e44b6c83e9f3be9f79ee87d0b77a27cea9a9cd67bd630362c86b7e56a748dfbb", size = 18709043, upload-time = "2026-03-14T14:38:08.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c0/637721f3cd5bb8bd16105a1a08efd781fc12f449931bdb3a4d0cfd63fa55/av-17.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b440da6ac47da0629d509316f24bcd858f33158dbdd0f1b7293d71e99beb26de", size = 34018780, upload-time = "2026-03-14T14:38:10.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/59/d19bc3257dd985d55337d7f0414c019414b97e16cd3690ebf9941a847543/av-17.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1060cba85f97f4a337311169d92c0b5e143452cfa5ca0e65fa499d7955e8592e", size = 36358757, upload-time = "2026-03-14T14:38:13.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/6c/a1f4f2677bae6f2ade7a8a18e90ebdcf70690c9b1c4e40e118aa30fa313f/av-17.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:deda202e6021cfc7ba3e816897760ec5431309d59a4da1f75df3c0e9413d71e7", size = 35195281, upload-time = "2026-03-14T14:38:15.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ea/52b0fc6f69432c7bf3f5fbe6f707113650aa40a1a05b9096ffc2bba4f77d/av-17.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffaf266a1a9c2148072de0a4b5ae98061465178d2cfaa69ee089761149342974", size = 37444817, upload-time = "2026-03-14T14:38:18.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ad/d2172966282cb8f146c13b6be7416efefde74186460c5e1708ddfc13dba6/av-17.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:45a35a40b2875bf2f98de7c952d74d960f92f319734e6d28e03b4c62a49e6f49", size = 28888553, upload-time = "2026-03-14T14:38:21.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/c5a4c4172c514d631fb506e6366b503576b8c7f29809cf42aca73e28ff01/av-17.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:3d32e9b5c5bbcb872a0b6917b352a1db8a42142237826c9b49a36d5dbd9e9c26", size = 21916910, upload-time = "2026-03-14T14:38:23.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/8e/c40ac08e63f79387c59f6ecc38f47d4c942b549130eee579ec1a91f6a291/av-17.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:d13250fb4b4522e9a6bec32da082556d5f257110ea223758151375748d9bbe25", size = 23483029, upload-time = "2026-03-14T14:38:25.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/fb/b4419494bfc249163ec393c613966d66db7e95c76da3345711cd115a79df/av-17.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:dbb56aa3b7ae72451d1bf6e9d37c7d83d39b97af712f73583ff419fbf08fc237", size = 18920446, upload-time = "2026-03-14T14:38:27.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/62/c2306d91602ddad2c56106f21dcb334fd51d5ea2e952f7fa025bb8aa39fc/av-17.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a213ac9e83b7ab12c2e9f277a09cac8e9d85cf0883efdab7a87a60e2e4e48879", size = 37477266, upload-time = "2026-03-14T14:38:30.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/cd/c8510a9607886785c0b3ca019d503e888c3757529be42a7287fe2bfa92d5/av-17.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:e15c88bb0921f9435bcc5a27a0863dba571a80ad5e1389c4fcf2073833bb4a74", size = 39572988, upload-time = "2026-03-14T14:38:32.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/2d/207d9361e25b5abec9be335bbab4df6b6b838e2214be4b374f4cfb285427/av-17.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:096cfd1e9fc896506726c7c42aaf9b370e78c2f257cde4d6ddb6c889bfcc49ec", size = 38399591, upload-time = "2026-03-14T14:38:35.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ca/307740c6aa2980966bf11383ffcb04bacc5b13f3d268ab4cfb274ad6f793/av-17.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3649ab3d2c7f58049ded1a36e100c0d8fd529cf258f41dd88678ba824034d8c9", size = 40590681, upload-time = "2026-03-14T14:38:38.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/f2/6fdb26d0651adf409864cb2a0d60da107e467d3d1aabc94b234ead54324a/av-17.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e5002271ab2135b551d980c2db8f3299d452e3b9d3633f24f6bb57fffe91cd10", size = 29216337, upload-time = "2026-03-14T14:38:40.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0a/0896b829a39b5669a2d811e1a79598de661693685cd62b31f11d0c18e65b/av-17.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dba98603fc4665b4f750de86fbaf6c0cfaece970671a9b529e0e3d1711e8367e", size = 22071058, upload-time = "2026-03-14T14:38:43.663Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.14.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloup"
|
||||
version = "3.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/64/7f0a66021ff81d51859c66adc13f3c71f0306c2f8dfb9877a0694cbada05/cloup-3.0.9.tar.gz", hash = "sha256:519f524d3c64040e49a0866b5fc0bfd6af3eac0d3d6a4b2b50b33ab0247db2d7", size = 229896, upload-time = "2026-04-04T03:53:54.182Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/15/435bb5a24e3a2dc8c26afedab0c6f0cf4291c66fb99509c168c38eb81135/cloup-3.0.9-py2.py3-none-any.whl", hash = "sha256:c761ef4e975454335d50c0e1eb83595a932735aac06e8955b3ced774bc62ae7b", size = 54724, upload-time = "2026-04-04T03:53:52.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cython"
|
||||
version = "3.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glcontext"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/80/8238a0e6e972292061176141c1028b5e670aa8c94cf4c2f819bd730d314e/glcontext-3.0.0.tar.gz", hash = "sha256:57168edcd38df2fc0d70c318edf6f7e59091fba1cd3dadb289d0aa50449211ef", size = 16422, upload-time = "2024-08-10T20:01:20.004Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/be/0ef6a6710164fde818040238b041b02a082a2b9d210f18632ab2354d863e/glcontext-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5bd37089570d3cdb01c6c0b315c49ce8a4dcdab2c431f5ba9f37a8b633cebfdf", size = 9366, upload-time = "2024-08-10T20:00:30.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d7/c3220898d72fbf938660ba5789c19cf245d21b45802a5d86cbcc67d66413/glcontext-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:857fd83e60f15580afd369dfb651a10d84a70ec35995622d253551bfb3ff9477", size = 9735, upload-time = "2024-08-10T20:00:31.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/c1/2d57062d2f2f6e55c58b12bdeab2a39b209f959df32039d099ecdfe96bf7/glcontext-3.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93fda9b378ce6d91f366e83e71ebdafdd167280a9834d1d6341ce6457c4e42ed", size = 50166, upload-time = "2024-08-10T20:00:32.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/4d/7fee00c76d678b06529e939ab6f3e3190af30208fe2e984526899026a437/glcontext-3.0.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89ad50d34aa62f03f6aaf6ae39fc27afd1b0eaefb0281aac51f686dc5672d473", size = 51186, upload-time = "2024-08-10T20:00:33.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/58/dc9a56192b889587e51ea511804bad5dec816de81ce16831db9fe19d5c40/glcontext-3.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2634d5e9647a6d7b0c5a5c0c57e91ac98aa79759bffb42459af4374b049fab01", size = 44658, upload-time = "2024-08-10T20:00:35.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ed/acb12e67589deaa96ad29d6994c2b9383afd18700f4f2a42ff342628aac5/glcontext-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0140c5df37cb48271527355062d35589dc3e1e7e73b51adf9962ed5048115f69", size = 46882, upload-time = "2024-08-10T20:00:36.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/6d/033ba23466d596c945f5f31f5c4e50cba3bc6664fdee58f6ceab54b76f4b/glcontext-3.0.0-cp313-cp313-win32.whl", hash = "sha256:6678e0552b516fa8fe62f500ef2b953bec991e82a003be2a9840d16556d03d2e", size = 12220, upload-time = "2024-08-10T20:00:37.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/b4/f0e0860526b8661ec6ae2b25a15b61100e551f57f488613c564752173a56/glcontext-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:18aa4b1df50e8c8ea39bd0f775f39bcc987521f92c4ed019ec7d70078471354d", size = 12971, upload-time = "2024-08-10T20:00:39.233Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isosurfaces"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/cf/bd7e70bb7b8dfd77afdc79aba8d83afd4a9263f045861cd4ddd34b7f6a12/isosurfaces-0.1.2.tar.gz", hash = "sha256:fa51ebe864ea9355b26830e27fdd6a41d5a58b419fa8d4b47e3b8b80718d6e21", size = 11348, upload-time = "2024-02-26T00:20:52.066Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/68/d5e9e6e0d6e43107d8393d2ee3d231dbb597bf93052c6f3117b313724980/isosurfaces-0.1.2-py3-none-any.whl", hash = "sha256:525a49ba93f4dbc35303cd2faf30976af0f99d9274cfa2787aec016b8ef96c64", size = 11649, upload-time = "2024-02-26T00:20:41.308Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manim"
|
||||
version = "0.20.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "audioop-lts" },
|
||||
{ name = "av" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "click" },
|
||||
{ name = "cloup" },
|
||||
{ name = "decorator" },
|
||||
{ name = "isosurfaces" },
|
||||
{ name = "manimpango" },
|
||||
{ name = "mapbox-earcut" },
|
||||
{ name = "moderngl" },
|
||||
{ name = "moderngl-window" },
|
||||
{ name = "networkx" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pycairo" },
|
||||
{ name = "pydub" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
{ name = "scipy" },
|
||||
{ name = "screeninfo" },
|
||||
{ name = "skia-pathops" },
|
||||
{ name = "srt" },
|
||||
{ name = "svgelements" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "watchdog" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/3b/ad54ce02f3e95d17d016cb1254708ae3795b60d5661f3b2085655940a565/manim-0.20.1.tar.gz", hash = "sha256:1e9747fb2fc1bde58ad09bcbd77d141793ce4b61811726a7fce537193d92e16b", size = 553537, upload-time = "2026-02-27T07:32:53.54Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/74/7a54f81bbb3016036662c5497edc89c5d053a8ab1e8f7c8faaca5d43b174/manim-0.20.1-py3-none-any.whl", hash = "sha256:2c76784716aef88630eace2762645e1eb25f236fe750cb1b3c4fdd3558de49e1", size = 652109, upload-time = "2026-02-27T07:32:55.066Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manimations"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "manim" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "manim", specifier = ">=0.20.1" }]
|
||||
|
||||
[[package]]
|
||||
name = "manimpango"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/55/d360e73eb4d04b102cef399ddebcba486a9b6c1977a26fe710beffd52e95/manimpango-0.6.1.tar.gz", hash = "sha256:59a00bbf8e99dab5f94341087c88e609fe946e79724627429cf59da84cbd40bf", size = 4080834, upload-time = "2025-10-23T06:04:48.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ee/01cd6b3087a5ead9483add3023f46c135bddd7773d506b771b44bbe0830b/manimpango-0.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79aafe6373cb31dcdd1ee505407ec2bea18e74aed877f93dcd27ec9d88584472", size = 7751685, upload-time = "2025-10-23T06:04:28.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/93/905ac20a9190655870131940dadb5121645cf8a23e6ea0f04be7f4604f2e/manimpango-0.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1049bda27ca504d24b48bec21756f4d8a3310ea7bc690167c54d05e97b8f7639", size = 7062629, upload-time = "2025-10-23T06:04:30.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/88/b2de7d2d3a0331cf79992f8c6da0f0abd873d4ec9f024cad5992f1cf7a6d/manimpango-0.6.1-cp313-cp313-win32.whl", hash = "sha256:8da8238147b96737b36725b0fc386dc1dd7901516dfd7e73b2e7d34cd5d934f1", size = 3626705, upload-time = "2025-10-23T06:04:31.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ed/a7a57491b26e8fb85ac0be9c39b53a6690f3c39beaf06d3715cb6916932b/manimpango-0.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:e90d6b926d0e673ce624963d44cc28c55a368b6074108ab885a43305baeae26c", size = 4203874, upload-time = "2025-10-23T06:04:32.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c0/318410215b409465a237c126a3e1f612c432d33b037b2f997870df327434/manimpango-0.6.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:318d14087901ee27ed6d0cfef0335aed57db217f8de831c8ca4e4c52b7805b45", size = 7751895, upload-time = "2025-10-23T06:04:34.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/83/f0a05e3e6312ca9673bc8b470c6a743cf2d0bd7f08b218efc993226e72e4/manimpango-0.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffe2f8353857f0ca9a16894430d871051da450bc2d19beb969cf8b57b032f580", size = 7063387, upload-time = "2025-10-23T06:04:36.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/76/0748323341647e8c338bd7af6fb527ffdc43d676003b57743740bdfcffcc/manimpango-0.6.1-cp314-cp314-win32.whl", hash = "sha256:c294ab801c8ffc217342dfeff7aeca97cbea86cd8b4e6e8c9a8d7da020b820e1", size = 3737933, upload-time = "2025-10-23T06:04:38.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/e9/42b995549cfd137780045e44b3bfba2cb1247b18bd2fb9f97079048e78ac/manimpango-0.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:d8b0ee675eb30bbb67097cecb2be469847c3ccf574291f2bd43360caeb5f62ed", size = 4357908, upload-time = "2025-10-23T06:04:40.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mapbox-earcut"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/7c/c5dd5b255b9828ba5df729e62fdd470a322c938f07ef392ca03c0592bb3a/mapbox_earcut-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:582329a81bd36cf0f82e443c395bcb8cfdb10caddafec76acaebac7c20bf1c31", size = 55619, upload-time = "2025-11-16T18:40:44.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/3f/03f23eac9831e7d0d8da3d6993695a9a3724659c94e9997f6b7aaccc199d/mapbox_earcut-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d2ac5f610b3e44a3a0c4df06b5552d503b4f1c2c409eeca20dbe05112bd60955", size = 52023, upload-time = "2025-11-16T18:40:45.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/f3/a92ccee494b3e437e4bd81ecd358e39d231dc90af010d6c43930506c10ad/mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58cc88513b87734b243d86f0d3fb87e96e0a78d9abd8fd615c55f766dd63f949", size = 56357, upload-time = "2025-11-16T18:40:47.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/30/e54ececd0403a5495c340b693075abec92a6d17dc44283b6cb059534f7ed/mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40218d887798451932f3c335992834aa807c35cd497c6e0733470fdbd77f9521", size = 59215, upload-time = "2025-11-16T18:40:48.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/e1/8fbff13a074c1fbf702b30ce7ec4d878bc664d659c1c2b1697831f4ea3a8/mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:39fa5cfa0e855b028ec9b0200c88ebfa252448f343ce2f67b6fc07fe1f22a3ae", size = 152304, upload-time = "2025-11-16T18:40:49.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d5/c757030b3cb3a9f2278ded6f7312d2b9d3761db6f3da8d395f7f7303dd66/mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:476b558473b8a43f238d46e819bc0f830c427842ec5feb19e23b4dcac8ad2455", size = 157270, upload-time = "2025-11-16T18:40:51.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/63/589c6decb1f032d8811f1066da552f0a718830f592e6d6539fa4c3c766b8/mapbox_earcut-2.0.0-cp313-cp313-win32.whl", hash = "sha256:8c2d125c182acbc490b39503c0dec4f937bae180d0849a26bcea0ee4a76024bd", size = 51207, upload-time = "2025-11-16T18:40:52.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/75/a79a6020c46d4f07731e88ec5cc9324f6b43343aba835def1dc0bf59fecf/mapbox_earcut-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e049e6a37c228d7a9cb2f54ae405aa21d35c5175d849530fb32064ddb38ad5ab", size = 56416, upload-time = "2025-11-16T18:40:53.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/5f/83e878c2b3e9e6db1f60b598a2cc5ed4c2b5bc8d281575c964869414a159/mapbox_earcut-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:8a833d73d63d4b6291bbd8b4d2f551e87f663282cdc547ecbbd9b423849ee996", size = 50103, upload-time = "2025-11-16T18:40:54.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/fc/f1b74324c83f510213ff91eb8b1d2697ad5a12418c5fba966e80f1104a5f/mapbox_earcut-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad1dc141797037b7d4c9d8d2e52b9665b36294913a8ec31008b282d1a95b9bdc", size = 55728, upload-time = "2025-11-16T18:40:56.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/59/053c04e29c4bd22157d3b6255f1e5c19c46cb7a594c4314298bdcbca723f/mapbox_earcut-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0f0f5c6f5ed8ffdce8efe6a003ba598089d0ee07eabd41868db183be50484f9f", size = 52063, upload-time = "2025-11-16T18:40:57.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/77/acc2d553c3bb8c769535a280545bb7d9608141e90511a2e6215a54611776/mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82cd92775f37fd1e4b8464c5e74a00e87130eecc55ee3df2492b8ca2bdf6ef3e", size = 56522, upload-time = "2025-11-16T18:40:58.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/f5/627dd6defd3c1a2b3069e9e27482aa04d268c841735e576c1e22848a34f6/mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:626ffc1310e0cc8910283e4ac3139e5fb0458f18f2c4874162f66159951933ff", size = 59204, upload-time = "2025-11-16T18:41:00.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/3e/819185542ab095ba1244ad65ececb3edcde6fd0111248a0f9318d695bfcf/mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ea951d764a356cad95b23fef950d8aa3b44b933795ad09d977fea7d4dbe377c3", size = 152550, upload-time = "2025-11-16T18:41:01.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ad/85e0f815e4774b90ad6761bce55c80d13ee21b2a24014b0be0d5010b0049/mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df1f217624abb5e02ecabcbd84369de970b8d8bc1e4e7c164c1cfcaddad76ca3", size = 157322, upload-time = "2025-11-16T18:41:02.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/4c/0f56369e7a000d2f3177d17baf34263559b206ae524fcd0c4c5d1d960dab/mapbox_earcut-2.0.0-cp314-cp314-win32.whl", hash = "sha256:6fa61307d38b50fc9bd5449c00dbae46d270a32b372c6fc3b8af4b85c85746e4", size = 52916, upload-time = "2025-11-16T18:41:04.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/9d/8c557dd9b3d9fe2344f5bd5ff3bb0b2a42ed6addb7e43ca4358051743b04/mapbox_earcut-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:0da20ed3c81b240450118773bcedfac34e70a56998f66147222c46f4356fff67", size = 57713, upload-time = "2025-11-16T18:41:05.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ec/678c5553938d3a29d02dd41dd898672267f054afc4e2821958dee6ec86ce/mapbox_earcut-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:847e74bd5878e4c64793dc100f9288f5443f87c55c3fe391fd90509029136ff6", size = 51872, upload-time = "2025-11-16T18:41:06.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/37/94f2d973669cbfef811e536713fe56ec012ba74e5f8795a832337b1866a3/mapbox_earcut-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ddc9e7175fc903185c64afbbf91febee56b50787dd0962fce2bfb4f20cf80d27", size = 56447, upload-time = "2025-11-16T18:41:07.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1c/e0afcc82659cc1727a7e59c4f9e9880bbc3f048a4a5325772b44d4a91dfd/mapbox_earcut-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6dc8a7568066af9a858018d6d92b7e77e164578f9fcd79093f1cbe4ec203461b", size = 53154, upload-time = "2025-11-16T18:41:08.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2d/9845281c8c35da2bea733b8c2df5b9fe694e73e7b05fe8a1d4c3c439a1bc/mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6abc5340edd9b433ab2dab2ee033082a199d5c51cce445124626c0040ec0d81b", size = 56285, upload-time = "2025-11-16T18:41:09.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8e/eeea762a519490662b8f480e2b35bf03701b0bcc5a446b62a4c5a1500b06/mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df7afdd8078a9aa28f469d9242531d304e09a4b14e514f048e021a949f3777b4", size = 58601, upload-time = "2025-11-16T18:41:10.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/67/932f80aa6af9bc1a317b6119052c74f327d81e00b457003a049e324b810c/mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a286f73e612a46cafd6d6c843365265090517af16823e2f37277c13cd8b6f09", size = 154924, upload-time = "2025-11-16T18:41:12.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/38/5db4a91f9f90cbb447be61da5468a2955fad3a840ae4c7dbde789b09d45a/mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8d081fe1d00dc553e3e68c02fc395324aad0d8ed955f3ff59289264c9b21ace4", size = 159194, upload-time = "2025-11-16T18:41:13.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/03/de3843b13fe854a010fb2f8b25551d4d5fe1c879ff2e7c8d7d8d7d735a8e/mapbox_earcut-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:13049ca96431bbc7ef7fd7780dd1872209ca11a5c1977f7aa91a1b574a8af863", size = 54143, upload-time = "2025-11-16T18:41:14.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/89/fbdee5a56ba51df9be6098b5428636ad75aa994e98d8bec6113d5cba401e/mapbox_earcut-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ace78e4fdba3b8cbb7768d44d77a981698305862a07f94bbb6f5cc16659adb4", size = 60833, upload-time = "2025-11-16T18:41:15.694Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moderngl"
|
||||
version = "5.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "glcontext" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/52/540e2f8c45060bb2709f56eb5a44ae828dfcc97ccecb342c1a7deb467889/moderngl-5.12.0.tar.gz", hash = "sha256:52936a98ccb2f2e1d6e3cb18528b2919f6831e7e3f924e788b5873badce5129b", size = 193232, upload-time = "2024-10-17T12:36:28.002Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/13/cf493bdc3cb4f7a6b4fb357e683404dc8a97d19f53d501e4afdd679538e2/moderngl-5.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f6efb432f5164f871471d1da36e3a4be9dc3efd7a1e48d0ac6b751e556af5d02", size = 112118, upload-time = "2024-10-17T12:37:14.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/1f/1d84bba5f42fb19ce240d08d5434fe8e2f34341e68bb9fa89336b6cbdcfc/moderngl-5.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9b09d8d15b2eaab41c8646a664429ec86af225fa25096758497cd212489d2e1e", size = 109371, upload-time = "2024-10-17T12:37:15.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/ae/bda0b95878e2b36eac66f64d88c08e6c8ea759607f7d40e843a21c2f4f32/moderngl-5.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071042dd4846e58cbe204cf49341b62cd209fdcb6d48018feb5a61c66707fcb2", size = 296206, upload-time = "2024-10-17T12:37:17.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/bc/93dc73251bcdb9c0f6f5c9a1d97ec2134672c307c68e6106948eab1f73d4/moderngl-5.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91db8302ac7f5d7a82a967388677e1378ff078f1e16d05da37ce77f4633b93b1", size = 270443, upload-time = "2024-10-17T12:37:18.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/a1/bd72c788b16c2392d3e1ebb570e56d7f871eaa1854f57917c0f131acb365/moderngl-5.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:51971d65ec96a212a814350c8b324ae0754353e1b61826d1a06aa2d060df170e", size = 1345675, upload-time = "2024-10-17T12:37:20.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/ec/6aff8fa267d9f80e4d32b7a606fdf9f0563103441a7dbaa4f53e272a4ada/moderngl-5.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d56827360c19e831e986243b5daaf6a51006f1ec0d5372084ad446308763d19f", size = 1274906, upload-time = "2024-10-17T12:37:22.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/4d/dc3ff763c125080e71b1095875f5dcc80949402019abc073bdfdbed1f4c2/moderngl-5.12.0-cp313-cp313-win32.whl", hash = "sha256:caa432c12b138a6c9571719075c4d103bdc2504cd31aeda38a00ad10fcf268cb", size = 101215, upload-time = "2024-10-17T12:37:23.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/8b/0a264732e0ee49fca109e98ec28f4d0c326ffc31466aa6e9668e8961aabb/moderngl-5.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e34d1cd38f7998258f76a08bb5e87f351ec653b7ea1928b2711f8719c10cefd1", size = 108514, upload-time = "2024-10-17T12:37:25.304Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moderngl-window"
|
||||
version = "3.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "moderngl" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyglet" },
|
||||
{ name = "pyglm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/02/92e235891300c901f59647112a0267a07454f58aeb2041aa44f6b85f9cb3/moderngl_window-3.1.1.tar.gz", hash = "sha256:29c2827505f87399f3461d480b2778910fddeebe44ea803301215cf212a6c1bc", size = 353495, upload-time = "2025-01-19T10:07:56.133Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/41/d3503f07de8ad3597165f88b07e1064629f82e65385c4307875b2f6b137f/moderngl_window-3.1.1-py3-none-any.whl", hash = "sha256:e3b3ac2b4e23afcbfdac1971318a4db893bed3ba6a8fbde3367b1226af39b2e5", size = 382381, upload-time = "2025-01-19T10:07:53.606Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycairo"
|
||||
version = "1.29.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/d9/1728840a22a4ef8a8f479b9156aa2943cd98c3907accd3849fb0d5f82bfd/pycairo-1.29.0.tar.gz", hash = "sha256:f3f7fde97325cae80224c09f12564ef58d0d0f655da0e3b040f5807bd5bd3142", size = 665871, upload-time = "2025-11-11T19:13:01.584Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/21/3f477dc318dd4e84a5ae6301e67284199d7e5a2384f3063714041086b65d/pycairo-1.29.0-cp313-cp313-win32.whl", hash = "sha256:3eb382a4141591807073274522f7aecab9e8fa2f14feafd11ac03a13a58141d7", size = 750949, upload-time = "2025-11-11T19:12:12.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/34/7d27a333c558d6ac16dbc12a35061d389735e99e494ee4effa4ec6d99bed/pycairo-1.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:91114e4b3fbf4287c2b0788f83e1f566ce031bda49cf1c3c3c19c3e986e95c38", size = 844149, upload-time = "2025-11-11T19:12:19.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/43/e782131e23df69e5c8e631a016ed84f94bbc4981bf6411079f57af730a23/pycairo-1.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:09b7f69a5ff6881e151354ea092137b97b0b1f0b2ab4eb81c92a02cc4a08e335", size = 693595, upload-time = "2025-11-11T19:12:23.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/fa/87eaeeb9d53344c769839d7b2854db7ff2cd596211e00dd1b702eeb1838f/pycairo-1.29.0-cp314-cp314-win32.whl", hash = "sha256:69e2a7968a3fbb839736257bae153f547bca787113cc8d21e9e08ca4526e0b6b", size = 767198, upload-time = "2025-11-11T19:12:42.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/90/3564d0f64d0a00926ab863dc3c4a129b1065133128e96900772e1c4421f8/pycairo-1.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:e91243437a21cc4c67c401eff4433eadc45745275fa3ade1a0d877e50ffb90da", size = 871579, upload-time = "2025-11-11T19:12:48.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/91/93632b6ba12ad69c61991e3208bde88486fdfc152be8cfdd13444e9bc650/pycairo-1.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:b72200ea0e5f73ae4c788cd2028a750062221385eb0e6d8f1ecc714d0b4fdf82", size = 719537, upload-time = "2025-11-11T19:12:55.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/23/37053c039f8d3b9b5017af9bc64d27b680c48a898d48b72e6d6583cf0155/pycairo-1.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5e45fce6185f553e79e4ef1722b8e98e6cde9900dbc48cb2637a9ccba86f627a", size = 874015, upload-time = "2025-11-11T19:12:28.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/54/123f6239685f5f3f2edc123f1e38d2eefacebee18cf3c532d2f4bd51d0ef/pycairo-1.29.0-cp314-cp314t-win_arm64.whl", hash = "sha256:caba0837a4b40d47c8dfb0f24cccc12c7831e3dd450837f2a356c75f21ce5a15", size = 721404, upload-time = "2025-11-11T19:12:36.919Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydub"
|
||||
version = "0.25.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyglet"
|
||||
version = "2.1.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/a6/ffd78b2d05a820ced85baa11210b8e5d0db0d951767021150be0a231df00/pyglet-2.1.14.tar.gz", hash = "sha256:feb3fba05a85f50f793688d88ddfbcdc57044d971f2d8d2107eb7578eb4af51e", size = 6595146, upload-time = "2026-04-05T08:24:35.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/d4/789d34a4674a4ccc0b65ad54d0f963f873d39000c3ebed0880574d5437e5/pyglet-2.1.14-py3-none-any.whl", hash = "sha256:70febf03f206607103e1f9806527019c589002626937aecdca4d095ce0a4b9b0", size = 1032928, upload-time = "2026-04-05T08:24:29.068Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyglm"
|
||||
version = "2.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/8b/bdaf7b9cacecd28f7b4c6fc2d7d136824c506ad38cfdb37a05ea7ec88694/pyglm-2.8.3.tar.gz", hash = "sha256:161781ea4d1267f796b645f85ebff53aeb8ee4f13b4e993c04d64c96d286e534", size = 584038, upload-time = "2025-11-26T12:12:59.47Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/22/ee11dff20adfc6aac3e0482ed275843e0b9854dbc2e812ab56cdacbed8d6/pyglm-2.8.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a6d07b6b73e55a36e1b6daa63acb53f4912f2ddf88f434af4cc70441435aef8", size = 1608547, upload-time = "2025-11-26T12:11:46.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5f/10c4cd636c3e6c63328a476f84bb03501d8016f12053c3a82ba588fc61ce/pyglm-2.8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eaf6241f7c3ff169575e11da78f4439422517e42558961332db4cd09e9599267", size = 1362651, upload-time = "2025-11-26T12:11:47.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/70/2c7fe768900ee9d0f87e7a89375fa7d83b5b0a8f0eee8d0ad06b22a96e37/pyglm-2.8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1ec0dfb8f2c848c4ee6330c70a1ca9333004776c8e5ec76096e0b67c739f688", size = 12157160, upload-time = "2025-11-26T12:11:49.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/a6/befefccf1c8a0a66f09a7a1a1d324a3f988cb5e6b634026280518e5a83cd/pyglm-2.8.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3eea9093210afb946769c84fe31f17a9f73696a1161ee84fb42e9255f8c5cc8a", size = 12787899, upload-time = "2025-11-26T12:11:50.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/d2/5475d0791b585ae26dc0d690862a0e1e6fda243552a1718628dd937e5543/pyglm-2.8.3-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:0c652650912dbd88994fa02ec9dba2f3b35dc3995427b6ae8056ae542d5a060a", size = 10886936, upload-time = "2025-11-26T12:11:53.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a1/035068410f60ed53007e0488af96f7e4634b679e9156d1090e70732b2159/pyglm-2.8.3-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:e3bdda68f75ad270e66b0fc7a1883749489cca14c44cbe22bac038fa170a8a1c", size = 11711601, upload-time = "2025-11-26T12:11:55.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e6/286d1879eae87199f086136b08c70a3bff27880417f98dadc0cbcdb65e50/pyglm-2.8.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08f8b1bd0d80ce396ee9cd5d3d4c7aeb4bbfa2a54dc73413924e3c9982412528", size = 11939755, upload-time = "2025-11-26T12:11:57.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/58/f40f109ac025bb18412db78e6e73ffc20a17b6b495de90352f5aabfb9982/pyglm-2.8.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12fa61feefa5a255097d0887415bce9dd72c1dd5e5a8c6577121ee5348336a16", size = 12774242, upload-time = "2025-11-26T12:11:59.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/fd/71b44ee5ac341e9979731a7b868e237f3775e140a9481cea79bae7abb83c/pyglm-2.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:33118ef1d678ab573546757dee7f0a1ca2fba8e8d7760c9fe6320fe0cfa3deb7", size = 1663010, upload-time = "2025-11-26T12:12:00.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/1e/9b8ba9d4627585797d8bda952412c93a4c09b70cb25e64756a52accddc1e/pyglm-2.8.3-cp313-cp313-win_arm64.whl", hash = "sha256:73ff3785dfc4ce017626d7ab56d6711a7119c29e2e71294efed73810c1d307f9", size = 1239893, upload-time = "2025-11-26T12:12:02.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/fe/494d7dce3fcaf0123e787320b4a558d8fe733d6d0e23b7fe51e687f88dc2/pyglm-2.8.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b312012458d0537b3d84f24f4ba51fd3930df7a0773fc643e36f8df27b807c7a", size = 1610593, upload-time = "2025-11-26T12:12:03.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/c0/37cced4a1b29957a29baebc1f0034be5d5419adf12da4b99140c56152cdb/pyglm-2.8.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3249bfe5352e18cc777fac591665679a99f270e1ff27cd11dc349af07684f007", size = 1366295, upload-time = "2025-11-26T12:12:04.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/ba/186176d1c3e26196ae4bec4b228bdbb1304576f2038a7f1635070923ee48/pyglm-2.8.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb8ea0d6721c763a26eacde59b2c9165719050dcf49c99a2857f6e1e5a5f30bb", size = 12140715, upload-time = "2025-11-26T12:12:05.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3e/8d9f307649e9b79b34ae51303e46e7012cc2550d60e6dd00ea8d3d8c9cf2/pyglm-2.8.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83effb89e2cf6dd79cf9ebecf2f9fbda3d25a92b61af264de3cadad408911de6", size = 12764551, upload-time = "2025-11-26T12:12:07.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d6/2481433abe537d7019d9ce75eba86b57e1731e3e0352fd1ac7f31f4d1883/pyglm-2.8.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:e958d65ed55f2716fd8a3a2ef872cc52893ea7300d7feec62dccb27ec25fbc2f", size = 10869966, upload-time = "2025-11-26T12:12:09.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/bf/3c78a9718e1d26c5b6ec468a584eb30cd797c46b6edd08e79cb1d67e8bf0/pyglm-2.8.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:ec7cc14d2eb9f46a18012ee7c1a164e0395b058ceb6e341bf6d986316b698574", size = 11693025, upload-time = "2025-11-26T12:12:10.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/0d/00ef293153b6ca7e56d1be6facbd9d0a8e331d0c6d3114f47f2cf2913eda/pyglm-2.8.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4beb0ada21e7641a577f274496451befcde79965467ef4027bd933334b3de39b", size = 11920727, upload-time = "2025-11-26T12:12:12.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/10/d8ecf9b5ac3b6fc1b179b33dfbd087b8f4d4b12bab99b5794f569a15a99a/pyglm-2.8.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77878c1b8c713b9e39fe32f870d82e20c864da3b11a3b875eca06c270b04cdbe", size = 12759345, upload-time = "2025-11-26T12:12:14.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/d1/cc1f75ee77fd2f9bcb122bc7f3a628710fe3f0bc517333fa9f38268a7065/pyglm-2.8.3-cp314-cp314-win_amd64.whl", hash = "sha256:15c77bc46ff69d945565309e13ca99c4a001d6a941a80c45f26fbdec80fa16c4", size = 1710524, upload-time = "2025-11-26T12:12:16.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/51/74c0a3107567dc769c06b6c04c7bc828f33792e089036c637334b7e4c573/pyglm-2.8.3-cp314-cp314-win_arm64.whl", hash = "sha256:2b16ec33bd43c514502bae8de2b319d168259090e101e1cde79cd0a7d33e1185", size = 1271521, upload-time = "2025-11-26T12:12:17.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/8e/867045b54da8257b21c71c0255ba7a231c251aae4b5f26b12eb50b651cc7/pyglm-2.8.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f1ab8debc06e0fdedb3f4285ded4bec38bf075652c393039838504767e6cf", size = 1633871, upload-time = "2025-11-26T12:12:18.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/f5/81bb8b52e132dc1ecc87b7ffb50c714b4fd2f71f98b801dd376e036f942b/pyglm-2.8.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7835d18747ab9f8e736e343cc35bee0a514f18add282f1fc8035945fcf9d9bd", size = 1387757, upload-time = "2025-11-26T12:12:20.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/40/8581283c00e2a18a6bb20a9192e25a792b605c53dee1c3abb8871eb0d3ca/pyglm-2.8.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af3ddee3d150bbef68ee7338ccd3e0710b75b08121b8efd52d786b0d2b6731be", size = 12724921, upload-time = "2025-11-26T12:12:21.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/0e/bc3c03038da822d1a66c38e166d4c89b6bdab846e576ac226442813af7f4/pyglm-2.8.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a76c8eaba0c58f5738e87be5efcd16c4e75540fe6ebfdf15c236a799a61358e", size = 13317946, upload-time = "2025-11-26T12:12:23.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/21/b1f52dc73d610e36aa3ccf3ef61634530e6f97c2ca809a057839bf578667/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:879ee9ab3c8ab47b1de59fe7e593eda854b8349f274ca60f057b83f3a405b84d", size = 11371582, upload-time = "2025-11-26T12:12:25.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/aa/e03cc7a2daceb1bbdf98780a991a323e4078254584cf6664984a626ecfe1/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:3e37c15b6c3e08f960b34ff9ec42e73469dfd868aec214e8a347da6c9d0245d6", size = 12192848, upload-time = "2025-11-26T12:12:28.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/07/be893aee24a9a8b5400feda4d032fad9d281047d4379aa9f5ef4bea1f6a4/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd34a8670debef4a55bc756b14cbe8b0a4daa49f8f6850c86c5e11d20554927f", size = 12414214, upload-time = "2025-11-26T12:12:30.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/06/6bb4e8a09f7dd2bf8dc1f4cc5edc938fc1247a1b2770833515fb418fc695/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:699f852e0335b79d0b664ba1c2d02cb4689256cda786e7780e821f60b0824c46", size = 13237516, upload-time = "2025-11-26T12:12:32.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/5c/7f15edd05020540748dad8f5eae0a07ed7f14f699bff530172b8850bd998/pyglm-2.8.3-cp314-cp314t-win_amd64.whl", hash = "sha256:78caadaf9cc2ddea1c55b0d44fa8032f35c9f821a6f152b72422e5657d38f01d", size = 1764614, upload-time = "2025-11-26T12:12:34.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f9/8bc8d010503a250319c55d2ceec5f90b0e807c7f85d581638c5ecd0a81de/pyglm-2.8.3-cp314-cp314t-win_arm64.whl", hash = "sha256:69400ad1852ca0972e4d9cbef9d9510941d4b81dc0fffebc5ac796a85440a119", size = 1292992, upload-time = "2025-11-26T12:12:35.681Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "screeninfo"
|
||||
version = "0.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cython", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skia-pathops"
|
||||
version = "0.9.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/f6/ab37d6fa21f25965d4ad059745c76f13ddfb92a2c06a842a42ad77961c24/skia_pathops-0.9.2.tar.gz", hash = "sha256:4b6d8459f6f4a69282cb26fca0c2bb0b321cc58a9bf9cc6579a52a391edc0319", size = 65115346, upload-time = "2026-02-16T10:30:22.753Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/10/45af3ea2dee706a340601da315e450c449f76aaa18651cfe2677b0c65161/skia_pathops-0.9.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:c7a925b919c050df1e0545d26a69b64d9c8e1959e4dca73b06df088bf01d30f0", size = 2897494, upload-time = "2026-02-16T10:30:00.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/20/711d733b252696aedd78175e0f60acd42299de680d5a50a2750b96578841/skia_pathops-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d94c18a60b1e56910240a65dcc841cca37067f6432a4881d20baff17b6d77915", size = 3300314, upload-time = "2026-02-16T10:30:02.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/47/bccbdb7ee12bbcc43c1051f9fd6933d16e3ffc8f20263155bd03709bde9e/skia_pathops-0.9.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b28883f209a0df84da14e980e7a07c55ed93b913955694d90ddef2f29a261b", size = 2999370, upload-time = "2026-02-16T10:30:04.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/55/d5a1925d61ab683aaf08a3a80749371991f82fdeb53e3dbeae0046ce80ac/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8122e50e03472657781c951f3019d9b85d9b1cbc93ebac635b673f99b18895b6", size = 3955374, upload-time = "2026-02-16T10:30:06.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/49/c81e1a551726b8153c7a600b041e4480ac85db13985b7b260a01d85e0a6c/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f8c72c70e57dd6655f880ae63642ff26753bb888f4c6411709a12cc10bee005e", size = 4316667, upload-time = "2026-02-16T10:30:08.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/b4/484ba09325c75d4470578b3f16bfe5a30cc348ece2566df59702c487084c/skia_pathops-0.9.2-cp310-abi3-win32.whl", hash = "sha256:3b9ef6629d2c8cb8604a7e22d4d2fe2c0160788b9792145a2cf0f968ec245751", size = 1450912, upload-time = "2026-02-16T10:30:09.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/96/bd1bfaf5335242a86d8af940ff2124861e9bc6a5520cf96fda374fd10945/skia_pathops-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:9ae72d801b5f4c5dbb937fb25476a12fa2da70860f28dd637da2b529dbd8271c", size = 1782901, upload-time = "2026-02-16T10:30:11.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srt"
|
||||
version = "3.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" }
|
||||
|
||||
[[package]]
|
||||
name = "svgelements"
|
||||
version = "1.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/29/1c93c94a2289675ba2ff898612f9c9a03f46d69f253bdf4da0dfc08a599d/svgelements-1.9.6.tar.gz", hash = "sha256:7c02ad6404cd3d1771fd50e40fbfc0550b0893933466f86a6eb815f3ba3f37f7", size = 162145, upload-time = "2023-08-17T02:01:51.822Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2c/6c9bb53db56c8a12a736d2158a8b842a5993b96daabc29d90a098e840280/svgelements-1.9.6-py2.py3-none-any.whl", hash = "sha256:8a5cf2cc066d98e713d5b875b1d6e5eeb9b92e855e835ebd7caab2713ae1dcad", size = 137856, upload-time = "2023-08-17T02:01:48.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||
]
|
||||
+3
-4
@@ -7,13 +7,12 @@
|
||||
"dev": "concurrently -n site,cms -c blue,magenta \"pnpm --filter site dev\" \"pnpm --filter cms develop\"",
|
||||
"dev:site": "pnpm --filter site dev",
|
||||
"dev:cms": "pnpm --filter cms develop",
|
||||
"build": "pnpm run build:site && pnpm run build:cms",
|
||||
"build:site": "pnpm --filter site build",
|
||||
"build:cms": "pnpm --filter cms build",
|
||||
"lighthouse": "pnpm --filter site lighthouse",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"legacy:generate:llms": "node scripts/generate-llms.mjs",
|
||||
"legacy:generate:og": "node scripts/generate-og-images.mjs",
|
||||
"legacy:build": "npm run legacy:generate:llms && npm run legacy:generate:og && zola build"
|
||||
"lint:fix": "biome check --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.5.11",
|
||||
|
||||
Generated
+1455
-4
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
// 404 — Document Not Found
|
||||
.notfound-wrap {
|
||||
min-height: 60dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.case-notice {
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.case-notice-ref {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.case-notice-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.case-notice-body {
|
||||
color: var(--text-muted);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.case-notice-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,611 +0,0 @@
|
||||
// Article details and content styling
|
||||
|
||||
.reading-progress-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: transparent;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.reading-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: var(--accent-color);
|
||||
transition: width 0.1s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
// Post Detail
|
||||
.post-detail {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.post-meta-label {
|
||||
font-style: normal;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--accent-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.post-meta-sep {
|
||||
color: var(--border-color);
|
||||
font-style: normal;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.post-meta-updated time {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
// Heading anchor links
|
||||
.post-body {
|
||||
h2,
|
||||
h3 {
|
||||
position: relative;
|
||||
|
||||
.heading-anchor {
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
color: var(--accent-color);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 400;
|
||||
font-size: 0.75em;
|
||||
opacity: 0;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
transition: opacity 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:hover .heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Series Navigation Box
|
||||
// ──────────────────────────────────────────
|
||||
.series-nav {
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid var(--accent-color);
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.series-nav-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.series-nav-eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--accent-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.series-nav-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--heading-color, var(--text-color));
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.series-nav-badge {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.1em 0.6em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.series-nav-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.series-nav-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
|
||||
&.is-current {
|
||||
.series-nav-index {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
.series-nav-label {
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.series-nav-index {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
width: 1.5ch;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.series-nav-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.post-toc {
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
summary {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
list-style: none; /* Hide default arrow */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&::before {
|
||||
content: '↓';
|
||||
color: var(--accent-color);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&[open] summary::before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95em;
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-tags-share {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
margin-top: 4rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.social-share {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
.share-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
transition: var(--transition-smooth);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-color);
|
||||
transform: translateY(-2px);
|
||||
border-color: transparent;
|
||||
|
||||
&.x {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
&.bluesky {
|
||||
background: #0285ff;
|
||||
}
|
||||
&.linkedin {
|
||||
background: #0077b5;
|
||||
}
|
||||
&.reddit {
|
||||
background: #ff4500;
|
||||
}
|
||||
&.hackernews {
|
||||
background: #ff6600;
|
||||
}
|
||||
&.email {
|
||||
background: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.post-nav-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
max-width: 34ch;
|
||||
}
|
||||
|
||||
.post-nav-label {
|
||||
color: var(--accent-color);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.post-nav-title {
|
||||
font-size: 1rem;
|
||||
line-height: 1.35;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.post-nav-link:hover .post-nav-title,
|
||||
.post-nav-link:hover .post-nav-label {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.post-nav-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.post-nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.post-nav-prev {
|
||||
justify-self: start;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post-nav-next {
|
||||
justify-self: end;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Sidebar TOC Layout
|
||||
// ──────────────────────────────────────────
|
||||
|
||||
.post-layout {
|
||||
display: contents;
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 800px) 240px;
|
||||
gap: 3rem;
|
||||
align-items: start;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.post-detail {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Hide inline TOC — sidebar takes over
|
||||
.post-toc {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.post-toc-sidebar {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 5.5rem;
|
||||
max-height: calc(100vh - 7rem);
|
||||
overflow-y: auto;
|
||||
padding-top: 4rem; // align with article content start
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-color) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toc-nav {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
// Nested level (h2 children)
|
||||
ul {
|
||||
padding-left: 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
padding: 0.2rem 0;
|
||||
border-left: 2px solid transparent;
|
||||
padding-left: 0.75rem;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
&.toc-active {
|
||||
color: var(--accent-color);
|
||||
border-left-color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toc-sidebar-title {
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 1rem 0;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Media Wrappers (YouTube, etc)
|
||||
// ──────────────────────────────────────────
|
||||
.youtube-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
margin: 2rem 0;
|
||||
background: #000;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Callouts / Admonitions
|
||||
// ──────────────────────────────────────────
|
||||
.callout {
|
||||
margin: 2rem 0;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-left: 4px solid var(--callout-color, var(--border-color));
|
||||
background: var(--surface-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
|
||||
.callout-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--callout-color, var(--heading-color));
|
||||
}
|
||||
|
||||
.callout-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.callout-content {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Variants
|
||||
&.callout-note {
|
||||
--callout-color: #3b82f6; // Blue
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
&.callout-warning {
|
||||
--callout-color: #f59e0b; // Amber
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
}
|
||||
|
||||
&.callout-tip {
|
||||
--callout-color: #16a34a; // Green
|
||||
background: rgba(22, 163, 74, 0.05);
|
||||
}
|
||||
|
||||
&.callout-danger {
|
||||
--callout-color: #ef4444; // Red
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.comments {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Related Posts
|
||||
// ──────────────────────────────────────────
|
||||
.related-posts-section {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
|
||||
h3 {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.5rem;
|
||||
color: var(--heading-color);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.related-posts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
// Override some constraints from feed-page cards when inside the grid
|
||||
.feed-item {
|
||||
border-bottom: none !important;
|
||||
padding-bottom: 0 !important;
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 1.5rem !important;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: var(--transition-smooth);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.item-snippet {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-304
@@ -1,304 +0,0 @@
|
||||
// Post Feed Page Container
|
||||
.feed-page {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// Post Feed
|
||||
.post-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.post-feed .feed-item {
|
||||
padding: 2rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Homepage "View all posts" CTA
|
||||
.view-all-posts {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.btn-view-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1.5rem;
|
||||
border: 1px solid var(--accent-color);
|
||||
border-radius: 2px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875rem;
|
||||
color: var(--accent-color);
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
transition: var(--transition-smooth);
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
&::after {
|
||||
content: '→';
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
|
||||
&::after {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Homepage post list (post-card.html) — a numbered index of entries
|
||||
.feed-heading {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.post-feed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feed-item.card {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 2rem 0;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
|
||||
a {
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .item-title a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.item-index {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
padding-top: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.item-meta-sep {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.item-snippet {
|
||||
color: var(--text-color);
|
||||
opacity: 0.9;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.item-readmore {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
color: var(--accent-color);
|
||||
|
||||
.readmore-arrow {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover .readmore-arrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.item-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.item-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-date-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.item-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.item-reading-time {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.5rem, 2vw, 2rem);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.item-link:hover .item-title {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Pagination
|
||||
// ──────────────────────────────────────────
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 5rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.pagination-pages {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
// Shared base for arrows + numbers
|
||||
.pagination-arrow,
|
||||
.pagination-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--surface-color);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition-smooth);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
// Active / current page
|
||||
.pagination-num.is-current {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled arrow (no prev/next page)
|
||||
.pagination-arrow.is-disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-muted);
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Ellipsis gap marker
|
||||
.pagination-ellipsis {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
// "Page N of M" helper text
|
||||
.pagination-meta {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Footer
|
||||
.site-footer {
|
||||
padding: 2rem 0 3rem;
|
||||
border-top: 3px solid var(--text-color);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font-mono);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// Component: Header
|
||||
.site-header {
|
||||
padding: 1.25rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background-color: var(--bg-color);
|
||||
border-bottom: 2px solid var(--text-color);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Right side wrapper (links + controls)
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
// Always-visible icon controls
|
||||
.nav-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0.35rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: var(--transition-smooth);
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Hamburger button — hidden on desktop, shown on mobile
|
||||
.nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.close-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
.hamburger-icon {
|
||||
display: none;
|
||||
}
|
||||
.close-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nav-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// Hide the site name when the menu is open
|
||||
.nav-container:has(#nav-toggle[aria-expanded='true']) .brand-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Make nav-right fill the full width so menu items span the container
|
||||
.nav-container:has(#nav-toggle[aria-expanded='true']) .nav-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 0 !important;
|
||||
width: 100%;
|
||||
order: 10; // push below nav-controls
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
border-top: 1px solid transparent;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
opacity 0.2s ease;
|
||||
|
||||
&.is-open {
|
||||
padding: 1rem 0;
|
||||
margin-top: 0.5rem;
|
||||
border-top-color: var(--border-color);
|
||||
max-height: 320px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
padding: 0.75rem 0;
|
||||
font-size: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Brand reads like a running header on a printed document — quiet,
|
||||
// small, letter-spaced — not competing with the masthead title below it.
|
||||
.brand-text {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.brand-link:hover .brand-text {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
background-color: var(--accent-color);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.nav-item:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.nav-item.is-active {
|
||||
color: var(--accent-color);
|
||||
|
||||
&::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Hero Section
|
||||
.hero {
|
||||
padding: 8rem 0 6rem 0;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.hero-compact {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: clamp(1.1rem, 1.5vw, 1.3rem);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
// Homepage masthead — spans the full width of .site-container, matching the navbar
|
||||
.masthead {
|
||||
padding: 4rem 0 3rem;
|
||||
}
|
||||
|
||||
.masthead-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.masthead-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// Colophon-style contact line — quiet, small, middot-separated
|
||||
.masthead-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.4rem;
|
||||
margin-top: 2rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--text-muted);
|
||||
transition: var(--transition-smooth);
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '·';
|
||||
margin: 0 0.75rem;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Layout Constraints
|
||||
.site-container {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 5vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Constrain all media & tables to their container
|
||||
img,
|
||||
video,
|
||||
iframe,
|
||||
table,
|
||||
svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
@media (max-width: 640px) {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Archive & Tags list pages
|
||||
|
||||
.list-page-header {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.list-page-title {
|
||||
font-size: clamp(2.5rem, 5vw, 4.5rem);
|
||||
}
|
||||
|
||||
.term-hash {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tag-description {
|
||||
max-width: 65ch;
|
||||
margin: 0.75rem 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
max-width: 65ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.archive-year {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.archive-year-title {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.archive-items {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.archive-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.archive-date {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.archive-link {
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
// Tags index
|
||||
.tags-page {
|
||||
max-width: 65ch;
|
||||
margin: 0 auto;
|
||||
padding-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tags-page-header {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 400;
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
font-size: 0.7em;
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.15rem;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Global Reset & Base
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-sans);
|
||||
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
|
||||
// Firefox
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
}
|
||||
|
||||
// Webkit
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 0; // hard edges = terminal feel
|
||||
border: 1px solid var(--scrollbar-track);
|
||||
|
||||
&:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.7;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-wrap: break-word; // break long words instead of overflowing
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Scroll to Top Floating Button
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-color);
|
||||
color: var(--accent-color);
|
||||
border: 1px solid var(--accent-color);
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
|
||||
// Hidden state by default
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(1rem);
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
opacity 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
visibility 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
box-shadow 0.2s ease-out;
|
||||
|
||||
// Visible state triggered by JS
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
transform: translateY(-0.25rem);
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent-color) 20%, transparent);
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
// Responsive padding for mobile devices
|
||||
@media (max-width: 768px) {
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Search Modal Styles
|
||||
.search-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: 10vh;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
visibility 0.2s ease;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
.search-modal {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 12px 32px var(--shadow-color);
|
||||
transform: translateY(-20px) scale(0.98);
|
||||
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--surface-color);
|
||||
|
||||
svg {
|
||||
color: var(--text-muted);
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
#search-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.search-footer {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: var(--surface-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
|
||||
kbd {
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.1rem 0.3rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
margin: 0 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
|
||||
// Custom scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 2px;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.15s ease;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
background-color: var(--surface-color);
|
||||
border-left-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 500;
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.result-summary {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Component: Theme Toggle
|
||||
.theme-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: var(--transition-smooth);
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .sun-icon {
|
||||
display: block;
|
||||
}
|
||||
html.dark .moon-icon {
|
||||
display: none;
|
||||
}
|
||||
html:not(.dark) .sun-icon {
|
||||
display: none;
|
||||
}
|
||||
html:not(.dark) .moon-icon {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Cross-Document View Transitions
|
||||
* Enabled via <meta name="view-transition" content="same-origin" />
|
||||
*/
|
||||
|
||||
// Disable default CSS transitions on the whole page during a view transition
|
||||
// so they don't fight with the View Transition API animation.
|
||||
html::view-transition-group(*) {
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
// Custom animation for the old page leaving
|
||||
@keyframes fade-out-scale {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom animation for the new page entering
|
||||
@keyframes fade-in-scale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the custom animations to the root view transition pseudo-elements
|
||||
html::view-transition-old(root) {
|
||||
animation: fade-out-scale 0.25s cubic-bezier(0.4, 0, 1, 1) both;
|
||||
}
|
||||
|
||||
html::view-transition-new(root) {
|
||||
animation: fade-in-scale 0.35s cubic-bezier(0, 0, 0.2, 1) both;
|
||||
// Delay slightly so the old page fades down first before the new one pops over
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
|
||||
// Respect user preference for reduced motion
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html::view-transition-group(*),
|
||||
html::view-transition-old(*),
|
||||
html::view-transition-new(*) {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
// Typography Scale (Fluid)
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.01em;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.75em;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.75rem, 5vw, 4.5rem);
|
||||
margin-top: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: clamp(1.35rem, 3vw, 2.5rem);
|
||||
}
|
||||
h3 {
|
||||
font-size: clamp(1.2rem, 2vw, 1.8rem);
|
||||
}
|
||||
h4 {
|
||||
font-size: clamp(1.05rem, 1.5vw, 1.3rem);
|
||||
}
|
||||
h5 {
|
||||
font-size: clamp(1rem, 1.25vw, 1.15rem);
|
||||
}
|
||||
h6 {
|
||||
font-size: clamp(0.9rem, 1vw, 1rem);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
figure,
|
||||
blockquote,
|
||||
pre,
|
||||
table {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 65ch; // Optimal reading width
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
// Footnotes
|
||||
.footnote-reference {
|
||||
font-size: 0.75em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
font-family: var(--font-mono);
|
||||
padding: 0 0.15em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-definition {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
border-left: 2px solid var(--border-color);
|
||||
padding-left: 1rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-definition-label {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-color);
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-color);
|
||||
font-size: 0.9em;
|
||||
text-decoration: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
font-feature-settings:
|
||||
'liga' 1,
|
||||
'calt' 1;
|
||||
font-variant-ligatures: contextual;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1.25rem;
|
||||
border-radius: 2px;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
background: #282828; // Gruvbox Dark bg0 (matches [markdown.highlighting] theme)
|
||||
// Scrollbars visible on desktop, hidden on mobile
|
||||
@media (max-width: 640px) {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
font-size: 0.82em;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(235, 219, 178, 0.1);
|
||||
color: #ebdbb2;
|
||||
border: 1px solid rgba(235, 219, 178, 0.2);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(235, 219, 178, 0.2);
|
||||
color: #fbf1c7;
|
||||
}
|
||||
|
||||
&.copied {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Button is always visible, no hover-to-reveal needed
|
||||
|
||||
code {
|
||||
background: var(--surface-color);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 2px;
|
||||
color: var(--accent-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Tag chip — a small filed label, shared by post-card and post-tags
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 2px;
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--text-muted);
|
||||
transition: var(--transition-smooth);
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Heavy hairline rule — the site's one recurring structural device
|
||||
// (masthead, between list entries, above case-reference blocks)
|
||||
.rule {
|
||||
border: none;
|
||||
border-top: 3px solid var(--text-color);
|
||||
}
|
||||
|
||||
// Utilities / Interactions
|
||||
.hover-scale {
|
||||
transition: transform 140ms ease-out;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Design Variables — "Case File": a declassified incident-report aesthetic.
|
||||
// One accent only; flat colors + hairline borders, no gradients or glass blur.
|
||||
:root {
|
||||
--bg-color: #e9e9e2; // paper (cool bone, not warm cream)
|
||||
--surface-color: #dedecf; // slightly recessed paper — cards, code blocks
|
||||
--text-color: #17181c; // ink
|
||||
--text-muted: #68675f; // faded ink
|
||||
--accent-color: #b42318; // classified-stamp red
|
||||
--accent-hover: #8e1b12;
|
||||
--border-color: rgba(23, 24, 28, 0.16);
|
||||
--shadow-color: rgba(23, 24, 28, 0.08);
|
||||
|
||||
--font-sans: 'Source Serif 4', Georgia, serif; // body copy
|
||||
--font-display: 'IBM Plex Serif', Georgia, serif; // headings, masthead
|
||||
--font-mono: 'JetBrains Mono', monospace; // case-reference lines, tags, code
|
||||
|
||||
--transition-smooth:
|
||||
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
border-color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
||||
--scrollbar-track: #dedecf;
|
||||
--scrollbar-thumb: #b0afa2;
|
||||
--scrollbar-thumb-hover: #b42318;
|
||||
}
|
||||
|
||||
// Dark mode — same document, read at night
|
||||
html.dark {
|
||||
--bg-color: #131311;
|
||||
--surface-color: #1b1a17;
|
||||
--text-color: #e9e7de;
|
||||
--text-muted: #8b8a80;
|
||||
--accent-color: #ff5a46;
|
||||
--accent-hover: #ff7c6b;
|
||||
--border-color: rgba(233, 231, 222, 0.14);
|
||||
--shadow-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
--scrollbar-track: #1b1a17;
|
||||
--scrollbar-thumb: #3a382f;
|
||||
--scrollbar-thumb-hover: #ff5a46;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Main SCSS entry point
|
||||
// Organized by feature and section for maintainability
|
||||
|
||||
@import 'variables';
|
||||
@import 'reset';
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'header';
|
||||
@import 'theme';
|
||||
@import 'hero';
|
||||
@import 'feed';
|
||||
@import 'lists';
|
||||
@import 'article';
|
||||
@import 'footer';
|
||||
@import 'search';
|
||||
@import 'transitions';
|
||||
@import 'utilities';
|
||||
@import 'scroll-top';
|
||||
@import '404';
|
||||
@@ -1,182 +0,0 @@
|
||||
// One-off migration: converts content/posts/*.md (legacy Zola posts) into
|
||||
// content/writings/<slug>/index.md (new schema). Additive only — never
|
||||
// touches content/posts/. Re-runnable: overwrites its own prior output.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
parse as parseToml,
|
||||
stringify as stringifyToml,
|
||||
TomlDate,
|
||||
} from "smol-toml";
|
||||
|
||||
const root = process.cwd();
|
||||
const postsDir = path.join(root, "content", "posts");
|
||||
const writingsDir = path.join(root, "content", "writings");
|
||||
|
||||
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
|
||||
|
||||
function walk(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return walk(full);
|
||||
return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function slugFor(file) {
|
||||
const rel = path.relative(postsDir, file).split(path.sep).join("/");
|
||||
return rel.endsWith("/index.md")
|
||||
? rel.slice(0, -"/index.md".length)
|
||||
: rel.slice(0, -".md".length);
|
||||
}
|
||||
|
||||
function compact(obj) {
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
if (Array.isArray(value) && value.length === 0) continue;
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
!(value instanceof Date)
|
||||
) {
|
||||
const nested = compact(value);
|
||||
if (Object.keys(nested).length === 0) continue;
|
||||
out[key] = nested;
|
||||
continue;
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeTag(tag) {
|
||||
return tag.trim().toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function toIsoDate(value) {
|
||||
if (value instanceof Date)
|
||||
return new TomlDate(value.toISOString().slice(0, 10));
|
||||
if (typeof value === "string") return new TomlDate(value.slice(0, 10));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Converts the one unambiguous Zola shortcode in use (youtube embeds) to a
|
||||
* plain HTML iframe, and strips Zola's `{% raw %}`/`{% endraw %}` escape
|
||||
* tags (meaningless now — there's no template engine processing the body).
|
||||
* Anything else Zola-specific (note/tip/warning/danger callouts) is left
|
||||
* as-is: those need real Astro components before they can be translated,
|
||||
* so they're flagged for manual follow-up instead of guessed at. */
|
||||
function migrateBody(body) {
|
||||
const flags = [];
|
||||
const out = body
|
||||
.replace(/\{%\s*raw\s*%\}\r?\n?/g, "")
|
||||
.replace(/\{%\s*endraw\s*%\}\r?\n?/g, "")
|
||||
.replace(/\{\{<\s*youtube\s+id="([^"]+)"[\s\S]*?\/>\s*\}\}/g, (_m, id) => {
|
||||
return `<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${id}" title="YouTube video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
|
||||
});
|
||||
|
||||
if (/\{%\s*<?\/?(note|tip|warning|danger)>?\s*%\}/.test(out)) {
|
||||
flags.push("callout-shortcode");
|
||||
}
|
||||
if (/\{\{|\{%/.test(out)) {
|
||||
flags.push("unresolved-template-syntax");
|
||||
}
|
||||
return { body: out, flags };
|
||||
}
|
||||
|
||||
const files = walk(postsDir).filter((f) => path.basename(f) !== "_index.md");
|
||||
fs.mkdirSync(writingsDir, { recursive: true });
|
||||
|
||||
let migrated = 0;
|
||||
const flaggedFiles = [];
|
||||
const migratedSlugs = [];
|
||||
|
||||
for (const file of files) {
|
||||
const raw = fs.readFileSync(file, "utf8");
|
||||
const match = raw.match(FRONTMATTER_RE);
|
||||
if (!match) continue;
|
||||
const [, fmSource, body] = match;
|
||||
const fm = parseToml(fmSource);
|
||||
const slug = slugFor(file);
|
||||
|
||||
const { body: newBody, flags } = migrateBody(body.trim());
|
||||
if (flags.length) flaggedFiles.push({ slug, flags });
|
||||
|
||||
const frontmatter = stringifyToml(
|
||||
compact({
|
||||
title: fm.title,
|
||||
description: fm.description,
|
||||
author: fm.author,
|
||||
date: toIsoDate(fm.date),
|
||||
updated: toIsoDate(fm.updated),
|
||||
draft: fm.draft ?? false,
|
||||
canonical_url: fm.canonical_url,
|
||||
taxonomies: { tags: (fm.taxonomies?.tags ?? []).map(normalizeTag) },
|
||||
extra: {
|
||||
series: fm.extra?.series,
|
||||
series_index: fm.extra?.series_index,
|
||||
llms_description: fm.extra?.llms_description,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const outDir = path.join(writingsDir, slug);
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, "index.md"),
|
||||
`+++\n${frontmatter}\n+++\n\n${newBody}\n`,
|
||||
);
|
||||
migrated += 1;
|
||||
migratedSlugs.push(slug);
|
||||
}
|
||||
|
||||
// Keep old /posts/<slug>/ URLs alive (RSS subscribers, inbound links) by
|
||||
// redirecting to the new /writings/<slug>/ path. Regenerated idempotently
|
||||
// inside marker comments so re-running this script doesn't duplicate or
|
||||
// clobber the rest of sws.toml.
|
||||
const swsPath = path.join(root, "sws.toml");
|
||||
const BEGIN =
|
||||
"# BEGIN migrated-posts-redirects (generated by scripts/migrate-posts-to-writings.mjs)";
|
||||
const END = "# END migrated-posts-redirects";
|
||||
const redirectBlock = [
|
||||
BEGIN,
|
||||
...migratedSlugs
|
||||
.sort()
|
||||
.flatMap((slug) => [
|
||||
"[[advanced.redirects]]",
|
||||
`source = "/posts/${slug}/"`,
|
||||
`destination = "/writings/${slug}/"`,
|
||||
"kind = 301",
|
||||
"",
|
||||
]),
|
||||
END,
|
||||
]
|
||||
.join("\n")
|
||||
.trimEnd();
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const swsSource = fs.readFileSync(swsPath, "utf8");
|
||||
const markerRe = new RegExp(
|
||||
`${escapeRegExp(BEGIN)}[\\s\\S]*?${escapeRegExp(END)}`,
|
||||
);
|
||||
const updatedSws = markerRe.test(swsSource)
|
||||
? swsSource.replace(markerRe, redirectBlock)
|
||||
: `${swsSource.trimEnd()}\n\n${redirectBlock}\n`;
|
||||
fs.writeFileSync(swsPath, `${updatedSws.trimEnd()}\n`);
|
||||
|
||||
console.log(
|
||||
`Migrated ${migrated} post(s) from content/posts/ to content/writings/.`,
|
||||
);
|
||||
console.log(`Wrote ${migratedSlugs.length} redirect(s) to sws.toml.`);
|
||||
if (flaggedFiles.length) {
|
||||
console.log(
|
||||
"\nNeeds manual follow-up (Zola-specific syntax with no Astro equivalent yet):",
|
||||
);
|
||||
for (const { slug, flags } of flaggedFiles) {
|
||||
console.log(` - ${slug}: ${flags.join(", ")}`);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEalcjtBYJKwYBBAHaRw8BAQdA55X1mRCKw9DA35kCAy+7Z30ye0rUHWBZsl7O
|
||||
Wmar+RS0IklhbiBBbmR3YXRpIDxhbmR3YXRpaWFuQGdtYWlsLmNvbT6IkwQTFgoA
|
||||
OxYhBK1YqreiDYUjy/WaeR8g/tCqBbDaBQJqVyO0AhsDBQsJCAcCAiICBhUKCQgL
|
||||
AgQWAgMBAh4HAheAAAoJEB8g/tCqBbDace8BAN1SpkEMSYll+XupKHiaIUwnhm4N
|
||||
eS79G1i+345Gl6CyAQDjgn+3rzG6sexjKh+10EKiy5omsQEGzUW03s7gS0T1Drg4
|
||||
BGpXI7QSCisGAQQBl1UBBQEBB0CXofBIYYXG2+dZqgllhsbkNPF4qlyfPgbRW+zb
|
||||
7Kw3IAMBCAeIeAQYFgoAIBYhBK1YqreiDYUjy/WaeR8g/tCqBbDaBQJqVyO0AhsM
|
||||
AAoJEB8g/tCqBbDauMkA/RG4iqtPZOI3DQzFJYu7SnxaD81vdP0LyTLKDQkynySu
|
||||
AQDrlWyF8LRlad4iX1yQPCuOV0aUpTNq5kPlWurtX99/BQ==
|
||||
=uKsS
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,17 +0,0 @@
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
Contact: mailto:[email protected]
|
||||
Expires: 2027-07-16T00:00:00.000Z
|
||||
Encryption: https://andwati.com/.well-known/pgp-key.txt
|
||||
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/AD58AAB7A20D8523CBF59A791F20FED0AA05B0DA
|
||||
Encryption: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xAD58AAB7A20D8523CBF59A791F20FED0AA05B0DA
|
||||
Preferred-Languages: en
|
||||
Canonical: https://andwati.com/.well-known/security.txt
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEARYKAB0WIQStWKq3og2FI8v1mnkfIP7QqgWw2gUCaljXlQAKCRAfIP7QqgWw
|
||||
2qAoAP0Q7Cc0YsP2dvV7fqDlqcnjuJepQQSTZBz7ehkPyjGV5gEA3K9ZqQmuI2ea
|
||||
KKADWxuKTTY43PBmuV9rMqLm8ZOPlAY=
|
||||
=NUkY
|
||||
-----END PGP SIGNATURE-----
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,75 +0,0 @@
|
||||
/* IBM Plex Serif — display/headings */
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Serif';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Serif';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('../fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-600.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Serif';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* Source Serif 4 — body copy */
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../fonts/source-serif-4/source-serif-4-v14-latin-regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../fonts/source-serif-4/source-serif-4-v14-latin-italic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('../fonts/source-serif-4/source-serif-4-v14-latin-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* JetBrains Mono (official release — full ligature support) — case-reference lines, tags, code */
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../fonts/jetbrains-mono/JetBrainsMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../fonts/jetbrains-mono/JetBrainsMono-Italic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../fonts/jetbrains-mono/JetBrainsMono-Bold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2') format('woff2');
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
-1128
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user