build!: upgrade to Tailwind CSS v4

This commit is contained in:
satnaing
2025-02-25 20:35:00 +07:00
committed by Sat Naing
parent 360278277d
commit 172e14b5f4
36 changed files with 7631 additions and 10633 deletions
+5 -5
View File
@@ -1,7 +1,7 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
import react from "@astrojs/react"; import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind";
import remarkToc from "remark-toc"; import remarkToc from "remark-toc";
import remarkCollapse from "remark-collapse"; import remarkCollapse from "remark-collapse";
import { SITE } from "./src/config"; import { SITE } from "./src/config";
@@ -10,9 +10,6 @@ import { SITE } from "./src/config";
export default defineConfig({ export default defineConfig({
site: SITE.website, site: SITE.website,
integrations: [ integrations: [
tailwind({
applyBaseStyles: false,
}),
react(), react(),
sitemap({ sitemap({
filter: page => SITE.showArchives || !page.endsWith("/archives"), filter: page => SITE.showArchives || !page.endsWith("/archives"),
@@ -35,9 +32,12 @@ export default defineConfig({
}, },
}, },
vite: { vite: {
plugins: [tailwindcss()],
optimizeDeps: { optimizeDeps: {
exclude: ["@resvg/resvg-js"], exclude: ["@resvg/resvg-js"],
}, },
}, },
scopedStyleStrategy: "where", experimental: {
svg: true,
},
}); });
-9421
View File
File diff suppressed because it is too large Load Diff
+17 -16
View File
@@ -1,37 +1,38 @@
{ {
"name": "astro-paper", "name": "astro-paper-v5",
"version": "4.8.0", "type": "module",
"private": false, "version": "0.0.1",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "build": "astro check && astro build",
"build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"sync": "astro sync",
"astro": "astro", "astro": "astro",
"format:check": "prettier --check . --plugin=prettier-plugin-astro", "format:check": "prettier --check .",
"format": "prettier --write . --plugin=prettier-plugin-astro", "format": "prettier --write .",
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/react": "^4.2.0",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"@resvg/resvg-js": "^2.6.2", "@resvg/resvg-js": "^2.6.2",
"astro": "^5.2.5", "@tailwindcss/vite": "^4.0.8",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"astro": "^5.3.1",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"lodash.kebabcase": "^4.1.1", "lodash.kebabcase": "^4.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"remark-collapse": "^0.1.2", "remark-collapse": "^0.1.2",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"satori": "^0.12.1", "satori": "^0.12.1",
"tailwindcss": "^3.4.11", "sharp": "^0.33.5",
"typescript": "^5.7.3" "tailwindcss": "^4.0.8"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/react": "^4.2.0", "@astrojs/check": "^0.9.4",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^6.0.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/github-slugger": "^1.3.0",
"@types/lodash.kebabcase": "^4.1.9", "@types/lodash.kebabcase": "^4.1.9",
"@typescript-eslint/parser": "^8.25.0", "@typescript-eslint/parser": "^8.25.0",
"eslint": "^9.21.0", "eslint": "^9.21.0",
+6989
View File
File diff suppressed because it is too large Load Diff
-211
View File
@@ -1,211 +0,0 @@
const socialIcons = {
Github: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
></path>
</svg>`,
Facebook: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M7 10v4h3v7h4v-7h3l1 -4h-4v-2a1 1 0 0 1 1 -1h3v-4h-3a5 5 0 0 0 -5 5v2h-3"
></path>
</svg>`,
Instagram: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="4" y="4" width="16" height="16" rx="4"></rect>
<circle cx="12" cy="12" r="3"></circle>
<line x1="16.5" y1="7.5" x2="16.5" y2="7.501"></line>
</svg>`,
LinkedIn: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
<line x1="8" y1="11" x2="8" y2="16"></line>
<line x1="8" y1="8" x2="8" y2="8.01"></line>
<line x1="12" y1="16" x2="12" y2="11"></line>
<path d="M16 16v-3a2 2 0 0 0 -4 0"></path>
</svg>`,
Mail: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
<polyline points="3 7 12 13 21 7"></polyline>
</svg>`,
X: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 4l11.733 16h4.267l-11.733 -16z" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />
</svg>`,
Twitch: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"></path>
</svg>`,
YouTube: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path>
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon>
</svg>`,
WhatsApp: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9"></path>
<path d="M9 10a0.5 .5 0 0 0 1 0v-1a0.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a0.5 .5 0 0 0 0 -1h-1a0.5 .5 0 0 0 0 1"></path>
</svg>`,
Snapchat: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M16.882 7.842a4.882 4.882 0 0 0 -9.764 0c0 4.273 -.213 6.409 -4.118 8.118c2 .882 2 .882 3 3c3 0 4 2 6 2s3 -2 6 -2c1 -2.118 1 -2.118 3 -3c-3.906 -1.709 -4.118 -3.845 -4.118 -8.118zm-13.882 8.119c4 -2.118 4 -4.118 1 -7.118m17 7.118c-4 -2.118 -4 -4.118 -1 -7.118"></path>
</svg>`,
Pinterest: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="8" y1="20" x2="12" y2="11"></line>
<path d="M10.7 14c.437 1.263 1.43 2 2.55 2c2.071 0 3.75 -1.554 3.75 -4a5 5 0 1 0 -9.7 1.7"></path>
<circle cx="12" cy="12" r="9"></circle>
</svg>`,
TikTok: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 12a4 4 0 1 0 4 4v-12a5 5 0 0 0 5 5"></path>
</svg>`,
CodePen: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 15l9 6l9 -6l-9 -6l-9 6"></path>
<path d="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
<line x1="3" y1="9" x2="3" y2="15"></line>
<line x1="21" y1="9" x2="21" y2="15"></line>
<line x1="12" y1="3" x2="12" y2="9"></line>
<line x1="12" y1="15" x2="12" y2="21"></line>
</svg>`,
Discord: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="9" cy="12" r="1"></circle>
<circle cx="15" cy="12" r="1"></circle>
<path d="M7.5 7.5c3.5 -1 5.5 -1 9 0"></path>
<path d="M7 16.5c3.5 1 6.5 1 10 0"></path>
<path d="M15.5 17c0 1 1.5 3 2 3c1.5 0 2.833 -1.667 3.5 -3c.667 -1.667 .5 -5.833 -1.5 -11.5c-1.457 -1.015 -3 -1.34 -4.5 -1.5l-1 2.5"></path>
<path d="M8.5 17c0 1 -1.356 3 -1.832 3c-1.429 0 -2.698 -1.667 -3.333 -3c-.635 -1.667 -.476 -5.833 1.428 -11.5c1.388 -1.015 2.782 -1.34 4.237 -1.5l1 2.5"></path>
</svg>`,
GitLab: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M21 14l-9 7l-9 -7l3 -11l3 7h6l3 -7z"></path>
</svg>`,
Reddit: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 8c2.648 0 5.028 .826 6.675 2.14a2.5 2.5 0 0 1 2.326 4.36c0 3.59 -4.03 6.5 -9 6.5c-4.875 0 -8.845 -2.8 -9 -6.294l-1 -.206a2.5 2.5 0 0 1 2.326 -4.36c1.646 -1.313 4.026 -2.14 6.674 -2.14z"></path>
<path d="M12 8l1 -5l6 1"></path>
<circle cx="19" cy="4" r="1"></circle>
<circle cx="9" cy="13" r=".5" fill="currentColor"></circle>
<circle cx="15" cy="13" r=".5" fill="currentColor"></circle>
<path d="M10 17c.667 .333 1.333 .5 2 .5s1.333 -.167 2 -.5"></path>
</svg>`,
Skype: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3a9 9 0 0 1 8.603 11.65a4.5 4.5 0 0 1 -5.953 5.953a9 9 0 0 1 -11.253 -11.253a4.5 4.5 0 0 1 5.953 -5.954a8.987 8.987 0 0 1 2.65 -.396z"></path>
<path d="M8 14.5c.5 2 2.358 2.5 4 2.5c2.905 0 4 -1.187 4 -2.5c0 -1.503 -1.927 -2.5 -4 -2.5s-4 -.997 -4 -2.5c0 -1.313 1.095 -2.5 4 -2.5c1.642 0 3.5 .5 4 2.5"></path>
</svg>`,
Steam: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M16.5 5a4.5 4.5 0 1 1 -.653 8.953l-4.347 3.009l0 .038a3 3 0 0 1 -2.824 2.995l-.176 .005a3 3 0 0 1 -2.94 -2.402l-2.56 -1.098v-3.5l3.51 1.755a2.989 2.989 0 0 1 2.834 -.635l2.727 -3.818a4.5 4.5 0 0 1 4.429 -5.302z"></path>
<circle fill="currentColor" cx="16.5" cy="9.5" r="1"></circle>
</svg>`,
Telegram: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4"></path>
</svg>`,
Mastodon: `<svg class="icon-tabler" viewBox="-10 -5 1034 1034" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<path fill="currentColor"
d="M499 112q-93 1 -166 11q-81 11 -128 33l-14 8q-16 10 -32 25q-22 21 -38 47q-21 33 -32 73q-14 47 -14 103v37q0 77 1 119q3 113 18 188q19 95 62 154q50 67 134 89q109 29 210 24q46 -3 88 -12q30 -7 55 -17l19 -8l-4 -75l-22 6q-28 6 -57 10q-41 6 -78 4q-53 -1 -80 -7
q-43 -8 -67 -30q-29 -25 -35 -72q-2 -14 -2 -29l25 6q31 6 65 10q48 7 93 9q42 2 92 -2q32 -2 88 -9t107 -30q49 -23 81.5 -54.5t38.5 -63.5q9 -45 13 -109q4 -46 5 -97v-41q0 -56 -14 -103q-11 -40 -32 -73q-16 -26 -38 -47q-15 -15 -32 -25q-12 -8 -14 -8
q-46 -22 -127 -33q-74 -10 -166 -11h-3zM367 267q73 0 109 56l24 39l24 -39q36 -56 109 -56q63 0 101 43t38 117v239h-95v-232q0 -74 -61 -74q-69 0 -69 88v127h-94v-127q0 -88 -69 -88q-61 0 -61 74v232h-95v-239q0 -74 38 -117t101 -43z" />
</svg>`,
};
export default socialIcons;
+33
View File
@@ -0,0 +1,33 @@
---
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import LinkButton from "./LinkButton.astro";
import { SITE } from "@/config";
---
{
SITE.showBackButton && (
<div class="mx-auto flex w-full max-w-3xl items-center justify-start px-2">
<LinkButton
id="back-button"
href="/"
class="focus-outline mt-8 mb-2 flex hover:text-foreground/75"
>
<IconChevronLeft class="inline-block size-6" />
<span>Go back</span>
</LinkButton>
</div>
)
}
<script is:inline data-astro-rerun>
/* Update Search Praam */
function updateGoBackUrl() {
const backButton = document.querySelector("#back-button");
const params = new URL(document.location.toString()).searchParams;
if (params.has("from")) {
backButton.href = params.get("from");
}
}
updateGoBackUrl();
</script>
@@ -8,34 +8,35 @@ const breadcrumbList = currentUrlPath.split("/").slice(1);
// if breadcrumb is Home > Posts > 1 <etc> // if breadcrumb is Home > Posts > 1 <etc>
// replace Posts with Posts (page number) // replace Posts with Posts (page number)
breadcrumbList[0] === "posts" && if (breadcrumbList[0] === "posts") {
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`); breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
}
// if breadcrumb is Home > Tags > [tag] > [page] <etc> // if breadcrumb is Home > Tags > [tag] > [page] <etc>
// replace [tag] > [page] with [tag] (page number) // replace [tag] > [page] with [tag] (page number)
breadcrumbList[0] === "tags" && if (breadcrumbList[0] === "tags" && !isNaN(Number(breadcrumbList[2]))) {
!isNaN(Number(breadcrumbList[2])) &&
breadcrumbList.splice( breadcrumbList.splice(
1, 1,
3, 3,
`${breadcrumbList[1]} ${ `${breadcrumbList[1]} ${Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"}`
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
}`
); );
}
--- ---
<nav class="breadcrumb" aria-label="breadcrumb"> <nav class="mx-auto mt-8 mb-1 w-full max-w-3xl px-4" aria-label="breadcrumb">
<ul> <ul
class="font-light [&>li]:inline [&>li:not(:last-child)>a]:hover:opacity-100"
>
<li> <li>
<a href="/">Home</a> <a href="/" class="opacity-80">Home</a>
<span aria-hidden="true">&raquo;</span> <span aria-hidden="true" class="opacity-80">&raquo;</span>
</li> </li>
{ {
breadcrumbList.map((breadcrumb, index) => breadcrumbList.map((breadcrumb, index) =>
index + 1 === breadcrumbList.length ? ( index + 1 === breadcrumbList.length ? (
<li> <li>
<span <span
class={`${index > 0 ? "lowercase" : "capitalize"}`} class:list={["capitalize opacity-75", { lowercase: index > 0 }]}
aria-current="page" aria-current="page"
> >
{/* make the last part lowercase in Home > Tags > some-tag */} {/* make the last part lowercase in Home > Tags > some-tag */}
@@ -44,7 +45,9 @@ breadcrumbList[0] === "tags" &&
</li> </li>
) : ( ) : (
<li> <li>
<a href={`/${breadcrumb}/`}>{breadcrumb}</a> <a href={`/${breadcrumb}/`} class="capitalize opacity-70">
{breadcrumb}
</a>
<span aria-hidden="true">&raquo;</span> <span aria-hidden="true">&raquo;</span>
</li> </li>
) )
@@ -52,21 +55,3 @@ breadcrumbList[0] === "tags" &&
} }
</ul> </ul>
</nav> </nav>
<style>
.breadcrumb {
@apply mx-auto mb-1 mt-8 w-full max-w-3xl px-4;
}
.breadcrumb ul li {
@apply inline;
}
.breadcrumb ul li a {
@apply capitalize opacity-70;
}
.breadcrumb ul li span {
@apply opacity-70;
}
.breadcrumb ul li:not(:last-child) a {
@apply hover:opacity-100;
}
</style>
+3 -3
View File
@@ -1,6 +1,6 @@
import { slugifyStr } from "@utils/slugify";
import Datetime from "./Datetime";
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import { slugifyStr } from "@/utils/slugify";
import { Datetime } from "./Datetime";
export interface Props { export interface Props {
href?: string; href?: string;
@@ -20,7 +20,7 @@ export default function Card({ href, frontmatter, secHeading = true }: Props) {
<li className="my-6"> <li className="my-6">
<a <a
href={href} href={href}
className="inline-block text-lg font-medium text-skin-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0" className="inline-block text-lg font-medium text-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
> >
{secHeading ? ( {secHeading ? (
<h2 {...headerProps}>{title}</h2> <h2 {...headerProps}>{title}</h2>
+11 -55
View File
@@ -1,56 +1,49 @@
import { LOCALE, SITE } from "@config"; import { LOCALE } from "@/constants";
import type { CollectionEntry } from "astro:content";
interface DatetimesProps { interface DatetimesProps {
pubDatetime: string | Date; pubDatetime: string | Date;
modDatetime: string | Date | undefined | null; modDatetime: string | Date | undefined | null;
} }
interface EditPostProps { interface Props extends DatetimesProps {
editPost?: CollectionEntry<"blog">["data"]["editPost"];
postId?: CollectionEntry<"blog">["id"];
}
interface Props extends DatetimesProps, EditPostProps {
size?: "sm" | "lg"; size?: "sm" | "lg";
className?: string; className?: string;
} }
export default function Datetime({ export function Datetime({
pubDatetime, pubDatetime,
modDatetime, modDatetime,
size = "sm", size = "sm",
className = "", className = "",
editPost,
postId,
}: Props) { }: Props) {
return ( return (
<div <div className={`flex items-end space-x-2 opacity-80 ${className}`.trim()}>
className={`flex items-center space-x-2 opacity-80 ${className}`.trim()}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={`${ className={`${
size === "sm" ? "scale-90" : "scale-100" size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 min-w-[1.375rem] fill-skin-base`} } inline-block h-6 w-6 min-w-[1.375rem] fill-foreground`}
aria-hidden="true" aria-hidden="true"
> >
<path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path> <path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path>
<path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path> <path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path>
</svg> </svg>
{modDatetime && modDatetime > pubDatetime ? ( {modDatetime && modDatetime > pubDatetime ? (
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}> <span
className={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
Updated: Updated:
</span> </span>
) : ( ) : (
<span className="sr-only">Published:</span> <span className="sr-only">Published:</span>
)} )}
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}> <span
className={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
<FormattedDatetime <FormattedDatetime
pubDatetime={pubDatetime} pubDatetime={pubDatetime}
modDatetime={modDatetime} modDatetime={modDatetime}
/> />
{size === "lg" && <EditPost editPost={editPost} postId={postId} />}
</span> </span>
</div> </div>
); );
@@ -81,40 +74,3 @@ const FormattedDatetime = ({ pubDatetime, modDatetime }: DatetimesProps) => {
</> </>
); );
}; };
const EditPost = ({ editPost, postId }: EditPostProps) => {
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
const appendFilePath =
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
if (appendFilePath && postId) {
editPostUrl += `/${postId}`;
}
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
return (
showEditPost && (
<>
<span aria-hidden="true"> | </span>
<a
className="space-x-1.5 hover:opacity-75"
href={editPostUrl}
rel="noopener noreferrer"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icons-tabler-outline icon-tabler-edit inline-block !scale-90 fill-skin-base"
aria-hidden="true"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
<path d="M16 5l3 3" />
</svg>
<span className="text-base italic">{editPostText}</span>
</a>
</>
)
);
};
+41
View File
@@ -0,0 +1,41 @@
---
import type { CollectionEntry } from "astro:content";
import IconEdit from "@/assets/icons/IconEdit.svg";
import { SITE } from "@/config";
export interface Props {
editPost?: CollectionEntry<"blog">["data"]["editPost"];
postId?: CollectionEntry<"blog">["id"];
class?: string;
}
const { editPost, postId, class: className = "" } = Astro.props;
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
const appendFilePath =
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
if (appendFilePath && postId) {
editPostUrl += `/${postId}`;
}
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
---
{
showEditPost && (
<div class:list={["opacity-80", className]}>
<span aria-hidden="true" class="max-sm:hidden">
|
</span>
<a
class="space-x-1.5 hover:opacity-75"
href={editPostUrl}
rel="noopener noreferrer"
target="_blank"
>
<IconEdit class="inline-block size-6" />
<span class="italic max-sm:text-sm sm:inline">{editPostText}</span>
</a>
</div>
)
}
+6 -25
View File
@@ -11,35 +11,16 @@ export interface Props {
const { noMarginTop = false } = Astro.props; const { noMarginTop = false } = Astro.props;
--- ---
<footer class={`${noMarginTop ? "" : "mt-auto"}`}> <footer class:list={["w-full", { "mt-auto": !noMarginTop }]}>
<Hr noPadding /> <Hr noPadding />
<div class="footer-wrapper"> <div
class="flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4"
>
<Socials centered /> <Socials centered />
<div class="copyright-wrapper"> <div class="my-2 flex flex-col items-center whitespace-nowrap sm:flex-row">
<span>Copyright &#169; {currentYear}</span> <span>Copyright &#169; {currentYear}</span>
<span class="separator">&nbsp;|&nbsp;</span> <span class="hidden sm:inline">&nbsp;|&nbsp;</span>
<span>All rights reserved.</span> <span>All rights reserved.</span>
</div> </div>
</div> </div>
</footer> </footer>
<style>
footer {
@apply w-full;
}
.footer-wrapper {
@apply flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4;
}
.link-button {
@apply my-1 p-2 hover:rotate-6;
}
.link-button svg {
@apply scale-125;
}
.copyright-wrapper {
@apply my-2 flex flex-col items-center whitespace-nowrap sm:flex-row;
}
.separator {
@apply hidden sm:inline;
}
</style>
+108 -190
View File
@@ -1,149 +1,138 @@
--- ---
import { LOGO_IMAGE, SITE } from "@config";
import Hr from "./Hr.astro"; import Hr from "./Hr.astro";
import IconX from "@/assets/icons/IconX.svg";
import IconMoon from "@/assets/icons/IconMoon.svg";
import IconSearch from "@/assets/icons/IconSearch.svg";
import IconArchive from "@/assets/icons/IconArchive.svg";
import IconSunHigh from "@/assets/icons/IconSunHigh.svg";
import IconMenuDeep from "@/assets/icons/IconMenuDeep.svg";
import LinkButton from "./LinkButton.astro"; import LinkButton from "./LinkButton.astro";
import { SITE } from "@/config";
export interface Props { const { pathname } = Astro.url;
activeNav?: "posts" | "archives" | "tags" | "about" | "search";
}
const { activeNav } = Astro.props; // Remove trailing slash from current pathname if exists
const currentPath =
pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
const isActive = (path: string) => {
const currentPathArray = currentPath.split("/").filter(p => p.trim());
const pathArray = path.split("/").filter(p => p.trim());
return currentPath === path || currentPathArray[0] === pathArray[0];
};
--- ---
<header> <header>
<a id="skip-to-content" href="#main-content">Skip to content</a> <a
<div class="nav-container"> id="skip-to-content"
<div class="top-nav-wrap"> href="#main-content"
<a href="/" class="logo whitespace-nowrap"> class="absolute -top-full left-16 z-50 bg-background px-3 py-2 text-accent backdrop-blur-lg transition-all focus:top-4"
{ >
LOGO_IMAGE.enable ? ( Skip to content
<img </a>
src={`/assets/${LOGO_IMAGE.svg ? "logo.svg" : "logo.png"}`} <div
alt={SITE.title} id="nav-container"
width={LOGO_IMAGE.width} class="mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row"
height={LOGO_IMAGE.height} >
/> <div
) : ( id="top-nav-wrap"
SITE.title class="relative flex w-full items-start justify-between bg-background p-4 sm:py-6"
) >
} <a
href="/"
class="absolute py-1 text-2xl leading-7 font-semibold whitespace-nowrap sm:static"
>
{SITE.title}
</a> </a>
<nav id="nav-menu"> <nav
id="nav-menu"
class="flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0"
>
<button <button
class="hamburger-menu focus-outline" id="menu-btn"
class="focus-outline self-end p-2 sm:hidden"
aria-label="Open Menu" aria-label="Open Menu"
aria-expanded="false" aria-expanded="false"
aria-controls="menu-items" aria-controls="menu-items"
> >
<svg <IconX id="close-icon" class="hidden" />
xmlns="http://www.w3.org/2000/svg" <IconMenuDeep id="menu-icon" />
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="menu-icon"
>
<line x1="7" y1="12" x2="21" y2="12" class="line"></line>
<line x1="3" y1="6" x2="21" y2="6" class="line"></line>
<line x1="12" y1="18" x2="21" y2="18" class="line"></line>
<line x1="18" y1="6" x2="6" y2="18" class="close"></line>
<line x1="6" y1="6" x2="18" y2="18" class="close"></line>
</svg>
</button> </button>
<ul id="menu-items" class="display-none sm:flex"> <ul
<li> id="menu-items"
<a href="/posts/" class={activeNav === "posts" ? "active" : ""}> class:list={[
"mt-4 grid w-44 grid-cols-2 place-content-center gap-2",
"[&>li>a]:block [&>li>a]:px-4 [&>li>a]:py-3 [&>li>a]:text-center [&>li>a]:font-medium [&>li>a]:hover:text-accent sm:[&>li>a]:px-2 sm:[&>li>a]:py-1",
"hidden",
"sm:mt-0 sm:ml-0 sm:flex sm:w-auto sm:gap-x-5 sm:gap-y-0",
]}
>
<li class="col-span-2">
<a href="/posts" class:list={{ "active-nav": isActive("/posts") }}>
Posts Posts
</a> </a>
</li> </li>
<li> <li class="col-span-2">
<a href="/tags/" class={activeNav === "tags" ? "active" : ""}> <a href="/tags" class:list={{ "active-nav": isActive("/tags") }}>
Tags Tags
</a> </a>
</li> </li>
<li> <li class="col-span-2">
<a href="/about/" class={activeNav === "about" ? "active" : ""}> <a href="/about" class:list={{ "active-nav": isActive("/about") }}>
About About
</a> </a>
</li> </li>
{ {
SITE.showArchives && ( SITE.showArchives && (
<li> <li class="col-span-2">
<LinkButton <LinkButton
href="/archives/" href="/archives"
className={`focus-outline flex justify-center p-3 sm:p-1`} class:list={[
"focus-outline flex justify-center p-3 sm:p-1",
{
"active-nav [&>svg]:stroke-accent": isActive("/archives"),
},
]}
ariaLabel="archives" ariaLabel="archives"
title="Archives" title="Archives"
> >
<svg <IconArchive class="hidden sm:inline-block" />
xmlns="http://www.w3.org/2000/svg" <span class="sm:sr-only">Archives</span>
class:list={[
"icon icon-tabler icons-tabler-outline !hidden sm:!inline-block",
activeNav === "archives" && "!stroke-skin-accent",
]}
>
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
<path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" />
<path d="M10 12l4 0" />
</>
</svg>
<span
class:list={[
"sm:sr-only",
activeNav === "archives" && "active",
]}
>
Archives
</span>
</LinkButton> </LinkButton>
</li> </li>
) )
} }
<li> <li class="col-span-1 flex items-center justify-center">
<LinkButton <LinkButton
href="/search/" href="/search"
className={`focus-outline p-3 sm:p-1 ${ class:list={[
activeNav === "search" ? "active" : "" "focus-outline flex p-3 sm:p-1",
} flex`} { "[&>svg]:stroke-accent": isActive("/search") },
]}
ariaLabel="search" ariaLabel="search"
title="Search" title="Search"
> >
<svg <IconSearch />
xmlns="http://www.w3.org/2000/svg"
class="scale-125 sm:scale-100"
><path
d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"
></path>
</svg>
<span class="sr-only">Search</span> <span class="sr-only">Search</span>
</LinkButton> </LinkButton>
</li> </li>
{ <li class="col-span-1 flex items-center justify-center">
SITE.lightAndDarkMode && ( <button
<li> id="theme-btn"
<button class="focus-outline relative size-12 p-4 sm:size-8 hover:[&>svg]:stroke-accent"
id="theme-btn" title="Toggles light & dark"
class="focus-outline" aria-label="auto"
title="Toggles light & dark" aria-live="polite"
aria-label="auto" >
aria-live="polite" <IconMoon
> class="absolute top-[50%] left-[50%] -translate-[50%] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90"
<svg xmlns="http://www.w3.org/2000/svg" id="moon-svg"> />
<path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z" /> <IconSunHigh
</svg> class="absolute top-[50%] left-[50%] -translate-[50%] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0"
<svg xmlns="http://www.w3.org/2000/svg" id="sun-svg"> />
<path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z" /> </button>
</svg> </li>
</button>
</li>
)
}
</ul> </ul>
</nav> </nav>
</div> </div>
@@ -151,95 +140,24 @@ const { activeNav } = Astro.props;
<Hr /> <Hr />
</header> </header>
<style>
#skip-to-content {
@apply absolute -top-full left-16 z-50 bg-skin-accent px-3 py-2 text-skin-inverted transition-all focus:top-4;
}
.nav-container {
@apply mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row;
}
.top-nav-wrap {
@apply relative flex w-full items-start justify-between p-4 sm:items-center sm:py-8;
}
.logo {
@apply absolute py-1 text-xl font-semibold sm:static sm:text-2xl;
}
.hamburger-menu {
@apply self-end p-2 sm:hidden;
}
.hamburger-menu svg {
@apply h-6 w-6 scale-125 fill-skin-base;
}
nav {
@apply flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0;
}
nav ul {
@apply mt-4 grid w-44 grid-cols-2 grid-rows-4 gap-x-2 gap-y-2 sm:ml-0 sm:mt-0 sm:w-auto sm:gap-x-5 sm:gap-y-0;
}
nav ul li {
@apply col-span-2 flex items-center justify-center;
}
nav ul li a {
@apply w-full px-4 py-3 text-center font-medium hover:text-skin-accent sm:my-0 sm:px-2 sm:py-1;
}
nav ul li:nth-last-child(2) a {
@apply w-auto;
}
nav ul li:nth-last-child(1),
nav ul li:nth-last-child(2) {
@apply col-span-1;
}
nav .active {
@apply underline decoration-wavy decoration-2 underline-offset-4;
}
nav a.active svg {
@apply fill-skin-accent;
}
nav button {
@apply p-1;
}
nav button svg {
@apply h-6 w-6 fill-skin-base hover:fill-skin-accent;
}
#theme-btn {
@apply p-3 sm:p-1;
}
#theme-btn svg {
@apply scale-125 hover:rotate-12 sm:scale-100;
}
.menu-icon line {
@apply transition-opacity duration-75 ease-in-out;
}
.menu-icon .close {
opacity: 0;
}
.menu-icon.is-active .line {
@apply opacity-0;
}
.menu-icon.is-active .close {
@apply opacity-100;
}
</style>
<script> <script>
function toggleNav() { function toggleNav() {
// Toggle menu const menuBtn = document.querySelector("#menu-btn");
const menuBtn = document.querySelector(".hamburger-menu");
const menuIcon = document.querySelector(".menu-icon");
const menuItems = document.querySelector("#menu-items"); const menuItems = document.querySelector("#menu-items");
const menuIcon = document.querySelector("#menu-icon");
const closeIcon = document.querySelector("#close-icon");
menuBtn?.addEventListener("click", () => { if (!menuBtn || !menuItems || !menuIcon || !closeIcon) return;
const menuExpanded = menuBtn.getAttribute("aria-expanded") === "true";
menuIcon?.classList.toggle("is-active"); menuBtn.addEventListener("click", () => {
menuBtn.setAttribute("aria-expanded", menuExpanded ? "false" : "true"); const openMenu = menuBtn.getAttribute("aria-expanded") === "true";
menuBtn.setAttribute(
"aria-label", menuBtn.setAttribute("aria-expanded", openMenu ? "false" : "true");
menuExpanded ? "Open Menu" : "Close Menu" menuBtn.setAttribute("aria-label", openMenu ? "Open Menu" : "Close Menu");
);
menuItems?.classList.toggle("display-none"); menuItems.classList.toggle("hidden");
menuIcon.classList.toggle("hidden");
closeIcon.classList.toggle("hidden");
}); });
} }
+1 -1
View File
@@ -8,5 +8,5 @@ const { noPadding = false, ariaHidden = true } = Astro.props;
--- ---
<div class={`max-w-3xl mx-auto ${noPadding ? "px-0" : "px-4"}`}> <div class={`max-w-3xl mx-auto ${noPadding ? "px-0" : "px-4"}`}>
<hr class="border-skin-line" aria-hidden={ariaHidden} /> <hr class="border-border" aria-hidden={ariaHidden} />
</div> </div>
+7 -3
View File
@@ -1,15 +1,17 @@
--- ---
export interface Props { export interface Props {
id?: string;
href: string; href: string;
className?: string; class?: string;
ariaLabel?: string; ariaLabel?: string;
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
} }
const { const {
id,
href, href,
className = "", class: className = "",
ariaLabel, ariaLabel,
title, title,
disabled = false, disabled = false,
@@ -19,6 +21,7 @@ const {
{ {
disabled ? ( disabled ? (
<span <span
id={id}
class:list={["group inline-block", className]} class:list={["group inline-block", className]}
title={title} title={title}
aria-disabled={disabled} aria-disabled={disabled}
@@ -27,8 +30,9 @@ const {
</span> </span>
) : ( ) : (
<a <a
id={id}
{href} {href}
class:list={["group inline-block hover:text-skin-accent", className]} class:list={["group inline-block hover:text-accent", className]}
aria-label={ariaLabel} aria-label={ariaLabel}
title={title} title={title}
> >
+8 -28
View File
@@ -1,7 +1,9 @@
--- ---
import type { Page } from "astro"; import type { Page } from "astro";
import LinkButton from "./LinkButton.astro";
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import IconArrowLeft from "@/assets/icons/IconArrowLeft.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import LinkButton from "./LinkButton.astro";
export interface Props { export interface Props {
page: Page<CollectionEntry<"blog">>; page: Page<CollectionEntry<"blog">>;
@@ -12,48 +14,26 @@ const { page } = Astro.props;
{ {
page.lastPage > 1 && ( page.lastPage > 1 && (
<nav class="pagination-wrapper" aria-label="Pagination"> <nav class="mt-auto mb-8 flex justify-center" aria-label="Pagination">
<LinkButton <LinkButton
disabled={!page.url.prev} disabled={!page.url.prev}
href={page.url.prev as string} href={page.url.prev as string}
className={`mr-4 select-none ${page.url.prev ? "" : "disabled"}`} class:list={["mr-4 select-none", { "opacity-50": !page.url.prev }]}
ariaLabel="Previous" ariaLabel="Previous"
> >
<svg <IconArrowLeft class="inline-block" />
xmlns="http://www.w3.org/2000/svg"
class:list={[{ "disabled-svg": !page.url.prev }]}
>
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
</svg>
Prev Prev
</LinkButton> </LinkButton>
{page.currentPage} / {page.lastPage} {page.currentPage} / {page.lastPage}
<LinkButton <LinkButton
disabled={!page.url.next} disabled={!page.url.next}
href={page.url.next as string} href={page.url.next as string}
className={`mx-4 select-none ${page.url.next ? "" : "disabled"}`} class:list={["ml-4 select-none", { "opacity-50": !page.url.next }]}
ariaLabel="Next" ariaLabel="Next"
> >
Next Next
<svg <IconArrowRight class="inline-block" />
xmlns="http://www.w3.org/2000/svg"
class:list={[{ "disabled-svg": !page.url.next }]}
>
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
</svg>
</LinkButton> </LinkButton>
</nav> </nav>
) )
} }
<style>
.pagination-wrapper {
@apply mb-8 mt-auto flex justify-center;
}
.disabled {
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
}
.disabled-svg {
@apply group-hover:!fill-skin-base;
}
</style>
@@ -1,7 +1,7 @@
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
import Card from "@components/Card";
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
import Card from "@/components/Card";
export type SearchItem = { export type SearchItem = {
title: string; title: string;
@@ -12,6 +12,7 @@ export type SearchItem = {
interface Props { interface Props {
searchList: SearchItem[]; searchList: SearchItem[];
backUrl: string;
} }
interface SearchResult { interface SearchResult {
@@ -19,7 +20,7 @@ interface SearchResult {
refIndex: number; refIndex: number;
} }
export default function SearchBar({ searchList }: Props) { export default function SearchBar({ searchList, backUrl }: Props) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [inputVal, setInputVal] = useState(""); const [inputVal, setInputVal] = useState("");
const [searchResults, setSearchResults] = useState<SearchResult[] | null>( const [searchResults, setSearchResults] = useState<SearchResult[] | null>(
@@ -55,6 +56,7 @@ export default function SearchBar({ searchList }: Props) {
}, 50); }, 50);
}, []); }, []);
let newRelativePathQuery = "";
useEffect(() => { useEffect(() => {
// Add search result only if // Add search result only if
// input value is more than one character // input value is more than one character
@@ -65,7 +67,7 @@ export default function SearchBar({ searchList }: Props) {
if (inputVal.length > 0) { if (inputVal.length > 0) {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
searchParams.set("q", inputVal); searchParams.set("q", inputVal);
const newRelativePathQuery = newRelativePathQuery =
window.location.pathname + "?" + searchParams.toString(); window.location.pathname + "?" + searchParams.toString();
history.replaceState(history.state, "", newRelativePathQuery); history.replaceState(history.state, "", newRelativePathQuery);
} else { } else {
@@ -80,17 +82,23 @@ export default function SearchBar({ searchList }: Props) {
} }
}, [inputVal]); }, [inputVal]);
const fullBackUrl = `${backUrl}${inputVal.trim() !== "" ? `?q=${inputVal}` : "/"}`;
return ( return (
<> <>
<label className="relative block"> <label className="relative block">
<span className="sr-only">Search</span>
<span className="absolute inset-y-0 left-0 flex items-center pl-2 opacity-75"> <span className="absolute inset-y-0 left-0 flex items-center pl-2 opacity-75">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> <svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className="inline-block size-6 fill-foreground"
>
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path> <path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
</svg> </svg>
<span className="sr-only">Search</span>
</span> </span>
<input <input
className="block w-full rounded border border-skin-fill/40 bg-skin-fill py-3 pl-10 pr-3 placeholder:italic focus:border-skin-accent focus:outline-none" className="border-skin-fill/40 bg-skin-fill focus:border-skin-accent block w-full rounded border py-3 pr-3 pl-10 placeholder:italic focus:outline-none"
placeholder="Search for anything..." placeholder="Search for anything..."
type="text" type="text"
name="search" name="search"
@@ -116,7 +124,7 @@ export default function SearchBar({ searchList }: Props) {
{searchResults && {searchResults &&
searchResults.map(({ item, refIndex }) => ( searchResults.map(({ item, refIndex }) => (
<Card <Card
href={`/posts/${item.slug}/`} href={`/posts/${item.slug}${fullBackUrl}`}
frontmatter={item.data} frontmatter={item.data}
key={`${refIndex}-${item.slug}`} key={`${refIndex}-${item.slug}`}
/> />
+7 -47
View File
@@ -1,66 +1,26 @@
--- ---
import { SHARE_LINKS } from "@/constants";
import LinkButton from "./LinkButton.astro"; import LinkButton from "./LinkButton.astro";
import socialIcons from "@assets/socialIcons";
const URL = Astro.url; const URL = Astro.url;
const shareLinks = [
{
name: "WhatsApp",
href: "https://wa.me/?text=",
linkTitle: `Share this post via WhatsApp`,
},
{
name: "Facebook",
href: "https://www.facebook.com/sharer.php?u=",
linkTitle: `Share this post on Facebook`,
},
{
name: "X",
href: "https://x.com/intent/post?url=",
linkTitle: `Share this post on X`,
},
{
name: "Telegram",
href: "https://t.me/share/url?url=",
linkTitle: `Share this post via Telegram`,
},
{
name: "Pinterest",
href: "https://pinterest.com/pin/create/button/?url=",
linkTitle: `Share this post on Pinterest`,
},
{
name: "Mail",
href: "mailto:?subject=See%20this%20post&body=",
linkTitle: `Share this post via email`,
},
] as const;
--- ---
<div class={`social-icons`}> <div
class="flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start"
>
<span class="italic">Share this post on:</span> <span class="italic">Share this post on:</span>
<div class="text-center"> <div class="text-center">
{ {
shareLinks.map(social => ( SHARE_LINKS.map(social => (
<LinkButton <LinkButton
href={`${social.href + URL}`} href={`${social.href + URL}`}
className="link-button" class="scale-90 p-2 hover:rotate-6 sm:p-1"
title={social.linkTitle} title={social.linkTitle}
> >
<Fragment set:html={socialIcons[social.name]} /> <social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{social.linkTitle}</span> <span class="sr-only">{social.linkTitle}</span>
</LinkButton> </LinkButton>
)) ))
} }
</div> </div>
</div> </div>
<style>
.social-icons {
@apply flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start;
}
.link-button {
@apply scale-90 p-2 hover:rotate-6 sm:p-1;
}
</style>
Executable → Regular
+5 -15
View File
@@ -1,7 +1,6 @@
--- ---
import { SOCIALS } from "@config"; import { SOCIALS } from "@/constants";
import LinkButton from "./LinkButton.astro"; import LinkButton from "./LinkButton.astro";
import socialIcons from "@assets/socialIcons";
export interface Props { export interface Props {
centered?: boolean; centered?: boolean;
@@ -10,26 +9,17 @@ export interface Props {
const { centered = false } = Astro.props; const { centered = false } = Astro.props;
--- ---
<div class={`social-icons ${centered ? "flex" : ""}`}> <div class:list={["flex-wrap justify-center gap-1", { flex: centered }]}>
{ {
SOCIALS.filter(social => social.active).map(social => ( SOCIALS.map(social => (
<LinkButton <LinkButton
href={social.href} href={social.href}
className="link-button" class="p-2 hover:rotate-6 sm:p-1"
title={social.linkTitle} title={social.linkTitle}
> >
<Fragment set:html={socialIcons[social.name]} /> <social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{social.linkTitle}</span> <span class="sr-only">{social.linkTitle}</span>
</LinkButton> </LinkButton>
)) ))
} }
</div> </div>
<style>
.social-icons {
@apply flex-wrap justify-center gap-1;
}
.link-button {
@apply p-2 hover:rotate-6 sm:p-1;
}
</style>
+17 -20
View File
@@ -1,4 +1,6 @@
--- ---
import IconHash from "@/assets/icons/IconHash.svg";
export interface Props { export interface Props {
tag: string; tag: string;
tagName: string; tagName: string;
@@ -9,31 +11,26 @@ const { tag, tagName, size = "sm" } = Astro.props;
--- ---
<li <li
class={`inline-block ${ class:list={[
size === "sm" ? "my-1 underline-offset-4" : "my-3 mx-1 underline-offset-8" "group inline-block group-hover:cursor-pointer",
}`} size === "sm" ? "my-1 underline-offset-4" : "mx-1 my-3 underline-offset-8",
]}
> >
<a <a
href={`/tags/${tag}/`} href={`/tags/${tag}/`}
transition:name={tag} transition:name={tag}
class={`${size === "sm" ? "text-sm" : "text-lg"} pr-2 group`} class:list={[
"relative pr-2 text-lg underline decoration-dashed group-hover:-top-0.5 group-hover:text-accent focus-visible:p-1",
{ "text-sm": size === "sm" },
]}
> >
<svg <IconHash
xmlns="http://www.w3.org/2000/svg" class:list={[
class={`${size === "sm" ? " scale-75" : "scale-110"}`} "inline-block opacity-80",
><path { "-mr-3.5 size-4": size === "sm" },
d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z" { "-mr-5 size-6": size === "lg" },
></path> ]}
</svg> />
&nbsp;<span>{tagName}</span> &nbsp;<span>{tagName}</span>
</a> </a>
</li> </li>
<style>
a {
@apply relative underline decoration-dashed hover:-top-0.5 hover:text-skin-accent focus-visible:p-1;
}
a svg {
@apply -mr-5 h-6 w-6 scale-95 text-skin-base opacity-80 group-hover:fill-skin-accent;
}
</style>
+6 -6
View File
@@ -1,9 +1,9 @@
--- ---
import { SITE } from "@config"; import Header from "@/components/Header.astro";
import Breadcrumbs from "@components/Breadcrumbs.astro"; import Footer from "@/components/Footer.astro";
import Footer from "@components/Footer.astro"; import Breadcrumb from "@/components/Breadcrumb.astro";
import Header from "@components/Header.astro";
import Layout from "./Layout.astro"; import Layout from "./Layout.astro";
import { SITE } from "@/config";
export interface Props { export interface Props {
frontmatter: { frontmatter: {
@@ -16,8 +16,8 @@ const { frontmatter } = Astro.props;
--- ---
<Layout title={`${frontmatter.title} | ${SITE.title}`}> <Layout title={`${frontmatter.title} | ${SITE.title}`}>
<Header activeNav="about" /> <Header />
<Breadcrumbs /> <Breadcrumb />
<main id="main-content"> <main id="main-content">
<section id="about" class="prose mb-28 max-w-3xl prose-img:border-0"> <section id="about" class="prose mb-28 max-w-3xl prose-img:border-0">
<h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1> <h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1>
+23 -17
View File
@@ -1,7 +1,8 @@
--- ---
import { LOCALE, SITE } from "@config"; import { ClientRouter } from "astro:transitions";
import "@styles/base.css"; import { SITE } from "@/config";
import { ViewTransitions } from "astro:transitions"; import { LOCALE } from "@/constants";
import "@/styles/global.css";
const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION; const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
@@ -23,16 +24,13 @@ const {
profile = SITE.profile, profile = SITE.profile,
description = SITE.desc, description = SITE.desc,
ogImage = SITE.ogImage, ogImage = SITE.ogImage,
canonicalURL = new URL(Astro.url.pathname, Astro.site).href, canonicalURL = new URL(Astro.url.pathname, Astro.url),
pubDatetime, pubDatetime,
modDatetime, modDatetime,
scrollSmooth = false, scrollSmooth = false,
} = Astro.props; } = Astro.props;
const socialImageURL = new URL( const socialImageURL = new URL(ogImage ?? SITE.ogImage ?? "og.png", Astro.url);
ogImage ?? SITE.ogImage ?? "og.png",
Astro.url.origin
).href;
const structuredData = { const structuredData = {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -104,18 +102,17 @@ const structuredData = {
<!-- Google JSON-LD Structured data --> <!-- Google JSON-LD Structured data -->
<script <script
type="application/ld+json" type="application/ld+json"
is:inline
set:html={JSON.stringify(structuredData)} set:html={JSON.stringify(structuredData)}
/> />
<!-- Google Font --> <!-- Enable RSS feed auto-discovery -->
<link rel="preconnect" href="https://fonts.googleapis.com" /> <!-- https://docs.astro.build/en/recipes/rss/#enabling-rss-feed-auto-discovery -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap" rel="alternate"
rel="preload" type="application/rss+xml"
as="style" title={SITE.title}
onload="this.onload=null; this.rel='stylesheet';" href={new URL("rss.xml", Astro.site)}
crossorigin
/> />
<meta name="theme-color" content="" /> <meta name="theme-color" content="" />
@@ -132,7 +129,7 @@ const structuredData = {
) )
} }
<ViewTransitions /> <ClientRouter />
<script is:inline src="/toggle-theme.js" async></script> <script is:inline src="/toggle-theme.js" async></script>
</head> </head>
@@ -140,3 +137,12 @@ const structuredData = {
<slot /> <slot />
</body> </body>
</html> </html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>
+6 -18
View File
@@ -1,5 +1,5 @@
--- ---
import Breadcrumbs from "@components/Breadcrumbs.astro"; import Breadcrumb from "@/components/Breadcrumb.astro";
interface StringTitleProp { interface StringTitleProp {
pageTitle: string; pageTitle: string;
@@ -17,32 +17,20 @@ export type Props = StringTitleProp | ArrayTitleProp;
const { props } = Astro; const { props } = Astro;
--- ---
<Breadcrumbs /> <Breadcrumb />
<main id="main-content"> <main id="main-content" class="mx-auto w-full max-w-3xl px-4 pb-4">
{ {
"titleTransition" in props ? ( "titleTransition" in props ? (
<h1> <h1 class="text-2xl font-semibold sm:text-3xl">
{props.pageTitle[0]} {props.pageTitle[0]}
<span transition:name={props.titleTransition}> <span transition:name={props.titleTransition}>
{props.pageTitle[1]} {props.pageTitle[1]}
</span> </span>
</h1> </h1>
) : ( ) : (
<h1>{props.pageTitle}</h1> <h1 class="text-2xl font-semibold sm:text-3xl">{props.pageTitle}</h1>
) )
} }
<p>{props.pageDesc}</p> <p class="mt-2 mb-6 italic">{props.pageDesc}</p>
<slot /> <slot />
</main> </main>
<style>
#main-content {
@apply mx-auto w-full max-w-3xl px-4 pb-4;
}
#main-content h1 {
@apply text-2xl font-semibold sm:text-3xl;
}
#main-content p {
@apply mb-6 mt-2 italic;
}
</style>
+51 -86
View File
@@ -1,13 +1,17 @@
--- ---
import Layout from "@layouts/Layout.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Tag from "@components/Tag.astro";
import Datetime from "@components/Datetime";
import { render, type CollectionEntry } from "astro:content"; import { render, type CollectionEntry } from "astro:content";
import { slugifyStr } from "@utils/slugify"; import Layout from "@/layouts/Layout.astro";
import ShareLinks from "@components/ShareLinks.astro"; import Header from "@/components/Header.astro";
import { SITE } from "@config"; import Footer from "@/components/Footer.astro";
import Tag from "@/components/Tag.astro";
import { Datetime } from "@/components/Datetime";
import EditPost from "@/components/EditPost.astro";
import ShareLinks from "@/components/ShareLinks.astro";
import BackButton from "@/components/BackButton.astro";
import { slugifyStr } from "@/utils/slugify";
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import IconChevronRight from "@/assets/icons/IconChevronRight.svg";
import { SITE } from "@/config";
export interface Props { export interface Props {
post: CollectionEntry<"blog">; post: CollectionEntry<"blog">;
@@ -32,7 +36,7 @@ const { Content } = await render(post);
const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.src; const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.src;
const ogUrl = new URL( const ogUrl = new URL(
ogImageUrl ?? `/posts/${slugifyStr(title)}.png`, ogImageUrl ?? `/posts/${slugifyStr(title)}/index.png`,
Astro.url.origin Astro.url.origin
).href; ).href;
@@ -63,55 +67,53 @@ const nextPost =
<Layout {...layoutProps}> <Layout {...layoutProps}>
<Header /> <Header />
<BackButton />
<div class="mx-auto flex w-full max-w-3xl justify-start px-2"> <main
<button id="main-content"
class="focus-outline mb-2 mt-8 flex hover:opacity-75" class:list={[
onclick="(() => (history.length === 1) ? window.location = '/' : history.back())()" "mx-auto w-full max-w-3xl px-4 pb-12",
{ "mt-8": !SITE.showBackButton },
]}
>
<h1
transition:name={slugifyStr(title)}
class="inline-block text-2xl font-bold text-accent sm:text-3xl"
> >
<svg xmlns="http://www.w3.org/2000/svg"
><path
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
></path>
</svg><span>Go back</span>
</button>
</div>
<main id="main-content">
<h1 transition:name={slugifyStr(title)} class="post-title inline-block">
{title} {title}
</h1> </h1>
<Datetime <div class="flex items-center gap-4">
pubDatetime={pubDatetime} <Datetime
modDatetime={modDatetime} pubDatetime={pubDatetime}
size="lg" modDatetime={modDatetime}
className="my-2" size="lg"
editPost={editPost} className="my-2"
postId={post.id} />
/> <EditPost class="max-sm:hidden" editPost={editPost} postId={post.id} />
<article id="article" class="prose mx-auto mt-8 max-w-3xl"> </div>
<article id="article" class="mx-auto prose mt-8 max-w-3xl">
<Content /> <Content />
</article> </article>
<ul class="my-8"> <hr class="my-8 border-dashed" />
<EditPost class="sm:hidden" editPost={editPost} postId={post.id} />
<ul class="mt-4 mb-8 sm:my-8">
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} />)} {tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} />)}
</ul> </ul>
<div <div
class="flex flex-col-reverse items-center justify-between gap-6 sm:flex-row-reverse sm:items-end sm:gap-4" class="flex flex-col items-center justify-between gap-6 sm:flex-row sm:items-end sm:gap-4"
> >
<ShareLinks />
<button <button
id="back-to-top" id="back-to-top"
class="focus-outline whitespace-nowrap py-1 hover:opacity-75" class="focus-outline py-1 whitespace-nowrap hover:opacity-75"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="rotate-90"> <IconChevronLeft class="inline-block rotate-90" />
<path
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
></path>
</svg>
<span>Back to Top</span> <span>Back to Top</span>
</button> </button>
<ShareLinks />
</div> </div>
<hr class="my-6 border-dashed" /> <hr class="my-6 border-dashed" />
@@ -124,24 +126,10 @@ const nextPost =
href={`/posts/${prevPost.slug}`} href={`/posts/${prevPost.slug}`}
class="flex w-full gap-1 hover:opacity-75" class="flex w-full gap-1 hover:opacity-75"
> >
<svg <IconChevronLeft class="inline-block flex-none" />
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left flex-none"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 6l-6 6l6 6" />
</svg>
<div> <div>
<span>Previous Post</span> <span>Previous Post</span>
<div class="text-sm text-skin-accent/85">{prevPost.title}</div> <div class="text-sm text-accent/85">{prevPost.title}</div>
</div> </div>
</a> </a>
) )
@@ -154,23 +142,9 @@ const nextPost =
> >
<div> <div>
<span>Next Post</span> <span>Next Post</span>
<div class="text-sm text-skin-accent/85">{nextPost.title}</div> <div class="text-sm text-accent/85">{nextPost.title}</div>
</div> </div>
<svg <IconChevronRight class="inline-block flex-none" />
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right flex-none"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 6l6 6l-6 6" />
</svg>
</a> </a>
) )
} }
@@ -179,15 +153,6 @@ const nextPost =
<Footer /> <Footer />
</Layout> </Layout>
<style>
main {
@apply mx-auto w-full max-w-3xl px-4 pb-12;
}
.post-title {
@apply text-2xl font-semibold text-skin-accent;
}
</style>
<script is:inline data-astro-rerun> <script is:inline data-astro-rerun>
/** Create a progress indicator /** Create a progress indicator
* at the top */ * at the top */
@@ -195,11 +160,11 @@ const nextPost =
// Create the main container div // Create the main container div
const progressContainer = document.createElement("div"); const progressContainer = document.createElement("div");
progressContainer.className = progressContainer.className =
"progress-container fixed top-0 z-10 h-1 w-full bg-skin-fill"; "progress-container fixed top-0 z-10 h-1 w-full bg-background";
// Create the progress bar div // Create the progress bar div
const progressBar = document.createElement("div"); const progressBar = document.createElement("div");
progressBar.className = "progress-bar h-1 w-0 bg-skin-accent"; progressBar.className = "progress-bar h-1 w-0 bg-accent";
progressBar.id = "myBar"; progressBar.id = "myBar";
// Append the progress bar to the progress container // Append the progress bar to the progress container
@@ -264,7 +229,7 @@ const nextPost =
const copyButton = document.createElement("button"); const copyButton = document.createElement("button");
copyButton.className = copyButton.className =
"copy-code absolute right-3 -top-3 rounded bg-skin-card px-2 py-1 text-xs leading-4 text-skin-base font-medium"; "copy-code absolute right-3 -top-3 rounded bg-muted px-2 py-1 text-xs leading-4 text-foreground font-medium";
copyButton.innerHTML = copyButtonLabel; copyButton.innerHTML = copyButtonLabel;
codeBlock.setAttribute("tabindex", "0"); codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton); codeBlock.appendChild(copyButton);
-34
View File
@@ -1,34 +0,0 @@
---
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Pagination from "@components/Pagination.astro";
import Card from "@components/Card";
import { SITE } from "@config";
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
export interface Props {
page: Page<CollectionEntry<"blog">>;
}
const { page } = Astro.props;
---
<Layout title={`Posts | ${SITE.title}`}>
<Header activeNav="posts" />
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
<ul>
{
page.data.map(({ data, id }) => (
<Card href={`/posts/${id}/`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>
-41
View File
@@ -1,41 +0,0 @@
---
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Card from "@components/Card";
import Pagination from "@components/Pagination.astro";
import { SITE } from "@config";
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
export interface Props {
page: Page<CollectionEntry<"blog">>;
tag: string;
tagName: string;
}
const { page, tag, tagName } = Astro.props;
---
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Header activeNav="tags" />
<Main
pageTitle={[`Tag:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tagName}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
page.data.map(({ data, id }) => (
<Card href={`/posts/${id}/`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>
+13 -25
View File
@@ -1,22 +1,25 @@
--- ---
import { SITE } from "@config"; import Layout from "@/layouts/Layout.astro";
import Layout from "@layouts/Layout.astro"; import Header from "@/components/Header.astro";
import Header from "@components/Header.astro"; import Footer from "@/components/Footer.astro";
import Footer from "@components/Footer.astro"; import LinkButton from "@/components/LinkButton.astro";
import LinkButton from "@components/LinkButton.astro"; import { SITE } from "@/config";
--- ---
<Layout title={`404 Not Found | ${SITE.title}`}> <Layout title={`404 Not Found | ${SITE.title}`}>
<Header /> <Header />
<main id="main-content"> <main
<div class="not-found-wrapper"> id="main-content"
<h1>404</h1> class="mx-auto flex max-w-3xl flex-1 items-center justify-center"
>
<div class="mb-14 flex flex-col items-center justify-center">
<h1 class="text-9xl font-bold text-accent">404</h1>
<span aria-hidden="true">¯\_(ツ)_/¯</span> <span aria-hidden="true">¯\_(ツ)_/¯</span>
<p>Page Not Found</p> <p class="mt-4 text-2xl sm:text-3xl">Page Not Found</p>
<LinkButton <LinkButton
href="/" href="/"
className="my-6 text-lg underline decoration-dashed underline-offset-8" class="my-6 text-lg underline decoration-dashed underline-offset-8"
> >
Go back home Go back home
</LinkButton> </LinkButton>
@@ -25,18 +28,3 @@ import LinkButton from "@components/LinkButton.astro";
<Footer /> <Footer />
</Layout> </Layout>
<style>
#main-content {
@apply mx-auto flex max-w-3xl flex-1 items-center justify-center;
}
.not-found-wrapper {
@apply mb-14 flex flex-col items-center justify-center;
}
.not-found-wrapper h1 {
@apply text-9xl font-bold text-skin-accent;
}
.not-found-wrapper p {
@apply mt-4 text-2xl sm:text-3xl;
}
</style>
+29 -24
View File
@@ -1,12 +1,12 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import Card from "@components/Card"; import Main from "@/layouts/Main.astro";
import Footer from "@components/Footer.astro"; import Layout from "@/layouts/Layout.astro";
import Header from "@components/Header.astro"; import Header from "@/components/Header.astro";
import { SITE } from "@config"; import Footer from "@/components/Footer.astro";
import Layout from "@layouts/Layout.astro"; import Card from "@/components/Card";
import Main from "@layouts/Main.astro"; import getPostsByGroupCondition from "@/utils/getPostsByGroupCondition";
import getPostsByGroupCondition from "@utils/getPostsByGroupCondition"; import { SITE } from "@/config";
// Redirect to 404 page if `showArchives` config is false // Redirect to 404 page if `showArchives` config is false
if (!SITE.showArchives) { if (!SITE.showArchives) {
@@ -15,24 +15,26 @@ if (!SITE.showArchives) {
const posts = await getCollection("blog", ({ data }) => !data.draft); const posts = await getCollection("blog", ({ data }) => !data.draft);
const MonthMap: Record<string, string> = { const months = [
"1": "January", "January",
"2": "February", "February",
"3": "March", "March",
"4": "April", "April",
"5": "May", "May",
"6": "June", "June",
"7": "July", "July",
"8": "August", "August",
"9": "September", "September",
"10": "October", "October",
"11": "November", "November",
"12": "December", "December",
}; ];
const backUrl = SITE.showBackButton ? `?from=${Astro.url.pathname}` : "/";
--- ---
<Layout title={`Archives | ${SITE.title}`}> <Layout title={`Archives | ${SITE.title}`}>
<Header activeNav="archives" /> <Header />
<Main pageTitle="Archives" pageDesc="All the articles I've archived."> <Main pageTitle="Archives" pageDesc="All the articles I've archived.">
{ {
Object.entries( Object.entries(
@@ -55,7 +57,7 @@ const MonthMap: Record<string, string> = {
.map(([month, monthGroup]) => ( .map(([month, monthGroup]) => (
<div class="flex flex-col sm:flex-row"> <div class="flex flex-col sm:flex-row">
<div class="mt-6 min-w-36 text-lg sm:my-6"> <div class="mt-6 min-w-36 text-lg sm:my-6">
<span class="font-bold">{MonthMap[month]}</span> <span class="font-bold">{months[Number(month) - 1]}</span>
<sup class="text-xs">{monthGroup.length}</sup> <sup class="text-xs">{monthGroup.length}</sup>
</div> </div>
<ul> <ul>
@@ -70,7 +72,10 @@ const MonthMap: Record<string, string> = {
) )
) )
.map(({ data, id }) => ( .map(({ data, id }) => (
<Card href={`/posts/${id}`} frontmatter={data} /> <Card
href={`/posts/${id}${backUrl}`}
frontmatter={data}
/>
))} ))}
</ul> </ul>
</div> </div>
+33 -75
View File
@@ -1,43 +1,44 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import Header from "@components/Header.astro"; import Header from "@/components/Header.astro";
import Footer from "@components/Footer.astro"; import Footer from "@/components/Footer.astro";
import LinkButton from "@components/LinkButton.astro"; import Socials from "@/components/Socials.astro";
import Hr from "@components/Hr.astro"; import LinkButton from "@/components/LinkButton.astro";
import Card from "@components/Card"; import Card from "@/components/Card";
import Socials from "@components/Socials.astro"; import Hr from "@/components/Hr.astro";
import getSortedPosts from "@utils/getSortedPosts"; import getSortedPosts from "@/utils/getSortedPosts";
import { SITE, SOCIALS } from "@config"; import IconRss from "@/assets/icons/IconRss.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import { SITE } from "@/config";
import { SOCIALS } from "@/constants";
const posts = await getCollection("blog"); const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts); const sortedPosts = getSortedPosts(posts);
const featuredPosts = sortedPosts.filter(({ data }) => data.featured); const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
const recentPosts = sortedPosts.filter(({ data }) => !data.featured); const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
const socialCount = SOCIALS.filter(social => social.active).length;
--- ---
<Layout> <Layout>
<Header /> <Header />
<main id="main-content"> <main id="main-content">
<section id="hero"> <section id="hero" class="pt-8 pb-6">
<h1>Mingalaba</h1> <h1 class="my-4 inline-block text-4xl font-bold sm:my-8 sm:text-5xl">
Mingalaba
</h1>
<a <a
target="_blank" target="_blank"
href="/rss.xml" href="/rss.xml"
class="rss-link" class="inline-block"
aria-label="rss feed" aria-label="rss feed"
title="RSS Feed" title="RSS Feed"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="rss-icon" <IconRss
><path width={20}
d="M19 20.001C19 11.729 12.271 5 4 5v2c7.168 0 13 5.832 13 13.001h2z" height={20}
></path><path class="scale-125 stroke-accent stroke-3"
d="M12 20.001h2C14 14.486 9.514 10 4 10v2c4.411 0 8 3.589 8 8.001z" />
></path><circle cx="6" cy="18" r="2"></circle>
</svg>
<span class="sr-only">RSS Feed</span> <span class="sr-only">RSS Feed</span>
</a> </a>
@@ -47,10 +48,10 @@ const socialCount = SOCIALS.filter(social => social.active).length;
out of the box. Light and dark mode are supported by default. Moreover, out of the box. Light and dark mode are supported by default. Moreover,
additional color schemes can also be configured. additional color schemes can also be configured.
</p> </p>
<p> <p class="mt-2">
Read the blog posts or check Read the blog posts or check
<LinkButton <LinkButton
className="underline decoration-dashed underline-offset-4 hover:text-skin-accent" class="underline decoration-dashed underline-offset-4 hover:text-accent"
href="https://github.com/satnaing/astro-paper#readme" href="https://github.com/satnaing/astro-paper#readme"
> >
README README
@@ -58,9 +59,9 @@ const socialCount = SOCIALS.filter(social => social.active).length;
</p> </p>
{ {
// only display if at least one social link is enabled // only display if at least one social link is enabled
socialCount > 0 && ( SOCIALS.length > 0 && (
<div class="social-wrapper"> <div class="mt-4 flex flex-col sm:flex-row sm:items-center">
<div class="social-links">Social Links:</div> <div class="mr-2 mb-1 whitespace-nowrap sm:mb-0">Social Links:</div>
<Socials /> <Socials />
</div> </div>
) )
@@ -72,8 +73,8 @@ const socialCount = SOCIALS.filter(social => social.active).length;
{ {
featuredPosts.length > 0 && ( featuredPosts.length > 0 && (
<> <>
<section id="featured"> <section id="featured" class="pt-12 pb-6">
<h2>Featured</h2> <h2 class="text-2xl font-semibold tracking-wide">Featured</h2>
<ul> <ul>
{featuredPosts.map(({ data, id }) => ( {featuredPosts.map(({ data, id }) => (
<Card <Card
@@ -91,8 +92,8 @@ const socialCount = SOCIALS.filter(social => social.active).length;
{ {
recentPosts.length > 0 && ( recentPosts.length > 0 && (
<section id="recent-posts"> <section id="recent-posts" class="pt-12 pb-6">
<h2>Recent Posts</h2> <h2 class="text-2xl font-semibold tracking-wide">Recent Posts</h2>
<ul> <ul>
{recentPosts.map( {recentPosts.map(
({ data, id }, index) => ({ data, id }, index) =>
@@ -109,55 +110,12 @@ const socialCount = SOCIALS.filter(social => social.active).length;
) )
} }
<div class="all-posts-btn-wrapper"> <div class="my-8 text-center">
<LinkButton href="/posts/"> <LinkButton href="/posts/">
All Posts All Posts
<svg xmlns="http://www.w3.org/2000/svg" <IconArrowRight class="inline-block" />
><path
d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z"
></path>
</svg>
</LinkButton> </LinkButton>
</div> </div>
</main> </main>
<Footer /> <Footer />
</Layout> </Layout>
<style>
/* ===== Hero Section ===== */
#hero {
@apply pb-6 pt-8;
}
#hero h1 {
@apply my-4 inline-block text-3xl font-bold sm:my-8 sm:text-5xl;
}
#hero .rss-link {
@apply mb-6;
}
#hero .rss-icon {
@apply mb-2 h-6 w-6 scale-110 fill-skin-accent sm:mb-3 sm:scale-125;
}
#hero p {
@apply my-2;
}
.social-wrapper {
@apply mt-4 flex flex-col sm:flex-row sm:items-center;
}
.social-links {
@apply mb-1 mr-2 whitespace-nowrap sm:mb-0;
}
/* ===== Featured & Recent Posts Sections ===== */
#featured,
#recent-posts {
@apply pb-6 pt-12;
}
#featured h2,
#recent-posts h2 {
@apply text-2xl font-semibold tracking-wide;
}
.all-posts-btn-wrapper {
@apply my-8 text-center;
}
</style>
+26 -4
View File
@@ -1,9 +1,14 @@
--- ---
import { SITE } from "@config";
import Posts from "@layouts/Posts.astro";
import type { GetStaticPaths } from "astro"; import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import getSortedPosts from "@utils/getSortedPosts"; import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Card from "@/components/Card";
import Pagination from "@/components/Pagination.astro";
import getSortedPosts from "@/utils/getSortedPosts";
import { SITE } from "@/config";
export const getStaticPaths = (async ({ paginate }) => { export const getStaticPaths = (async ({ paginate }) => {
const posts = await getCollection("blog", ({ data }) => !data.draft); const posts = await getCollection("blog", ({ data }) => !data.draft);
@@ -11,6 +16,23 @@ export const getStaticPaths = (async ({ paginate }) => {
}) satisfies GetStaticPaths; }) satisfies GetStaticPaths;
const { page } = Astro.props; const { page } = Astro.props;
const backUrl = SITE.showBackButton ? `?from=${Astro.url.pathname}` : "/";
--- ---
<Posts {page} /> <Layout title={`Posts | ${SITE.title}`}>
<Header />
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
<ul>
{
page.data.map(({ data, id }) => (
<Card href={`/posts/${id}${backUrl}`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>
+11 -9
View File
@@ -1,12 +1,12 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE } from "@config"; import Main from "@/layouts/Main.astro";
import Layout from "@layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import Main from "@layouts/Main.astro"; import Header from "@/components/Header.astro";
import Header from "@components/Header.astro"; import Footer from "@/components/Footer.astro";
import Footer from "@components/Footer.astro"; import SearchBar from "@/components/SearchBar";
import SearchBar from "@components/Search"; import getSortedPosts from "@/utils/getSortedPosts";
import getSortedPosts from "@utils/getSortedPosts"; import { SITE } from "@/config";
// Retrieve all published articles // Retrieve all published articles
const posts = await getCollection("blog", ({ data }) => !data.draft); const posts = await getCollection("blog", ({ data }) => !data.draft);
@@ -19,12 +19,14 @@ const searchList = sortedPosts.map(({ data, id }) => ({
data, data,
slug: id, slug: id,
})); }));
const backUrl = SITE.showBackButton ? `?from=${Astro.url.pathname}` : "/";
--- ---
<Layout title={`Search | ${SITE.title}`}> <Layout title={`Search | ${SITE.title}`}>
<Header activeNav="search" /> <Header />
<Main pageTitle="Search" pageDesc="Search any article ..."> <Main pageTitle="Search" pageDesc="Search any article ...">
<SearchBar client:load searchList={searchList} /> <SearchBar client:load searchList={searchList} backUrl={backUrl} />
</Main> </Main>
<Footer /> <Footer />
</Layout> </Layout>
+32 -5
View File
@@ -1,10 +1,15 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import TagPosts from "@layouts/TagPosts.astro";
import getUniqueTags from "@utils/getUniqueTags";
import getPostsByTag from "@utils/getPostsByTag";
import type { GetStaticPathsOptions } from "astro"; import type { GetStaticPathsOptions } from "astro";
import { SITE } from "@config"; import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Card from "@/components/Card";
import Pagination from "@/components/Pagination.astro";
import getUniqueTags from "@/utils/getUniqueTags";
import getPostsByTag from "@/utils/getPostsByTag";
import { SITE } from "@/config";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) { export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const posts = await getCollection("blog"); const posts = await getCollection("blog");
@@ -24,6 +29,28 @@ export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const params = Astro.params; const params = Astro.params;
const { tag } = params; const { tag } = params;
const { page, tagName } = Astro.props; const { page, tagName } = Astro.props;
const backUrl = SITE.showBackButton ? `?from=${Astro.url.pathname}` : "/";
--- ---
<TagPosts {page} {tag} {tagName} /> <Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Header />
<Main
pageTitle={[`Tag:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tagName}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
page.data.map(({ data, id }) => (
<Card href={`/posts/${id}${backUrl}`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>
+3 -3
View File
@@ -1,10 +1,10 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import Tag from "@/components/Tag.astro";
import Header from "@/components/Header.astro"; import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro"; import Footer from "@/components/Footer.astro";
import Main from "@/layouts/Main.astro";
import Tag from "@/components/Tag.astro";
import getUniqueTags from "@/utils/getUniqueTags"; import getUniqueTags from "@/utils/getUniqueTags";
import { SITE } from "@/config"; import { SITE } from "@/config";
@@ -14,7 +14,7 @@ let tags = getUniqueTags(posts);
--- ---
<Layout title={`Tags | ${SITE.title}`}> <Layout title={`Tags | ${SITE.title}`}>
<Header activeNav="tags" /> <Header />
<Main pageTitle="Tags" pageDesc="All the tags used in posts."> <Main pageTitle="Tags" pageDesc="All the tags used in posts.">
<ul> <ul>
{tags.map(({ tag, tagName }) => <Tag {tag} {tagName} size="lg" />)} {tags.map(({ tag, tagName }) => <Tag {tag} {tagName} size="lg" />)}
-127
View File
@@ -1,127 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root,
html[data-theme="light"] {
--color-fill: 251, 254, 251;
--color-text-base: 40, 39, 40;
--color-accent: 0, 108, 172;
--color-card: 230, 230, 230;
--color-card-muted: 205, 205, 205;
--color-border: 236, 233, 233;
}
html[data-theme="dark"] {
--color-fill: 33, 39, 55;
--color-text-base: 234, 237, 243;
--color-accent: 255, 107, 1;
--color-card: 52, 63, 96;
--color-card-muted: 138, 51, 2;
--color-border: 171, 75, 8;
}
#sun-svg,
html[data-theme="dark"] #moon-svg {
display: none;
}
#moon-svg,
html[data-theme="dark"] #sun-svg {
display: block;
}
body {
@apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base selection:bg-skin-accent/70 selection:text-skin-inverted;
}
section,
footer {
@apply mx-auto max-w-3xl px-4;
}
a {
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
}
svg {
@apply inline-block h-6 w-6 fill-skin-base group-hover:fill-skin-accent;
}
svg.icon-tabler {
@apply inline-block h-6 w-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110;
}
.prose {
@apply prose-headings:!mb-3 prose-headings:!text-skin-base prose-h3:italic prose-p:!text-skin-base prose-a:!text-skin-base prose-a:!decoration-dashed prose-a:underline-offset-8 hover:prose-a:text-skin-accent prose-blockquote:!border-l-skin-accent/50 prose-blockquote:opacity-80 prose-figcaption:!text-skin-base prose-figcaption:opacity-70 prose-strong:!text-skin-base prose-code:rounded prose-code:bg-skin-card/75 prose-code:p-1 prose-code:before:!content-none prose-code:after:!content-none prose-ol:!text-skin-base prose-ul:overflow-x-clip prose-ul:!text-skin-base prose-li:marker:!text-skin-accent prose-table:text-skin-base prose-th:border prose-th:border-skin-line prose-td:border prose-td:border-skin-line prose-img:!my-2 prose-img:mx-auto prose-img:border-2 prose-img:border-skin-line prose-hr:!border-skin-line;
}
.prose a {
@apply break-words hover:!text-skin-accent;
}
.prose thead th:first-child,
tbody td:first-child,
tfoot td:first-child {
padding-left: 0.5714286em;
}
.prose h2#table-of-contents {
@apply mb-2;
}
.prose details {
@apply inline-block cursor-pointer select-none text-skin-base;
}
.prose summary {
@apply focus-outline;
}
.prose h2#table-of-contents + p {
@apply hidden;
}
/* ===== scrollbar ===== */
html {
overflow-y: scroll;
}
/* width */
::-webkit-scrollbar {
@apply w-3;
}
/* Track */
::-webkit-scrollbar-track {
@apply bg-skin-fill;
}
/* Handle */
::-webkit-scrollbar-thumb {
@apply bg-skin-card;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
@apply bg-skin-card-muted;
}
/* ===== Code Blocks & Syntax Highlighting ===== */
pre:has(code) {
@apply border border-skin-line;
}
code,
blockquote {
word-wrap: break-word;
}
pre > code {
white-space: pre;
}
/* Apply Dark Theme (if multi-theme specified) */
html[data-theme="dark"] pre:has(code),
html[data-theme="dark"] pre:has(code) span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
@layer components {
.display-none {
@apply hidden;
}
.focus-outline {
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
}
}
+59
View File
@@ -0,0 +1,59 @@
@import "tailwindcss";
@import "./typography.css";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
:root,
html[data-theme="light"] {
--background: #fdfdfd;
--foreground: #282728;
--accent: #006cac;
--muted: #e6e6e6;
--border: #ece9e9;
}
html[data-theme="dark"] {
--background: #212737;
--foreground: #eaedf3;
--accent: #ff6b01;
--muted: #343f60bf;
--border: #ab4b08;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-accent: var(--accent);
--color-muted: var(--muted);
--color-border: var(--border);
}
@layer base {
* {
@apply border-border outline-accent/75;
scrollbar-width: auto;
scrollbar-color: var(--color-muted) transparent;
}
html {
@apply overflow-y-scroll scroll-smooth;
}
body {
@apply flex min-h-svh flex-col bg-background font-mono text-foreground selection:bg-accent/75 selection:text-background;
}
a,
button {
@apply outline-offset-1 outline-accent focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-dashed;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
section,
footer {
@apply mx-auto max-w-3xl px-4;
}
}
.active-nav {
@apply underline decoration-wavy decoration-2 underline-offset-4;
}
+50
View File
@@ -0,0 +1,50 @@
@plugin '@tailwindcss/typography';
@layer base {
.prose {
@apply prose-headings:!mb-3 prose-headings:!text-foreground prose-h3:italic prose-p:!text-foreground prose-a:!text-foreground prose-a:!decoration-dashed prose-a:underline-offset-8 hover:prose-a:text-accent prose-blockquote:!border-l-accent/50 prose-blockquote:opacity-80 prose-figcaption:!text-foreground prose-figcaption:opacity-70 prose-strong:!text-foreground prose-code:rounded prose-code:bg-muted/75 prose-code:p-1 prose-code:!text-foreground prose-code:before:!content-none prose-code:after:!content-none prose-ol:!text-foreground prose-ul:overflow-x-clip prose-ul:!text-foreground prose-li:marker:!text-accent prose-table:text-foreground prose-th:border prose-th:border-border prose-td:border prose-td:border-border prose-img:mx-auto prose-img:!my-2 prose-img:border-2 prose-img:border-border prose-hr:!border-border;
}
.prose a {
@apply break-words hover:!text-accent;
}
.prose thead th:first-child,
tbody td:first-child,
tfoot td:first-child {
padding-left: 0.5714286em;
}
.prose h2#table-of-contents {
@apply mb-2;
}
.prose details {
@apply inline-block cursor-pointer text-foreground select-none;
}
.prose summary {
@apply focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent focus-visible:outline-dashed;
}
.prose h2#table-of-contents + p {
@apply hidden;
}
/* ===== Code Blocks & Syntax Highlighting ===== */
pre:has(code) {
@apply border border-border;
}
code,
blockquote {
word-wrap: break-word;
}
pre > code {
white-space: pre;
}
/* Apply Dark Theme (if multi-theme specified) */
html[data-theme="dark"] pre:has(code),
html[data-theme="dark"] pre:has(code) span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
-81
View File
@@ -1,81 +0,0 @@
function withOpacity(variableName) {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(${variableName}), ${opacityValue})`;
}
return `rgb(var(${variableName}))`;
};
}
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["selector", "[data-theme='dark']"],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
// Remove the following screen breakpoint or add other breakpoints
// if one breakpoint is not enough for you
screens: {
sm: "640px",
},
extend: {
textColor: {
skin: {
base: withOpacity("--color-text-base"),
accent: withOpacity("--color-accent"),
inverted: withOpacity("--color-fill"),
},
},
backgroundColor: {
skin: {
fill: withOpacity("--color-fill"),
accent: withOpacity("--color-accent"),
inverted: withOpacity("--color-text-base"),
card: withOpacity("--color-card"),
"card-muted": withOpacity("--color-card-muted"),
},
},
outlineColor: {
skin: {
fill: withOpacity("--color-accent"),
},
},
borderColor: {
skin: {
line: withOpacity("--color-border"),
fill: withOpacity("--color-text-base"),
accent: withOpacity("--color-accent"),
},
},
fill: {
skin: {
base: withOpacity("--color-text-base"),
accent: withOpacity("--color-accent"),
},
transparent: "transparent",
},
stroke: {
skin: {
accent: withOpacity("--color-accent")
}
},
fontFamily: {
mono: ["IBM Plex Mono", "monospace"],
},
typography: {
DEFAULT: {
css: {
pre: {
color: false,
},
code: {
color: false,
},
},
},
},
},
},
plugins: [require("@tailwindcss/typography")],
};