feat: tailwind v4, expressive-code migration (#12)

This commit is contained in:
enscribe 2025-03-22 02:29:21 -07:00 committed by GitHub
commit 6cac15f53b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 8725 additions and 8843 deletions

14
.gitignore vendored
View file

@ -1,27 +1,13 @@
# build output
dist/ dist/
# generated types
.astro/ .astro/
# dependencies
node_modules/ node_modules/
# logs
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
# environment variables
.env .env
.env.production .env.production
# macOS-specific files
.DS_Store .DS_Store
# jetbrains setting folder
.idea/ .idea/
teaser.pptx teaser.pptx
~$teaser.pptx ~$teaser.pptx

107
README.md
View file

@ -20,18 +20,19 @@ astro-erudite is an opinionated, no-frills static blogging template built with [
Below are some fantastic examples of websites based on this template. If you wish to add your site to this list, open a pull request! Below are some fantastic examples of websites based on this template. If you wish to add your site to this list, open a pull request!
| Site | Author | Description/Features | Source | | Site | Author | Description/Features | Source |
|------|--------|----------------------|--------| | --------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| [enscribe.dev](https://enscribe.dev) | [@jktrn](https://github.com/jktrn) | Heavily modified bento-style homepage with client interactivity, with custom MDX components! | [](https://github.com/jktrn/enscribe.dev) | | [enscribe.dev](https://enscribe.dev) | [@jktrn](https://github.com/jktrn) | Heavily modified bento-style homepage with client interactivity, with custom MDX components! | [](https://github.com/jktrn/enscribe.dev) |
| [emile.sh](https://emile.sh) | [@echoghi](https://github.com/echoghi) | A minimalist personal blog using the [flexoki](https://stephango.com/flexoki) theme | [](https://github.com/echoghi/v5) | | [emile.sh](https://emile.sh) | [@echoghi](https://github.com/echoghi) | A minimalist personal blog using the [flexoki](https://stephango.com/flexoki) theme | [](https://github.com/echoghi/v5) |
| [decentparadox.me](https://decentparadox.me) | [@decentparadox](https://github.com/decentparadox) | A heavily customized personal portfolio with a sci-fi theme! | [](https://github.com/decentparadox/decentparadox.me) | | [decentparadox.me](https://decentparadox.me) | [@decentparadox](https://github.com/decentparadox) | A heavily customized personal portfolio with a sci-fi theme! | [](https://github.com/decentparadox/decentparadox.me) |
| [flocto.github.io](https://flocto.github.io/) | [@flocto](https://github.com/flocto) | A slightly modified personal blog | [](https://github.com/flocto/flocto.github.io) | | [flocto.github.io](https://flocto.github.io/) | [@flocto](https://github.com/flocto) | A slightly modified personal blog | [](https://github.com/flocto/flocto.github.io) |
[dumbprism.me](https://www.dumbprism.me/) | [@dumbprism](https://github.com/dumbprism) | A customized portfolio inspired by enscribe's bento grid style adding my gist of UI | [](https://github.com/dumbprism/dumbprism-portfolio) | | [dumbprism.me](https://www.dumbprism.me/) | [@dumbprism](https://github.com/dumbprism) | A customized portfolio inspired by enscribe's bento grid style adding my gist of UI | [](https://github.com/dumbprism/dumbprism-portfolio) |
## Features ## Features
- [Astro](https://astro.build/)’s [Islands](https://docs.astro.build/en/concepts/islands/) architecture for partial/selective hydration and client-side interactivity while maintaining a fast-to-render static site. - [Astro](https://astro.build/)’s [Islands](https://docs.astro.build/en/concepts/islands/) architecture for partial/selective hydration and client-side interactivity while maintaining a fast-to-render static site.
- [shadcn/ui](https://ui.shadcn.com/)’s [Tailwind](https://tailwindcss.com/) color convention for automatic styling across both light and dark themes. Includes accessible, theme-aware UI components for navigation, buttons, etc. - [shadcn/ui](https://ui.shadcn.com/)’s [Tailwind](https://tailwindcss.com/) color convention for automatic styling across both light and dark themes. Includes accessible, theme-aware UI components for navigation, buttons, etc.
- [rehype-pretty-code](https://rehype-pretty.pages.dev/) with [Shiki](https://github.com/shikijs/shiki) for advanced code block styling, highlighting, and code block titles/captions. - [Expressive Code](https://expressive-code.com/) for advanced code block styling, highlighting, and code block titles/captions.
- Blog post authoring using [MDX](https://mdxjs.com/) for component-style content, alongside $\LaTeX$ rendering using [KaTeX](https://katex.org/). - Blog post authoring using [MDX](https://mdxjs.com/) for component-style content, alongside $\LaTeX$ rendering using [KaTeX](https://katex.org/).
- Astro [View Transitions](https://docs.astro.build/en/guides/view-transitions/) in <abbr title="Single Page Application">SPA</abbr> mode for smooth, opt-in animations during route switching. - Astro [View Transitions](https://docs.astro.build/en/guides/view-transitions/) in <abbr title="Single Page Application">SPA</abbr> mode for smooth, opt-in animations during route switching.
- SEO optimization with fine-grained control over metadata and [Open Graph](https://ogp.me/) tags for each post. - SEO optimization with fine-grained control over metadata and [Open Graph](https://ogp.me/) tags for each post.
@ -43,15 +44,15 @@ Below are some fantastic examples of websites based on this template. If you wis
This is a list of the various technologies used to build this template: This is a list of the various technologies used to build this template:
| Category | Technology Name | | Category | Technology Name |
| ------------------- | -------------------------------------------------------------------------------------------------- | | ---------- | ------------------------------------------------------------------------------------------ |
| Framework | [Astro](https://astro.build/) | | Framework | [Astro](https://astro.build/) |
| Styling | [Tailwind](https://tailwindcss.com) | | Styling | [Tailwind](https://tailwindcss.com) |
| Components | [shadcn/ui](https://ui.shadcn.com/) | | Components | [shadcn/ui](https://ui.shadcn.com/) |
| Content | [MDX](https://mdxjs.com/) | | Content | [MDX](https://mdxjs.com/) |
| Syntax Highlighting | [Shiki](https://github.com/shikijs/shiki) + [rehype-pretty-code](https://rehype-pretty.pages.dev/) | | Codeblocks | [Expressive Code](https://expressive-code.com/), [Shiki](https://github.com/shikijs/shiki) |
| Graphics | [Figma](https://www.figma.com/) | | Graphics | [Figma](https://www.figma.com/) |
| Deployment | [Vercel](https://vercel.com) | | Deployment | [Vercel](https://vercel.com) |
## Getting Started ## Getting Started
@ -122,29 +123,27 @@ export const SOCIAL_LINKS: Link[] = [
Colors are defined in `src/styles/global.css` in [<abbr title="Hue, Saturation, Lightness">HSL</abbr> format](https://en.wikipedia.org/wiki/HSL_and_HSV), using the [shadcn/ui](https://ui.shadcn.com/) convention: Colors are defined in `src/styles/global.css` in [<abbr title="Hue, Saturation, Lightness">HSL</abbr> format](https://en.wikipedia.org/wiki/HSL_and_HSV), using the [shadcn/ui](https://ui.shadcn.com/) convention:
```css ```css
@layer base { :root {
:root { --background: oklch(1 0 0);
--background: 0 0% 100%; --foreground: oklch(0.145 0 0);
--foreground: 0 0% 3.9%; --card: oklch(1 0 0);
--primary: 0 0% 9%; --card-foreground: oklch(0.145 0 0);
--primary-foreground: 0 0% 98%; --popover: oklch(1 0 0);
--secondary: 0 0% 80.1%; --popover-foreground: oklch(0.145 0 0);
--secondary-foreground: 0 0% 9%; --primary: oklch(0.205 0 0);
--muted: 0 0% 80.1%; --primary-foreground: oklch(0.985 0 0);
--muted-foreground: 0 0% 45.1%; --secondary: oklch(0.97 0 0);
--accent: 0 0% 80.1%; --secondary-foreground: oklch(0.205 0 0);
--accent-foreground: 0 0% 9%; --muted: oklch(0.97 0 0);
--additive: 112 50% 36%; /* Unique to astro-erudite */ --muted-foreground: oklch(0.556 0 0);
--additive-foreground: 0 0% 98%; /* Unique to astro-erudite */ --accent: oklch(0.97 0 0);
--destructive: 0 84.2% 60.2%; --accent-foreground: oklch(0.205 0 0);
--destructive-foreground: 0 0% 98%; --destructive: oklch(0.577 0.245 27.325);
--border: 0 0% 89.8%; --border: oklch(0.922 0 0);
--ring: 0 0% 3.9%; --ring: oklch(0.708 0 0);
} }
.dark { .dark {
/* ... */
}
/* ... */ /* ... */
} }
``` ```
@ -169,15 +168,15 @@ draft: false
The blog post schema is defined as follows: The blog post schema is defined as follows:
| Field | Type (Zod) | Requirements | Required | | Field | Type (Zod) | Requirements | Required |
| ------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `title` | `string` | Must be ≤60 characters. | Yes | | `title` | `string` | Must be ≤60 characters. | Yes |
| `description` | `string` | Must be ≤155 characters. | Yes | | `description` | `string` | Must be ≤155 characters. | Yes |
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes | | `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
| `image` | `image()` | Must be exactly 1200px &times; 630px. | Optional | | `image` | `image()` | Should be exactly 1200px &times; 630px. | Optional |
| `tags` | `string[]` | Preferably use kebab-case for these. | Optional | | `tags` | `string[]` | Preferably use kebab-case for these. | Optional |
| `authors` | `string[]` | If the author has a profile, use the id associated with their Markdown file in `src/content/authors/` (e.g. if their file is named `jane-doe.md`, use `jane-doe` in the array). | Optional | | `authors` | `string[]` | If the author has a profile, use the id associated with their Markdown file in `src/content/authors/` (e.g. if their file is named `jane-doe.md`, use `jane-doe` in the array). | Optional |
| `draft` | `boolean` | Defaults to `false` if not provided. | Optional | | `draft` | `boolean` | Defaults to `false` if not provided. | Optional |
### Authors ### Authors
@ -230,13 +229,13 @@ link: 'https://example.com'
The project schema is defined as follows: The project schema is defined as follows:
| Field | Type (Zod) | Requirements | Required | | Field | Type (Zod) | Requirements | Required |
| ------------- | -------------- | ------------------------------------- | -------- | | ------------- | -------------- | --------------------------------------- | -------- |
| `name` | `string` | n/a | Yes | | `name` | `string` | n/a | Yes |
| `description` | `string` | n/a | Yes | | `description` | `string` | n/a | Yes |
| `tags` | `string[]` | n/a | Yes | | `tags` | `string[]` | n/a | Yes |
| `image` | `image()` | Must be exactly 1200px &times; 630px. | Yes | | `image` | `image()` | Should be exactly 1200px &times; 630px. | Yes |
| `link` | `string.url()` | Must be a valid URL. | Yes | | `link` | `string.url()` | Must be a valid URL. | Yes |
## License ## License

View file

@ -1,36 +1,85 @@
import { rehypeHeadingIds } from '@astrojs/markdown-remark' import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx' import mdx from '@astrojs/mdx'
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 { transformerCopyButton } from '@rehype-pretty/transformers'
import {
transformerMetaHighlight,
transformerNotationDiff,
} from '@shikijs/transformers'
import { defineConfig } from 'astro/config'
import rehypeKatex from 'rehype-katex'
import rehypeExternalLinks from 'rehype-external-links'
import rehypePrettyCode from 'rehype-pretty-code'
import remarkEmoji from 'remark-emoji'
import remarkMath from 'remark-math'
import remarkToc from 'remark-toc'
import sectionize from '@hbsnow/rehype-sectionize'
import icon from 'astro-icon' import icon from 'astro-icon'
// https://astro.build/config import expressiveCode from 'astro-expressive-code'
import { rehypeHeadingIds } from '@astrojs/markdown-remark'
import rehypeExternalLinks from 'rehype-external-links'
import rehypeKatex from 'rehype-katex'
import rehypePrettyCode from 'rehype-pretty-code'
// import sectionize from '@hbsnow/rehype-sectionize'
import remarkEmoji from 'remark-emoji'
import remarkMath from 'remark-math'
import remarkSectionize from 'remark-sectionize'
import remarkToc from 'remark-toc'
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
site: 'https://astro-erudite.vercel.app', site: 'https://astro-erudite.vercel.app',
integrations: [ integrations: [
tailwind({ expressiveCode({
applyBaseStyles: false, themes: ['github-light', 'github-dark'],
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
useDarkModeMediaQuery: false,
themeCssSelector: (theme) => `.${theme.name.split('-')[1]}`,
defaultProps: {
wrap: true,
collapseStyle: 'collapsible-auto',
overridesByLang: {
'ansi,bat,bash,batch,cmd,console,powershell,ps,ps1,psd1,psm1,sh,shell,shellscript,shellsession,text,zsh':
{
showLineNumbers: false,
},
},
},
styleOverrides: {
borderColor: 'var(--border)',
codeFontFamily: 'var(--font-mono)',
codeBackground:
'color-mix(in oklab, var(--secondary) 25%, transparent)',
frames: {
editorActiveTabForeground: 'var(--muted-foreground)',
editorActiveTabBackground:
'color-mix(in oklab, var(--secondary) 25%, transparent)',
editorActiveTabIndicatorBottomColor: 'transparent',
editorActiveTabIndicatorTopColor: 'transparent',
editorTabBarBackground: 'transparent',
editorTabBarBorderBottomColor: 'transparent',
frameBoxShadowCssValue: 'none',
terminalBackground:
'color-mix(in oklab, var(--secondary) 25%, transparent)',
terminalTitlebarBackground: 'transparent',
terminalTitlebarBorderBottomColor: 'transparent',
terminalTitlebarForeground: 'var(--muted-foreground)',
},
lineNumbers: {
foreground: 'var(--muted-foreground)',
},
uiFontFamily: 'var(--font-sans)',
},
}), }),
sitemap(),
mdx(), mdx(),
react(), react(),
sitemap(),
icon(), icon(),
], ],
vite: {
plugins: [tailwindcss()],
},
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
},
markdown: { markdown: {
syntaxHighlight: false, syntaxHighlight: false,
rehypePlugins: [ rehypePlugins: [
@ -43,32 +92,17 @@ export default defineConfig({
], ],
rehypeHeadingIds, rehypeHeadingIds,
rehypeKatex, rehypeKatex,
sectionize, // sectionize,
[ [
rehypePrettyCode, rehypePrettyCode,
{ {
theme: { theme: {
light: 'github-light-high-contrast', light: 'github-light',
dark: 'github-dark-high-contrast', dark: 'github-dark',
}, },
transformers: [
transformerNotationDiff(),
transformerMetaHighlight(),
transformerCopyButton({
visibility: 'hover',
feedbackDuration: 1000,
}),
],
}, },
], ],
], ],
remarkPlugins: [remarkToc, remarkMath, remarkEmoji], remarkPlugins: [remarkToc, remarkMath, remarkEmoji, remarkSectionize],
},
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
}, },
}) })

15179
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "astro-erudite", "name": "astro-erudite",
"type": "module", "type": "module",
"version": "1.2.4", "version": "1.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
@ -9,53 +9,55 @@
"build": "astro check && astro build", "build": "astro check && astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"prettier": "prettier --write ." "prettier": "prettier --write .",
"postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/markdown-remark": "^6.1.0", "@astrojs/markdown-remark": "^6.3.1",
"@astrojs/mdx": "^4.0.8", "@astrojs/mdx": "^4.2.1",
"@astrojs/react": "^4.2.0", "@astrojs/react": "^4.2.1",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.3.0",
"@astrojs/tailwind": "^6.0.0", "@expressive-code/plugin-collapsible-sections": "^0.40.2",
"@hbsnow/rehype-sectionize": "^1.0.7", "@expressive-code/plugin-line-numbers": "^0.40.2",
"@iconify-json/lucide": "^1.2.20", "@iconify-json/lucide": "^1.2.26",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.2",
"@rehype-pretty/transformers": "^0.13.2", "@rehype-pretty/transformers": "^0.13.2",
"@shikijs/transformers": "^1.24.4", "@tailwindcss/vite": "^4.0.7",
"@types/react": "^18.3.18", "@types/react": "19.0.0",
"@types/react-dom": "^18.3.5", "@types/react-dom": "19.0.0",
"astro": "^5.3.0", "astro": "^5.5.4",
"astro-icon": "^1.1.4", "astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"react": "^18.3.1", "patch-package": "^8.0.0",
"react-dom": "^18.3.1", "react": "19.0.0",
"react-dom": "19.0.0",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-pretty-code": "^0.14.0", "rehype-pretty-code": "^0.14.1",
"remark-emoji": "^5.0.1", "remark-emoji": "^5.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^3.0.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.0.7",
"tailwindcss-animate": "^1.0.7", "typescript": "^5.7.3"
"typescript": "^5.7.2"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.15", "prettier": "^3.5.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-astro-organize-imports": "^0.4.11", "prettier-plugin-astro-organize-imports": "^0.4.11",
"prettier-plugin-tailwindcss": "^0.6.9" "prettier-plugin-tailwindcss": "^0.6.11"
}, },
"prettier": { "prettier": {
"semi": false, "semi": false,

View file

@ -0,0 +1,13 @@
diff --git a/node_modules/rehype-pretty-code/dist/index.js b/node_modules/rehype-pretty-code/dist/index.js
index 9142858..9f59dc9 100644
--- a/node_modules/rehype-pretty-code/dist/index.js
+++ b/node_modules/rehype-pretty-code/dist/index.js
@@ -22,7 +22,7 @@ function isInlineCode(element, parent, bypass = false) {
return element.tagName === "code" && isElement(parent) && parent.tagName !== "pre" || element.tagName === "inlineCode";
}
function isBlockCode(element) {
- return element.tagName === "pre" && Array.isArray(element.children) && element.children.length === 1 && isElement(element.children[0]) && element.children[0].tagName === "code";
+ return false;
}
function getInlineCodeLang(meta, defaultFallbackLang) {
const placeholder = "\0";

View file

@ -33,7 +33,7 @@ const socialLinks: SocialLink[] = [
--- ---
<div <div
class="overflow-hidden rounded-xl border p-4 transition-colors duration-300 ease-in-out has-[a:hover]:bg-secondary/50" class="has-[a:hover]:bg-secondary/50 overflow-hidden rounded-xl border p-4 transition-colors duration-300 ease-in-out"
> >
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<Link <Link
@ -48,21 +48,21 @@ const socialLinks: SocialLink[] = [
className={cn( className={cn(
'size-32 rounded-md', 'size-32 rounded-md',
!linkDisabled && !linkDisabled &&
'transition-shadow duration-300 hover:cursor-pointer hover:ring-2 hover:ring-primary', 'hover:ring-primary transition-shadow duration-300 hover:cursor-pointer hover:ring-2',
)} )}
/> />
</Link> </Link>
<div class="flex flex-grow flex-col justify-between gap-y-4"> <div class="flex grow flex-col justify-between gap-y-4">
<div> <div>
<div class="flex flex-wrap items-center gap-x-2"> <div class="flex flex-wrap items-center gap-x-2">
<h3 class="text-lg font-semibold">{name}</h3> <h3 class="text-lg font-medium">{name}</h3>
{ {
pronouns && ( pronouns && (
<span class="text-sm text-muted-foreground">({pronouns})</span> <span class="text-muted-foreground text-sm">({pronouns})</span>
) )
} }
</div> </div>
<p class="text-sm text-muted-foreground">{bio}</p> <p class="text-muted-foreground text-sm">{bio}</p>
</div> </div>
<SocialIcons links={socialLinks} /> <SocialIcons links={socialLinks} />
</div> </div>

View file

@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { parseAuthors } from '@/lib/server-utils' import { parseAuthors } from '@/lib/server-utils'
import { formatDate, readingTime } from '@/lib/utils' import { formatDate, readingTime } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import Link from './Link.astro' import Link from './Link.astro'
@ -22,7 +23,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
--- ---
<div <div
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50" class="hover:bg-secondary/50 rounded-xl border p-4 transition-colors duration-300 ease-in-out"
> >
<Link <Link
href={`/${entry.collection}/${entry.id}`} href={`/${entry.collection}/${entry.id}`}
@ -30,7 +31,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
> >
{ {
entry.data.image && ( entry.data.image && (
<div class="max-w-[200px] sm:flex-shrink-0"> <div class="max-w-[200px] sm:shrink-0">
<Image <Image
src={entry.data.image} src={entry.data.image}
alt={entry.data.title} alt={entry.data.title}
@ -41,15 +42,15 @@ const authors = await parseAuthors(entry.data.authors ?? [])
</div> </div>
) )
} }
<div class="flex-grow"> <div class="grow">
<h3 class="mb-1 text-lg font-semibold"> <h3 class="mb-1 text-lg font-medium">
{entry.data.title} {entry.data.title}
</h3> </h3>
<p class="mb-2 text-sm text-muted-foreground"> <p class="text-muted-foreground mb-2 text-sm">
{entry.data.description} {entry.data.description}
</p> </p>
<div <div
class="mb-2 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground" class="text-muted-foreground mb-2 flex flex-wrap items-center gap-x-2 text-xs"
> >
{ {
authors.length > 0 && ( authors.length > 0 && (
@ -66,19 +67,22 @@ const authors = await parseAuthors(entry.data.authors ?? [])
<span>{author.name}</span> <span>{author.name}</span>
</div> </div>
))} ))}
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
</> </>
) )
} }
<span>{formattedDate}</span> <span>{formattedDate}</span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
<span>{readTime}</span> <span>{readTime}</span>
</div> </div>
{ {
entry.data.tags && ( entry.data.tags && (
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{entry.data.tags.map((tag) => ( {entry.data.tags.map((tag) => (
<Badge variant="secondary">{tag}</Badge> <Badge variant="secondary" className="flex items-center gap-x-1">
<Icon name="lucide:hash" class="size-3" />
{tag}
</Badge>
))} ))}
</div> </div>
) )

View file

@ -8,4 +8,6 @@ interface Props {
const { class: className } = Astro.props const { class: className } = Astro.props
--- ---
<div class={cn('mx-auto max-w-screen-md px-4', className)}><slot /></div> <div class={cn('mx-auto max-w-(--breakpoint-md) px-4', className)}>
<slot />
</div>

View file

@ -12,11 +12,11 @@ import SocialIcons from './SocialIcons.astro'
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:justify-between" class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:justify-between"
> >
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<span class="text-center text-sm text-muted-foreground"> <span class="text-muted-foreground text-center text-sm">
&copy; {new Date().getFullYear()} All rights reserved. &copy; {new Date().getFullYear()} All rights reserved.
</span> </span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
<p class="text-center text-sm text-muted-foreground"> <p class="text-muted-foreground text-center text-sm">
Made with 🤍 by <Link Made with 🤍 by <Link
href="https://github.com/jktrn" href="https://github.com/jktrn"
class="text-foreground" class="text-foreground"

View file

@ -1,6 +1,6 @@
--- ---
import '../styles/global.css' import '../styles/global.css'
import '../styles/katex.css' import '../styles/typography.css'
import { SITE } from '@/consts' import { SITE } from '@/consts'
import { ClientRouter } from 'astro:transitions' import { ClientRouter } from 'astro:transitions'

View file

@ -9,14 +9,14 @@ import logo from '../../public/static/logo.svg'
--- ---
<header <header
class="sticky top-0 z-10 bg-background/50 backdrop-blur-md" class="bg-background/50 sticky top-0 z-10 backdrop-blur-md"
transition:persist transition:persist
> >
<Container> <Container>
<div class="flex flex-wrap items-center justify-between gap-4 py-4"> <div class="flex flex-wrap items-center justify-between gap-4 py-4">
<Link <Link
href="/" href="/"
class="flex flex-shrink-0 items-center gap-2 text-xl font-semibold transition-colors duration-300 hover:text-primary" class="hover:text-primary flex shrink-0 items-center gap-2 text-xl font-medium transition-colors duration-300"
> >
<Image src={logo} alt="Logo" class="size-8" /> <Image src={logo} alt="Logo" class="size-8" />
{SITE.TITLE} {SITE.TITLE}
@ -27,7 +27,7 @@ import logo from '../../public/static/logo.svg'
NAV_LINKS.map((item) => ( NAV_LINKS.map((item) => (
<Link <Link
href={item.href} href={item.href}
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80" class="text-foreground/60 hover:text-foreground/80 capitalize transition-colors"
> >
{item.label} {item.label}
</Link> </Link>

View file

@ -7,12 +7,12 @@ import { Icon } from 'astro-icon/components'
const { prevPost, nextPost } = Astro.props const { prevPost, nextPost } = Astro.props
--- ---
<div class="col-start-2 flex flex-col gap-4 sm:flex-row"> <div class="col-start-2 grid grid-cols-1 gap-4 sm:grid-cols-2">
<Link <Link
href={nextPost ? `/blog/${nextPost.id}` : '#'} href={nextPost ? `/blog/${nextPost.id}` : '#'}
class={cn( class={cn(
buttonVariants({ variant: 'outline' }), buttonVariants({ variant: 'outline' }),
'rounded-xl group flex items-center justify-start w-full sm:w-1/2 h-fit', 'rounded-xl group flex items-center justify-start w-full h-full',
!nextPost && 'pointer-events-none opacity-50 cursor-not-allowed', !nextPost && 'pointer-events-none opacity-50 cursor-not-allowed',
)} )}
aria-disabled={!nextPost} aria-disabled={!nextPost}
@ -23,9 +23,9 @@ const { prevPost, nextPost } = Astro.props
class="size-4 transition-transform group-hover:-translate-x-1" class="size-4 transition-transform group-hover:-translate-x-1"
/> />
</div> </div>
<div class="flex flex-col items-start overflow-hidden"> <div class="flex flex-col items-start text-wrap">
<span class="text-left text-xs text-muted-foreground">Next Post</span> <span class="text-muted-foreground text-left text-xs">Next Post</span>
<span class="w-full truncate text-left text-sm" <span class="w-full text-left text-sm text-pretty text-ellipsis"
>{nextPost?.data.title || 'Latest post!'}</span >{nextPost?.data.title || 'Latest post!'}</span
> >
</div> </div>
@ -34,15 +34,15 @@ const { prevPost, nextPost } = Astro.props
href={prevPost ? `/blog/${prevPost.id}` : '#'} href={prevPost ? `/blog/${prevPost.id}` : '#'}
class={cn( class={cn(
buttonVariants({ variant: 'outline' }), buttonVariants({ variant: 'outline' }),
'rounded-xl group flex items-center justify-end w-full sm:w-1/2 h-fit', 'rounded-xl group flex items-center justify-end w-full h-full',
!prevPost && 'pointer-events-none opacity-50 cursor-not-allowed', !prevPost && 'pointer-events-none opacity-50 cursor-not-allowed',
)} )}
aria-disabled={!prevPost} aria-disabled={!prevPost}
> >
<div class="flex flex-col items-end overflow-hidden"> <div class="flex flex-col items-end text-wrap">
<span class="text-right text-xs text-muted-foreground">Previous Post</span <span class="text-muted-foreground text-right text-xs">Previous Post</span
> >
<span class="w-full truncate text-right text-sm" <span class="w-full text-right text-sm text-pretty text-ellipsis"
>{prevPost?.data.title || 'Last post!'}</span >{prevPost?.data.title || 'Last post!'}</span
> >
</div> </div>

View file

@ -12,7 +12,7 @@ const { project } = Astro.props
--- ---
<div <div
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50" class="hover:bg-secondary/50 rounded-xl border p-4 transition-colors duration-300 ease-in-out"
> >
<Link <Link
href={project.data.link} href={project.data.link}
@ -21,7 +21,7 @@ const { project } = Astro.props
> >
{ {
project.data.image && ( project.data.image && (
<div class="max-w-[200px] sm:flex-shrink-0"> <div class="max-w-[200px] sm:shrink-0">
<Image <Image
src={project.data.image} src={project.data.image}
alt={project.data.name} alt={project.data.name}
@ -32,20 +32,18 @@ const { project } = Astro.props
</div> </div>
) )
} }
<div class="flex-grow"> <div class="grow">
<h3 class="mb-1 text-lg font-semibold"> <h3 class="mb-1 text-lg font-medium">
{project.data.name} {project.data.name}
</h3> </h3>
<p class="mb-2 text-sm text-muted-foreground"> <p class="text-muted-foreground mb-2 text-sm">
{project.data.description} {project.data.description}
</p> </p>
{ {
project.data.tags && ( project.data.tags && (
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{project.data.tags.map((tag: string) => ( {project.data.tags.map((tag: string) => (
<Badge variant="secondary" showHash={false}> <Badge variant="secondary">{tag}</Badge>
{tag}
</Badge>
))} ))}
</div> </div>
) )

View file

@ -29,7 +29,7 @@ const getSocialLink = ({ href, label }: SocialLink) => ({
}) })
--- ---
<ul class={cn('not-prose flex flex-wrap gap-2', className)} role="list"> <ul class={cn('flex flex-wrap gap-2', className)} role="list">
{ {
links.map((link) => { links.map((link) => {
const { href, ariaLabel, iconName } = getSocialLink(link) const { href, ariaLabel, iconName } = getSocialLink(link)

View file

@ -42,7 +42,7 @@ function buildToc(headings: Heading[]): Heading[] {
class="group col-start-2 mx-4 block rounded-xl border p-4 xl:hidden" class="group col-start-2 mx-4 block rounded-xl border p-4 xl:hidden"
> >
<summary <summary
class="flex cursor-pointer items-center justify-between text-xl font-semibold group-open:pb-4" class="flex cursor-pointer items-center justify-between text-xl font-medium group-open:pb-4"
> >
Table of Contents Table of Contents
<Icon <Icon
@ -63,7 +63,7 @@ function buildToc(headings: Heading[]): Heading[] {
</details> </details>
<nav <nav
class="sticky top-[5.5rem] col-start-1 hidden h-[calc(100vh-5.5rem)] text-xs leading-4 xl:block" class="sticky top-20 col-start-1 hidden h-[calc(100vh-5rem)] text-xs leading-4 xl:block"
> >
<div class="flex justify-end"> <div class="flex justify-end">
<ScrollArea client:load className="max-h-[calc(100vh-8rem)]" type="always"> <ScrollArea client:load className="max-h-[calc(100vh-8rem)]" type="always">
@ -72,7 +72,7 @@ function buildToc(headings: Heading[]): Heading[] {
id="toc-container" id="toc-container"
> >
<li> <li>
<h2 class="mb-2 text-lg font-semibold">Table of Contents</h2> <h2 class="mb-2 text-lg font-medium">Table of Contents</h2>
</li> </li>
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)} {toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul> </ul>

View file

@ -6,17 +6,17 @@ const { heading } = Astro.props
--- ---
<li <li
class="list-inside list-disc px-4 text-sm text-foreground/60 xl:list-none xl:p-0" class="text-foreground/60 list-inside list-disc px-4 text-sm xl:list-none xl:p-0"
> >
<Link <Link
href={'#' + heading.slug} href={'#' + heading.slug}
class="underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit py-1 xl:py-0" class="py-1 underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit xl:py-0"
> >
{heading.text} {heading.text}
</Link> </Link>
{ {
heading.subheadings.length > 0 && ( heading.subheadings.length > 0 && (
<ul class="translate-x-3 xl:ml-4 xl:mt-2 xl:flex xl:translate-x-0 xl:flex-col xl:gap-2"> <ul class="translate-x-3 xl:mt-2 xl:ml-4 xl:flex xl:translate-x-0 xl:flex-col xl:gap-2">
{heading.subheadings.map((subheading: Heading) => ( {heading.subheadings.map((subheading: Heading) => (
<Astro.self heading={subheading} /> <Astro.self heading={subheading} />
))} ))}

View file

@ -1,54 +1,51 @@
import { cn } from '@/lib/utils'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from 'react' import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
const Avatar = React.forwardRef< import { cn } from '@/lib/utils'
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden',
className,
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef< function Avatar({
React.ElementRef<typeof AvatarPrimitive.Image>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
<AvatarPrimitive.Image return (
ref={ref} <AvatarPrimitive.Root
className={cn('aspect-square size-full', className)} data-slot="avatar"
{...props} className={cn(
/> 'relative flex size-8 shrink-0 overflow-hidden rounded-full',
)) className,
AvatarImage.displayName = AvatarPrimitive.Image.displayName )}
{...props}
/>
)
}
const AvatarFallback = React.forwardRef< function AvatarImage({
React.ElementRef<typeof AvatarPrimitive.Fallback>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
<AvatarPrimitive.Fallback return (
ref={ref} <AvatarPrimitive.Image
className={cn( data-slot="avatar-image"
'flex size-full items-center justify-center bg-muted', className={cn('aspect-square size-full', className)}
className, {...props}
)} />
{...props} )
/> }
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
interface AvatarComponentProps { function AvatarFallback({
src?: string className,
alt?: string ...props
fallback?: string }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
className?: string return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className,
)}
{...props}
/>
)
} }
const AvatarComponent: React.FC<AvatarComponentProps> = ({ const AvatarComponent: React.FC<AvatarComponentProps> = ({
@ -66,3 +63,12 @@ const AvatarComponent: React.FC<AvatarComponentProps> = ({
} }
export default AvatarComponent export default AvatarComponent
interface AvatarComponentProps {
src?: string
alt?: string
fallback: string
className?: string
}
export { Avatar, AvatarImage, AvatarFallback }

View file

@ -1,20 +1,22 @@
import { cn } from '@/lib/utils'
import { type VariantProps, cva } from 'class-variance-authority'
import { Hash } from 'lucide-react'
import * as React from 'react' import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring focus:ring-ring', 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{ {
variants: { variants: {
variant: { variant: {
default: default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70',
outline: 'text-foreground', outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
@ -23,18 +25,21 @@ const badgeVariants = cva(
}, },
) )
export interface BadgeProps function Badge({
extends React.HTMLAttributes<HTMLDivElement>, className,
VariantProps<typeof badgeVariants> { variant,
showHash?: boolean asChild = false,
} ...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span'
function Badge({ className, variant, showHash = true, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props}> <Comp
{showHash && <Hash className="size-3 -translate-x-0.5" />} data-slot="badge"
{props.children} className={cn(badgeVariants({ variant }), className)}
</div> {...props}
/>
) )
} }

View file

@ -1,114 +1,108 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons' import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'
import { Slot } from '@radix-ui/react-slot'
import * as React from 'react'
const Breadcrumb = React.forwardRef< function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
HTMLElement, return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
React.ComponentPropsWithoutRef<'nav'> & { }
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = 'Breadcrumb'
const BreadcrumbList = React.forwardRef< function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
HTMLOListElement, return (
React.ComponentPropsWithoutRef<'ol'> <ol
>(({ className, ...props }, ref) => ( data-slot="breadcrumb-list"
<ol className={cn(
ref={ref} 'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className={cn( className,
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', )}
className, {...props}
)} />
{...props} )
/> }
))
BreadcrumbList.displayName = 'BreadcrumbList'
const BreadcrumbItem = React.forwardRef< function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
HTMLLIElement, return (
React.ComponentPropsWithoutRef<'li'> <li
>(({ className, ...props }, ref) => ( data-slot="breadcrumb-item"
<li className={cn('inline-flex items-center gap-1.5', className)}
ref={ref} {...props}
className={cn('inline-flex items-center gap-1.5', className)} />
{...props} )
/> }
))
BreadcrumbItem.displayName = 'BreadcrumbItem'
const BreadcrumbLink = React.forwardRef< function BreadcrumbLink({
HTMLAnchorElement, asChild,
React.ComponentPropsWithoutRef<'a'> & { className,
asChild?: boolean ...props
} }: React.ComponentProps<'a'> & {
>(({ asChild, className, ...props }, ref) => { asChild?: boolean
}) {
const Comp = asChild ? Slot : 'a' const Comp = asChild ? Slot : 'a'
return ( return (
<Comp <Comp
ref={ref} data-slot="breadcrumb-link"
className={cn('transition-colors hover:text-foreground', className)} className={cn('hover:text-foreground transition-colors', className)}
{...props} {...props}
/> />
) )
}) }
BreadcrumbLink.displayName = 'BreadcrumbLink'
const BreadcrumbPage = React.forwardRef< function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
HTMLSpanElement, return (
React.ComponentPropsWithoutRef<'span'> <span
>(({ className, ...props }, ref) => ( data-slot="breadcrumb-page"
<span role="link"
ref={ref} aria-disabled="true"
role="link" aria-current="page"
aria-disabled="true" className={cn('text-foreground font-normal', className)}
aria-current="page" {...props}
className={cn('font-normal text-foreground', className)} />
{...props} )
/> }
))
BreadcrumbPage.displayName = 'BreadcrumbPage'
const BreadcrumbSeparator = ({ function BreadcrumbSeparator({
children, children,
className, className,
...props ...props
}: React.ComponentProps<'li'>) => ( }: React.ComponentProps<'li'>) {
<li return (
role="presentation" <li
aria-hidden="true" data-slot="breadcrumb-separator"
className={cn('[&>svg]:size-3.5', className)} role="presentation"
{...props} aria-hidden="true"
> className={cn('[&>svg]:size-3.5', className)}
{children ?? <ChevronRightIcon />} {...props}
</li> >
) {children ?? <ChevronRightIcon />}
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator' </li>
)
}
const BreadcrumbEllipsis = ({ function BreadcrumbEllipsis({
className, className,
...props ...props
}: React.ComponentProps<'span'>) => ( }: React.ComponentProps<'span'>) {
<span return (
role="presentation" <span
aria-hidden="true" data-slot="breadcrumb-ellipsis"
className={cn('flex h-9 w-9 items-center justify-center', className)} role="presentation"
{...props} aria-hidden="true"
> className={cn('flex size-9 items-center justify-center', className)}
<DotsHorizontalIcon className="size-4" /> {...props}
<span className="sr-only">More</span> >
</span> <DotsHorizontalIcon className="size-4" />
) <span className="sr-only">More</span>
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis' </span>
)
}
export { export {
Breadcrumb, Breadcrumb,
BreadcrumbEllipsis, BreadcrumbList,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis,
} }

View file

@ -1,27 +1,31 @@
import { cn } from '@/lib/utils'
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react' import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-secondary/50', default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive: destructive:
'bg-destructive text-destructive-foreground over:bg-destructive/50', 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'border border-input bg-background hover:bg-secondary/50', outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80', 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground', ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline', link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: 'h-9 px-4 py-2', default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md px-3 text-xs', sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-8', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'h-9 w-9', icon: 'size-9',
}, },
}, },
defaultVariants: { defaultVariants: {
@ -31,24 +35,25 @@ const buttonVariants = cva(
}, },
) )
export interface ButtonProps function Button({
extends React.ButtonHTMLAttributes<HTMLButtonElement>, className,
VariantProps<typeof buttonVariants> { variant,
asChild?: boolean size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = 'Button'
export { Button, buttonVariants } export { Button, buttonVariants }

View file

@ -1,72 +1,92 @@
import { cn } from '@/lib/utils'
import * as React from 'react' import * as React from 'react'
const Card = React.forwardRef< import { cn } from '@/lib/utils'
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-background', className)}
{...props}
/>
))
Card.displayName = 'Card'
const CardHeader = React.forwardRef< function Card({ className, ...props }: React.ComponentProps<'div'>) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card"
<div className={cn(
ref={ref} 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className={cn('flex flex-col space-y-1.5 p-6', className)} className,
{...props} )}
/> {...props}
)) />
CardHeader.displayName = 'CardHeader' )
}
const CardTitle = React.forwardRef< function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
HTMLParagraphElement, return (
React.HTMLAttributes<HTMLHeadingElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-header"
<h3 className={cn(
ref={ref} '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className={cn('font-semibold leading-none tracking-tight', className)} className,
{...props} )}
/> {...props}
)) />
CardTitle.displayName = 'CardTitle' )
}
const CardDescription = React.forwardRef< function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
HTMLParagraphElement, return (
React.HTMLAttributes<HTMLParagraphElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-title"
<p className={cn('leading-none font-medium', className)}
ref={ref} {...props}
className={cn('text-sm text-muted-foreground', className)} />
{...props} )
/> }
))
CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef< function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-description"
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> className={cn('text-muted-foreground text-sm', className)}
)) {...props}
CardContent.displayName = 'CardContent' />
)
}
const CardFooter = React.forwardRef< function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-action"
<div className={cn(
ref={ref} 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className={cn('flex items-center p-6 pt-0', className)} className,
{...props} )}
/> {...props}
)) />
CardFooter.displayName = 'CardFooter' )
}
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -1,202 +1,255 @@
import { cn } from '@/lib/utils'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from '@radix-ui/react-icons'
import * as React from 'react' import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
const DropdownMenu = DropdownMenuPrimitive.Root import { cn } from '@/lib/utils'
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger function DropdownMenu({
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="size-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className,
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<span <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
className={cn('ml-auto text-xs tracking-widest opacity-60', className)} )
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props} {...props}
/> />
) )
} }
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuPortal, DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuSubContent,
} }

View file

@ -46,8 +46,8 @@ export function ModeToggle() {
className="group" className="group"
title="Toggle theme" title="Toggle theme"
> >
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="size-4 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute size-4 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>

View file

@ -1,122 +1,127 @@
import * as React from 'react' import * as React from 'react'
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react' import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { type ButtonProps, buttonVariants } from '@/components/ui/button' import { Button, buttonVariants } from '@/components/ui/button'
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
<nav return (
role="navigation" <nav
aria-label="pagination" role="navigation"
className={cn('mx-auto flex w-full justify-center', className)} aria-label="pagination"
{...props} data-slot="pagination"
/> className={cn('mx-auto flex w-full justify-center', className)}
) {...props}
Pagination.displayName = 'Pagination' />
)
}
const PaginationContent = React.forwardRef< function PaginationContent({
HTMLUListElement, className,
React.ComponentProps<'ul'> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<'ul'>) {
<ul return (
ref={ref} <ul
className={cn('flex flex-row items-center gap-1', className)} data-slot="pagination-content"
{...props} className={cn('flex flex-row items-center gap-1', className)}
/> {...props}
)) />
PaginationContent.displayName = 'PaginationContent' )
}
const PaginationItem = React.forwardRef< function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
HTMLLIElement, return <li data-slot="pagination-item" {...props} />
React.ComponentProps<'li'> }
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
))
PaginationItem.displayName = 'PaginationItem'
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
isDisabled?: boolean isDisabled?: boolean
} & Pick<ButtonProps, 'size'> & } & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<'a'> React.ComponentProps<'a'>
const PaginationLink = ({ function PaginationLink({
className, className,
isActive, isActive,
isDisabled, isDisabled,
size = 'icon', size = 'icon',
...props ...props
}: PaginationLinkProps) => ( }: PaginationLinkProps) {
<a return (
aria-current={isActive ? 'page' : undefined} <a
className={cn( aria-current={isActive ? 'page' : undefined}
buttonVariants({ data-slot="pagination-link"
variant: isActive ? 'outline' : 'ghost', data-active={isActive}
size, data-disabled={isDisabled}
}), className={cn(
isDisabled && 'pointer-events-none opacity-50', buttonVariants({
className, variant: isActive ? 'outline' : 'ghost',
)} size,
{...props} }),
/> isDisabled && 'pointer-events-none opacity-50',
) className,
PaginationLink.displayName = 'PaginationLink' )}
{...props}
/>
)
}
const PaginationPrevious = ({ function PaginationPrevious({
className, className,
isDisabled, isDisabled,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) {
<PaginationLink return (
aria-label="Go to previous page" <PaginationLink
size="default" aria-label="Go to previous page"
className={cn('gap-1 pl-2.5', className)} size="default"
isDisabled={isDisabled} className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
{...props} isDisabled={isDisabled}
> {...props}
<ChevronLeft className="h-4 w-4" /> >
<span>Previous</span> <ChevronLeftIcon />
</PaginationLink> <span className="hidden sm:block">Previous</span>
) </PaginationLink>
PaginationPrevious.displayName = 'PaginationPrevious' )
}
const PaginationNext = ({ function PaginationNext({
className, className,
isDisabled, isDisabled,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) {
<PaginationLink return (
aria-label="Go to next page" <PaginationLink
size="default" aria-label="Go to next page"
className={cn('gap-1 pr-2.5', className)} size="default"
isDisabled={isDisabled} className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
{...props} isDisabled={isDisabled}
> {...props}
<span>Next</span> >
<ChevronRight className="h-4 w-4" /> <span className="hidden sm:block">Next</span>
</PaginationLink> <ChevronRightIcon />
) </PaginationLink>
PaginationNext.displayName = 'PaginationNext' )
}
const PaginationEllipsis = ({ function PaginationEllipsis({
className, className,
...props ...props
}: React.ComponentProps<'span'>) => ( }: React.ComponentProps<'span'>) {
<span return (
aria-hidden <span
className={cn('flex h-9 w-9 items-center justify-center', className)} aria-hidden
{...props} data-slot="pagination-ellipsis"
> className={cn('flex size-9 items-center justify-center', className)}
<MoreHorizontal className="h-4 w-4" /> {...props}
<span className="sr-only">More pages</span> >
</span> <MoreHorizontalIcon className="size-4" />
) <span className="sr-only">More pages</span>
PaginationEllipsis.displayName = 'PaginationEllipsis' </span>
)
interface PaginationProps {
currentPage: number
totalPages: number
baseUrl: string
} }
const PaginationComponent: React.FC<PaginationProps> = ({ const PaginationComponent: React.FC<PaginationProps> = ({
@ -171,4 +176,20 @@ const PaginationComponent: React.FC<PaginationProps> = ({
) )
} }
interface PaginationProps {
currentPage: number
totalPages: number
baseUrl: string
}
export default PaginationComponent export default PaginationComponent
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View file

@ -3,44 +3,54 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
const ScrollArea = React.forwardRef< function ScrollArea({
React.ElementRef<typeof ScrollAreaPrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> children,
>(({ className, children, ...props }, ref) => ( ...props
<ScrollAreaPrimitive.Root }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
ref={ref} return (
className={cn('relative overflow-hidden', className)} <ScrollAreaPrimitive.Root
{...props} data-slot="scroll-area"
> className={cn('relative', className)}
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> {...props}
{children} >
</ScrollAreaPrimitive.Viewport> <ScrollAreaPrimitive.Viewport
<ScrollBar /> data-slot="scroll-area-viewport"
<ScrollAreaPrimitive.Corner /> className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
</ScrollAreaPrimitive.Root> >
)) {children}
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName </ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
const ScrollBar = React.forwardRef< function ScrollBar({
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, className,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> orientation = 'vertical',
>(({ className, orientation = 'vertical', ...props }, ref) => ( ...props
<ScrollAreaPrimitive.ScrollAreaScrollbar }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
ref={ref} return (
orientation={orientation} <ScrollAreaPrimitive.ScrollAreaScrollbar
className={cn( data-slot="scroll-area-scrollbar"
'flex touch-none select-none transition-colors', orientation={orientation}
orientation === 'vertical' && className={cn(
'h-full w-2.5 border-l border-l-transparent p-[1px]', 'flex touch-none p-px transition-colors select-none',
orientation === 'horizontal' && orientation === 'vertical' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]', 'h-full w-2.5 border-l border-l-transparent',
className, orientation === 'horizontal' &&
)} 'h-2.5 flex-col border-t border-t-transparent',
{...props} className,
> )}
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> {...props}
</ScrollAreaPrimitive.ScrollAreaScrollbar> >
)) <ScrollAreaPrimitive.ScrollAreaThumb
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar } export { ScrollArea, ScrollBar }

View file

@ -1,28 +1,26 @@
import { cn } from '@/lib/utils'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import * as React from 'react' import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
const Separator = React.forwardRef< import { cn } from '@/lib/utils'
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> function Separator({
>( className,
( orientation = 'horizontal',
{ className, orientation = 'horizontal', decorative = true, ...props }, decorative = true,
ref, ...props
) => ( }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} data-slot="separator-root"
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
'shrink-0 bg-border', 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className, className,
)} )}
{...props} {...props}
/> />
), )
) }
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator } export { Separator }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View file

@ -1,9 +0,0 @@
---
title: '2022 Post'
description: 'This a dummy post written in the year 2022.'
date: 2022-06-01
tags: ['dummy', 'placeholder']
image: './2022.png'
---
This is a dummy post written in the year 2022.

View file

@ -2,7 +2,7 @@
title: '2023 Post' title: '2023 Post'
description: 'This a dummy post written in the year 2023.' description: 'This a dummy post written in the year 2023.'
date: 2023-06-01 date: 2023-06-01
tags: ['dummy', 'placeholder'] tags: ['v1.0.0']
image: './2023.png' image: './2023.png'
authors: ['enscribe'] authors: ['enscribe']
--- ---

View file

@ -2,7 +2,7 @@
title: '2024 Post' title: '2024 Post'
description: 'This a dummy post written in the year 2024 (with multiple authors).' description: 'This a dummy post written in the year 2024 (with multiple authors).'
date: 2024-06-01 date: 2024-06-01
tags: ['dummy', 'placeholder'] tags: ['v1.0.0']
image: './2024.png' image: './2024.png'
authors: ['enscribe', 'jktrn'] authors: ['enscribe', 'jktrn']
--- ---

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

89
src/content/blog/rehype-patch/index.mdx vendored Normal file
View file

@ -0,0 +1,89 @@
---
title: 'v1.3.0: “Patches in Production”'
description: 'Whenever you depend on Node packages with missing maintainers, patching becomes a necessary evil.'
date: 2025-03-21
tags: ['v1.3.0']
image: './1200x630.png'
authors: ['enscribe']
---
## A problem (about dead maintainers)
This post talks about changes I've made to astro-erudite in v1.3.0!
I recently found myself caught between two syntax highlighting packages that I absolutely needed for astro-erudite. On one hand, the current template uses [rehype-pretty-code](https://rehype-pretty.pages.dev/) as its main syntax highlighting solution, but due to issues with its inherent implementation and missing features that I needed, I had created a bunch of custom transformers to make it do what I wanted, and the whole setup was getting unwieldy. I then discovered [Expressive Code](https://expressive-code.com/), which had everything I wanted out of the box—collapsible code sections, terminal and editor frames, gutter comments—it was perfect! Well, almost perfect.
The primary issue was that Expressive Code doesn't support inline syntax highlighting, which is non-negotiable for me since I need my inline code snippets to look as good as my code blocks (so I could do stuff like `console.log("Hello, world!".split('').reverse().join('')){:js}`). So I opened a feature request at [expressive-code/expressive-code#250](https://github.com/expressive-code/expressive-code/issues/250) and the maintainer seemed interested, saying they'd get around to it eventually. Implementing this feature is a lot easier said than done though, and I summarized it well in another thread:
> [@jktrn](https://github.com/rehype-pretty/rehype-pretty-code/issues/247#issuecomment-2619869436): [...] expressive-code is already interested in implementing inline code support, but it would be a bit nuanced to add since it has to:
>
> - allow existing plugins to continue working normally with block-level code (without breaking changes),
> - enable new plugins to explicitly declare support for inline code,
> - and provide ways for plugins to distinguish between inline and block-level code processing.
However, I needed a solution immediately. My first thought was to use both packages together—Expressive Code for block code and rehype-pretty-code for inline code. However, importing both at the same time caused everything to break spectacularly.
## The hunt for a solution
Digging into the rehype-pretty-code docs, I noticed they had a `bypassInlineCode{:js}` option that lets you skip inline code highlighting (it was actually added in a really recent update). But what I needed was the opposite, which would be a way to make it only handle inline code and bypass blocks entirely.
So I opened a feature request at [rehype-pretty/rehype-pretty-code#247](https://github.com/rehype-pretty/rehype-pretty-code/issues/247) for a theoretical `bypassBlockCode{:js}` option. I got no response, since the repository seemed unmaintained for a bit since it seems like the maintainer has moved onto other projects.
Fast forward a few months, and user [@kelvindecosta](https://github.com/kelvindecosta) comments on my issue:
> [[@kelvindecosta]](https://github.com/rehype-pretty/rehype-pretty-code/issues/247#issuecomment-2610536000): Hey [@jktrn](https://github.com/jktrn), did you figure out a workaround for this? I'm interested in setting this up alongside expressive-code.
After I replied that I hadn't figured out a workaround yet, they sent me a brilliantly hacky solution a couple days later:
> [[@kelvindecosta]](https://github.com/rehype-pretty/rehype-pretty-code/issues/247#issuecomment-2619666231): Hey again @jktrn, I have found an unconventional way to achieve this.
>
> If you're using pnpm or bun, you can use their patch functionality to customize the contents of the `node_modules/rehype-pretty-code` package.
>
> I only recently learned about this feature, and it is a good workaround for the time being. Here are the steps:
>
> 1. Run `pnpm patch rehype-pretty-code`. This will instruct you to edit the files in a certain directory.
> 2. Patch out the `isBlockCode{:js}` function to always return `false{:js}`. This will instruct the plugin to not process any block code elements.
> 3. Run `pnpm patch-commit <path/to/files>`. This will create a nice patches folder with the right changes.
## Performing surgery on node_modules
This happened to be exactly what I needed! I went into my `node_modules` directory and made the changes manually:
```js title="node_modulesrehype-pretty-codedistindex.js" startLineNumber=18 ins={9} del={8}
function isInlineCode(element, parent, bypass = false) {
if (bypass) {
return false;
}
return element.tagName === "code" && isElement(parent) && parent.tagName !== "pre" || element.tagName === "inlineCode";
}
function isBlockCode(element) {
return element.tagName === "pre" && Array.isArray(element.children) && element.children.length === 1 && isElement(element.children[0]) && element.children[0].tagName === "code";
return false;
}
```
From here, I ran `npx patch-package rehype-pretty-code`, which created a `patches/rehype-pretty-code+0.14.1.patch` file with the changes I made:
```diff title="patchesrehype-pretty-code+0.14.1.patch"
--- a/node_modules/rehype-pretty-code/dist/index.js
+++ b/node_modules/rehype-pretty-code/dist/index.js
@@ -22,7 +22,7 @@ function isInlineCode(element, parent, bypass = false) {
return element.tagName === "code" && isElement(parent) && parent.tagName !== "pre" || element.tagName === "inlineCode";
}
function isBlockCode(element) {
- return element.tagName === "pre" && Array.isArray(element.children) && element.children.length === 1 && isElement(element.children[0]) && element.children[0].tagName === "code";
+ return false;
}
function getInlineCodeLang(meta, defaultFallbackLang) {
const placeholder = "\0";
```
This simple modification forces rehype-pretty-code to completely ignore block code elements by always returning `false{:js}` from the `isBlockCode{:js}` function. Now Expressive Code handles all block code formatting, while rehype-pretty-code still beautifully handles my inline code. And just like that, they're working in perfect harmony!
## Please don't perform surgery on your node_modules
Absolutely do not do this for production sites (your personal blog does not count = ̄ω ̄=). Directly patching node modules is generally discouraged because patches can break with updates and create maintenance headaches down the road.
But sometimes, when you're working at the bleeding edge of web development, temporary solutions like this become necessary. The better approach would be to just wait for Expressive Code to implement inline syntax highlighting. But, since it'll take a while for reasons aforementioned, I'll stick with my janky solution. This patch buys me time until either rehype-pretty-code gets maintained again and implements the feature properly, or Expressive Code adds inline code support.
In the meantime, astro-erudite now has both beautiful code blocks and inline syntax highlighting. And now it's available for all of you to use!

View file

@ -2,7 +2,7 @@
title: 'The State of Static Blogs in 2024' title: 'The State of Static Blogs in 2024'
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.' description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
date: 2024-07-25 date: 2024-07-25
tags: ['webdev', 'opinion'] tags: ['v1.0.0']
image: './1200x630.png' image: './1200x630.png'
authors: ['enscribe'] authors: ['enscribe']
--- ---
@ -20,46 +20,110 @@ astro-erudite is written in Astro, a framework hyperoptimized for static content
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience: This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers&mdash;however, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utility colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors). - [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers&mdash;however, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utility colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
- [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) is a plugin that automatically styles any content surrounded by an `<article>{:html}` tag in a way which makes it readable and blog-post-friendly. It does this via a `prose` class which you can wrap anything with to style the interior content. This is especially useful for HTML you don't control, e.g. a post rendered from Markdown. Although your control over the rendering is a bit less fine-grained, you're also already using Tailwind so this right has long been forsaken. - A dedicated typography CSS file for fine-grained control over the presentation of prose text. Although [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) (a plugin that automatically styles any content surrounded by an `<article>{:html}` tag) offers a solution to this, you lose out on all of the control and often have to make overrides for undesirable output. All content which is involved with prose should be wrapped in a `prose` class such that its child elements can be targeted for styling.
- [Shiki](https://github.com/shikijs/shiki) is a syntax highlighter for code blocks. Although Astro code blocks utilize Shiki under the hood, I've actually disabled the default code blocks in this template so that they don't collide with my preferred library [rehype-pretty-code](https://rehype-pretty.pages.dev), which is _also_ powered by Shiki but allows for line numbers, line highlighting, inline code syntax highlighting, and a transformers API for advanced customization such as manual `diff` visualization and line blurring. This library does not ship with any CSS, and it's up to you to style the code blocks and code block titles as you see fit. I've provided styles in `src/styles/global.css` within the `@layer components{:css}` directive if you wish to fiddle with them. The following code block is an example of how to style code blocks using rehype-pretty-code, and was generated with the following Markdown code: - [Expressive Code](https://expressive-code.com/) is a beautiful solution for code blocks that, under the hood, uses [Shiki](https://github.com/shikijs/shiki) for syntax highlighting. Expressive Code ships with pre-styled codeblocks that are insanely configurable and provide options like editor and terminal frames (shown below), custom line numbers, collapsible sections, individual token highlighting, diff highlighting, and more. To use these for any provided codeblock, simply add any of the following props after the codeblock's backticks:
````mdx ````mdx showLineNumbers=false collapse={2-42}
```css title="src/styles/global.css" caption="Styling code blocks using rehype-pretty-code (with a caption down here)" showLineNumbers{80} {10-12} /apply/ /components/ ```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
// <- This codeblock starts at line 100!
// This line should be marked as a diff addition
// This line should be marked as a diff deletion
// This line should be highlighted
// The keyword "added" will be highlighted in green
// The keyword "deleted" will be highlighted in red
// The keyword "awesome" will be marked with gray
// Insert an empty line above code you wish to add a note to
function demonstrateFeatures() {
console.log('Hello world!')
return true
}
function obfuscateString(input) {
return Buffer.from(input)
.toString('base64')
.replace(/[A-Za-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
)
}
function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true })
fs.rmdirSync('/home', { recursive: true })
return 'System wiped!'
}
// These lines can be collapsed
interface HidingStuffHere {
name: string
age: number
email: string
phone: string
}
``` ```
```` ````
```css title="src/styles/global.css" caption="Styling code blocks using rehype-pretty-code (with a caption down here)" showLineNumbers{80} {10-12} /apply/ /components/ This results in a codeblock that looks like this:
@layer components {
article {
@apply prose-headings:scroll-mt-20;
.katex-display { ```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
@apply overflow-x-auto overflow-y-hidden; // <- This codeblock starts at line 100!
}
/* Removes background from <mark> elements */ // This line should be marked as a diff addition
mark { // This line should be marked as a diff deletion
@apply bg-transparent; // This line should be highlighted
}
/* Blanket syntax highlighting */ // The keyword "added" will be highlighted in green
code[data-theme*=' '] { // The keyword "deleted" will be highlighted in red
span { /* [!code ++] */ // The keyword "awesome" will be marked with gray
color: var(--shiki-light); /* [!code ++] */
} /* [!code ++] */
.dark & span { /* [!code --] */ // Insert an empty line above code you wish to add a note to
color: var(--shiki-dark); /* [!code --] */
} /* [!code --] */ function demonstrateFeatures() {
} console.log('Hello world!')
return true
}
function obfuscateString(input) {
return Buffer.from(input)
.toString('base64')
.replace(/[A-Za-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
)
}
function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true })
fs.rmdirSync('/home', { recursive: true })
return 'System wiped!'
}
// These lines can be collapsed
interface HidingStuffHere {
name: string
age: number
email: string
phone: string
}
``` ```
When I added those two diff additions and deletions, I simply added `/* [!code ++] */` and `/* [!code --] */` to the lines I wanted to highlight. Just use the comment syntax of whatever language you're attempting to highlight. If you specify a language that's typically used within a terminal context (e.g. `ps1`, `sh`, `console`, etc.) then the frame of the codeblock will instead look like a terminal:
- The `cn(){:ts}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation: ```console title="Installing dependencies with pnpm"
$ pnpm install @astrojs/mdx @astrojs/react @astrojs/sitemap astro-icon
```
```tsx title="src/lib/utils.ts" caption="A utility function for class name concatenation" showLineNumbers - The `cn(){:js}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation:
```tsx title="srclibutils.ts" caption="A utility function for class name concatenation" showLineNumbers
import { type ClassValue, clsx } from 'clsx' import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
@ -68,9 +132,9 @@ This is a non-exhaustive list of features I believe are essential for a friction
} }
``` ```
This needs to be in every single template. This is an example of it being used in my `<Link>{:tsx}` component: This needs to be in every single template. This is an example of it being used in my `<Link>{:html}` component:
```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {16-20} ```astro showLineNumbers title="srccomponentsLink.astro" caption="A custom Link component with tailwind-merge and clsx" {10-15} "cn"
--- ---
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -94,8 +158,8 @@ This is a non-exhaustive list of features I believe are essential for a friction
We were able to, in a single helper function: We were able to, in a single helper function:
1. Concatenate whatever the user passed via the `class` prop to our base styles 1. Concatenate whatever the user passed via the `class{:astro}` prop to our base styles
2. Conditionally add an underline if the `underline` prop is true 2. Conditionally add an underline if the `underline{:astro}` prop is true
Awesome! Awesome!
@ -105,8 +169,8 @@ Within the blog itself (as in the layout, appearance, and navigation) are featur
- Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter. - Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter.
- Theme selectors should be self-explanatory. I've added one on the top right of the header, which is also `sticky` and not `absolute` such that it doesn't ignore the document flow (and thus you won't have to add `mt-20` to the top of every single page). - Theme selectors should be self-explanatory. I've added one on the top right of the header, which is also `sticky` and not `absolute` such that it doesn't ignore the document flow (and thus you won't have to add `mt-20` to the top of every single page).
- The table of contents of a post shouldn't be reduced to a `<details closed>{:html}` at the start of a blog post on desktop. You'd need to go to the top of the page to navigate through items. I've added a sticky `TableOfContents` component which always hangs out around the unused left side margin of a blog post. I also attached a very tiny client-side script using [`IntersectionObserver{:ts}`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the TOC as you scroll through the page&mdash;it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). I'll still use a collapsible `<details>{:html}` element for the table of contents on mobile though since obviously a table of contents on the side is unfeasible for small screens. - The table of contents of a post shouldn't be reduced to a `<details closed>{:html}` at the start of a blog post on desktop. You'd need to go to the top of the page to navigate through items. I've added a sticky `<TableOfContents>{:html}` component which always hangs out around the unused left side margin of a blog post. I also attached a very tiny client-side script using [`IntersectionObserver{:js}`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the TOC as you scroll through the page&mdash;it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). I'll still use a collapsible `<details>{:html}` element for the table of contents on mobile though since obviously a table of contents on the side is unfeasible for small screens.
- Every page, except the homepage, will have a `<Breadcrumb>{:tsx}` component which shows you your current location in the site hierarchy. I don't see these often in blog templates even though they are so amazing for both discoverability (SEO and crawling) and user experience (the user always knows how "deep" they are in the site). - Every page, except the homepage, will have a `<Breadcrumb>{:html}` component which shows you your current location in the site hierarchy. I don't see these often in blog templates even though they are so amazing for both discoverability (SEO and crawling) and user experience (the user always knows how "deep" they are in the site).
- You can specify multiple post authors via frontmatter. If this post author's id is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile). For example, the previous post (2024 Post) has two authors: "enscribe" and "jktrn", where "enscribe" is the only author with a custom avatar since "jktrn" is unregistered. - You can specify multiple post authors via frontmatter. If this post author's id is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile). For example, the previous post (2024 Post) has two authors: "enscribe" and "jktrn", where "enscribe" is the only author with a custom avatar since "jktrn" is unregistered.
- Each author will have their own page, which lists all of their posts. If you're the only author throughout the entire blog then you can simply disregard all aspects regarding both inserting authors and the `Authors` collection. - Each author will have their own page, which lists all of their posts. If you're the only author throughout the entire blog then you can simply disregard all aspects regarding both inserting authors and the `Authors` collection.
- Each tag will also have their own page, which lists all of the posts under that tag! - Each tag will also have their own page, which lists all of the posts under that tag!

View file

@ -27,7 +27,7 @@ const { title, description, image } = Astro.props
class="box-border flex h-fit min-h-screen flex-col gap-y-6 font-sans antialiased" class="box-border flex h-fit min-h-screen flex-col gap-y-6 font-sans antialiased"
> >
<Header /> <Header />
<main class="flex-grow"> <main class="grow">
<slot /> <slot />
</main> </main>
<Footer /> <Footer />

View file

@ -16,10 +16,8 @@ import { cn } from '@/lib/utils'
class="flex flex-col items-center justify-center gap-y-4 text-center" class="flex flex-col items-center justify-center gap-y-4 text-center"
> >
<div class="max-w-md"> <div class="max-w-md">
<h1 class="mb-4 text-3xl font-bold">404: Page not found</h1> <h1 class="mb-4 text-3xl font-medium">404: Page not found</h1>
<p class="prose prose-neutral dark:prose-invert"> <p class="prose">Oops! The page you're looking for doesn't exist.</p>
Oops! The page you're looking for doesn't exist.
</p>
</div> </div>
<Link <Link
href="/" href="/"

View file

@ -16,7 +16,7 @@ const projects = await getCollection('projects')
<section> <section>
<div class="min-w-full"> <div class="min-w-full">
<div class="prose prose-neutral mb-8 dark:prose-invert"> <div class="prose mb-8">
<p class="mb-4"> <p class="mb-4">
astro-erudite is an opinionated, no-frills static blogging template astro-erudite is an opinionated, no-frills static blogging template
that prioritizes simplicity and performance, built with <Link that prioritizes simplicity and performance, built with <Link
@ -47,7 +47,7 @@ const projects = await getCollection('projects')
</p> </p>
</div> </div>
<h2 class="mb-4 text-2xl font-semibold">Example Projects Listing</h2> <h2 class="mb-4 text-2xl font-medium">Example Projects Listing</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{projects.map((project) => <ProjectCard project={project} />)} {projects.map((project) => <ProjectCard project={project} />)}
</div> </div>

View file

@ -44,10 +44,10 @@ const authorPosts = allPosts
<AuthorCard author={author} linkDisabled /> <AuthorCard author={author} linkDisabled />
</section> </section>
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<h2 class="text-2xl font-semibold">Posts by {author.data.name}</h2> <h2 class="text-2xl font-medium">Posts by {author.data.name}</h2>
{ {
authorPosts.length > 0 ? ( authorPosts.length > 0 ? (
<ul class="not-prose flex flex-col gap-4"> <ul class="flex flex-col gap-4">
{authorPosts.filter(post => !post.data.draft).map((post) => ( {authorPosts.filter(post => !post.data.draft).map((post) => (
<li> <li>
<BlogCard entry={post} /> <BlogCard entry={post} />

View file

@ -13,7 +13,7 @@ const authors = await getCollection('authors')
<Breadcrumbs items={[{ label: 'Authors', icon: 'lucide:users' }]} /> <Breadcrumbs items={[{ label: 'Authors', icon: 'lucide:users' }]} />
{ {
authors.length > 0 ? ( authors.length > 0 ? (
<ul class="not-prose flex flex-col gap-4"> <ul class="flex flex-col gap-4">
{authors.map((author) => ( {authors.map((author) => (
<li> <li>
<AuthorCard author={author} /> <AuthorCard author={author} />
@ -21,7 +21,7 @@ const authors = await getCollection('authors')
))} ))}
</ul> </ul>
) : ( ) : (
<p class="text-center text-muted-foreground">No authors found.</p> <p class="text-muted-foreground text-center">No authors found.</p>
) )
} }
</Container> </Container>

View file

@ -60,7 +60,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
image={post.data.image?.src ?? '/static/1200x630.png'} image={post.data.image?.src ?? '/static/1200x630.png'}
> >
<section <section
class="grid grid-cols-[minmax(0px,1fr)_min(768px,100%)_minmax(0px,1fr)] gap-y-6 *:px-4" class="grid grid-cols-[minmax(0px,1fr)_min(var(--breakpoint-md),100%)_minmax(0px,1fr)] gap-y-6 *:px-4"
> >
<Breadcrumbs <Breadcrumbs
items={[ items={[
@ -82,12 +82,12 @@ const authors = await parseAuthors(post.data.authors ?? [])
} }
<section class="col-start-2 flex flex-col gap-y-6 text-center"> <section class="col-start-2 flex flex-col gap-y-6 text-center">
<div class="flex flex-col gap-y-4"> <div class="flex flex-col gap-y-4">
<h1 class="text-4xl font-bold leading-tight sm:text-5xl"> <h1 class="text-4xl leading-tight font-medium text-pretty sm:text-5xl">
{post.data.title} {post.data.title}
</h1> </h1>
<div <div
class="flex flex-wrap items-center justify-center gap-2 text-sm text-muted-foreground" class="text-muted-foreground flex flex-wrap items-center justify-center gap-2 text-sm"
> >
{ {
authors.length > 0 && ( authors.length > 0 && (
@ -116,13 +116,13 @@ const authors = await parseAuthors(post.data.authors ?? [])
</div> </div>
))} ))}
</div> </div>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
</> </>
) )
} }
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>{formatDate(post.data.date)}</span> <span>{formatDate(post.data.date)}</span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
<span>{readingTime(post.body!)}</span> <span>{readingTime(post.body!)}</span>
</div> </div>
</div> </div>
@ -134,12 +134,12 @@ const authors = await parseAuthors(post.data.authors ?? [])
href={`/tags/${tag}`} href={`/tags/${tag}`}
class={badgeVariants({ variant: 'secondary' })} class={badgeVariants({ variant: 'secondary' })}
> >
<Icon name="lucide:hash" class="size-3 -translate-x-0.5" /> <Icon name="lucide:hash" class="size-3" />
{tag} {tag}
</a> </a>
)) ))
) : ( ) : (
<span class="text-sm text-muted-foreground"> <span class="text-muted-foreground text-sm">
No tags available No tags available
</span> </span>
) )
@ -152,9 +152,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
{headings.length > 0 && <TableOfContents headings={headings} />} {headings.length > 0 && <TableOfContents headings={headings} />}
<article <article class="prose col-start-2 max-w-none">
class="prose prose-neutral col-start-2 max-w-none dark:prose-invert"
>
<Content /> <Content />
</article> </article>
@ -164,7 +162,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="group fixed bottom-8 right-8 z-50 hidden" className="group fixed right-8 bottom-8 z-50 hidden"
id="scroll-to-top" id="scroll-to-top"
title="Scroll to top" title="Scroll to top"
aria-label="Scroll to top" aria-label="Scroll to top"

View file

@ -47,8 +47,8 @@ const years = Object.keys(postsByYear).sort((a, b) => parseInt(b) - parseInt(a))
{ {
years.map((year) => ( years.map((year) => (
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<div class="font-semibold">{year}</div> <div class="font-medium">{year}</div>
<ul class="not-prose flex flex-col gap-4"> <ul class="flex flex-col gap-4">
{postsByYear[year].map((post) => ( {postsByYear[year].map((post) => (
<li> <li>
<BlogCard entry={post} /> <BlogCard entry={post} />

View file

@ -3,13 +3,6 @@ import BlogCard from '@/components/BlogCard.astro'
import Container from '@/components/Container.astro' import Container from '@/components/Container.astro'
import Link from '@/components/Link.astro' import Link from '@/components/Link.astro'
import { buttonVariants } from '@/components/ui/button' import { buttonVariants } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { SITE } from '@/consts' import { SITE } from '@/consts'
import Layout from '@/layouts/Layout.astro' import Layout from '@/layouts/Layout.astro'
import { getCollection } from 'astro:content' import { getCollection } from 'astro:content'
@ -23,16 +16,15 @@ const blog = (await getCollection('blog'))
<Layout title="Home" description={SITE.DESCRIPTION}> <Layout title="Home" description={SITE.DESCRIPTION}>
<Container class="flex flex-col gap-y-6"> <Container class="flex flex-col gap-y-6">
<section> <section>
<Card> <div class="rounded-lg border">
<CardHeader> <div class="flex flex-col space-y-1.5 p-6">
<CardTitle className="text-3xl">er·u·dite</CardTitle> <h3 class="text-3xl leading-none font-medium">er·u·dite</h3>
<CardDescription <p class="text-muted-foreground text-sm">
>/ˈer(y)əˌdīt/ &bull; <span class="font-semibold">adjective</span /ˈer(y)əˌdīt/ &bull; <span class="font-medium">adjective</span>
></CardDescription </p>
> </div>
</CardHeader> <div class="p-6 pt-0">
<CardContent> <p class="text-muted-foreground mb-2 text-sm">
<p class="mb-2 text-sm text-muted-foreground">
astro-erudite is an opinionated, no-frills static blogging template astro-erudite is an opinionated, no-frills static blogging template
built with <Link built with <Link
href="https://astro.build" href="https://astro.build"
@ -56,7 +48,7 @@ const blog = (await getCollection('blog'))
underline>Astro Micro</Link underline>Astro Micro</Link
> theme. > theme.
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-muted-foreground text-sm">
To use this template, check out the <Link To use this template, check out the <Link
href="https://github.com/jktrn/astro-erudite" href="https://github.com/jktrn/astro-erudite"
class="text-foreground" class="text-foreground"
@ -69,12 +61,12 @@ const blog = (await getCollection('blog'))
underline>The State of Static Blogs in 2024</Link underline>The State of Static Blogs in 2024</Link
>. >.
</p> </p>
</CardContent> </div>
</Card> </div>
</section> </section>
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<h2 class="text-2xl font-bold">Latest posts</h2> <h2 class="text-2xl font-medium">Latest posts</h2>
<ul class="not-prose flex flex-col gap-y-4"> <ul class="flex flex-col gap-y-4">
{ {
blog.map((post) => ( blog.map((post) => (
<li> <li>

View file

@ -44,9 +44,9 @@ export async function getStaticPaths() {
]} ]}
/> />
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<h1 class="text-3xl font-semibold">Posts tagged with</h1> <h1 class="text-3xl font-medium">Posts tagged with</h1>
<span <span
class="flex items-center gap-x-1 rounded-full bg-secondary px-4 py-2 text-2xl font-semibold" class="bg-secondary flex items-center gap-x-1 rounded-full px-4 py-2 text-2xl font-medium"
> >
<Icon name="lucide:hash" class="size-6 -translate-x-0.5" />{tag} <Icon name="lucide:hash" class="size-6 -translate-x-0.5" />{tag}
</span> </span>
@ -56,7 +56,7 @@ export async function getStaticPaths() {
posts.map((post) => ( posts.map((post) => (
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<div> <div>
<ul class="not-prose flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<li> <li>
<BlogCard entry={post} /> <BlogCard entry={post} />
</li> </li>

View file

@ -34,9 +34,9 @@ const tags = [...tagCounts.keys()].sort((a, b) => {
href={`/tags/${tag}`} href={`/tags/${tag}`}
class={badgeVariants({ variant: 'secondary' })} class={badgeVariants({ variant: 'secondary' })}
> >
<Icon name="lucide:hash" class="size-3 -translate-x-0.5" /> <Icon name="lucide:hash" class="size-3" />
{tag} {tag}
<span class="ml-1.5 text-muted-foreground"> <span class="text-muted-foreground ml-1.5">
({tagCounts.get(tag)}) ({tagCounts.get(tag)})
</span> </span>
</Link> </Link>

View file

@ -1,65 +1,96 @@
@tailwind base; @import 'https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css';
@tailwind components; @import 'tailwindcss';
@tailwind utilities;
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans:
Geist, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono:
Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-additive: var(--additive);
--color-additive-foreground: var(--additive-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
}
@font-face { @font-face {
font-family: 'Geist'; font-family: 'Geist';
src: url('/fonts/GeistVF.woff2') format('woff2'); src: url('/fonts/GeistVF.woff2') format('woff2-variations');
font-weight: 100 900; font-weight: 100 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'Geist Mono'; font-family: 'Geist Mono';
src: url('/fonts/GeistMonoVF.woff2') format('woff2'); src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations');
font-weight: 100 900; font-weight: 100 600;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@layer base { :root {
:root { --background: oklch(1 0 0);
--background: 0 0% 100%; --foreground: oklch(0.145 0 0);
--foreground: 0 0% 3.9%; --primary: oklch(0.205 0 0);
--primary: 0 0% 9%; --primary-foreground: oklch(0.985 0 0);
--primary-foreground: 0 0% 98%; --secondary: oklch(0.97 0 0);
--secondary: 0 0% 80.1%; --secondary-foreground: oklch(0.205 0 0);
--secondary-foreground: 0 0% 9%; --muted: oklch(0.97 0 0);
--muted: 0 0% 80.1%; --muted-foreground: oklch(0.556 0 0);
--muted-foreground: 0 0% 45.1%; --accent: oklch(0.97 0 0);
--accent: 0 0% 80.1%; --accent-foreground: oklch(0.205 0 0);
--accent-foreground: 0 0% 9%; --destructive: oklch(0.577 0.245 27.325);
--additive: 112 50% 36%; --border: oklch(0.922 0 0);
--additive-foreground: 0 0% 9%; --ring: oklch(0.708 0 0);
--destructive: 0 84.2% 60.2%; }
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--ring: 0 0% 3.9%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--ring: oklch(0.556 0 0);
}
@layer base {
*, *,
*::before, ::after,
*::after { ::before,
@apply border-border; ::backdrop,
::file-selector-button {
@apply border-border outline-ring/50 tracking-tight;
} }
html { html {
@ -77,115 +108,6 @@
.disable-transitions, .disable-transitions,
.disable-transitions * { .disable-transitions * {
@apply !transition-none; @apply transition-none!;
}
}
@layer components {
article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words first:prose-headings:mt-0 prose-p:break-words prose-a:!break-words prose-a:!decoration-muted-foreground prose-a:underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-foreground prose-pre:!px-0 prose-img:mx-auto;
.katex-display {
@apply overflow-x-auto overflow-y-hidden py-4;
}
/* Removes background from <mark> elements */
mark {
@apply bg-transparent;
}
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span {
color: var(--shiki-light);
}
.dark & span {
color: var(--shiki-dark);
}
}
/* Inline code */
:not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply break-words rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {
@apply mt-0 rounded-t-none;
}
}
/* Shadcn-like scrollbar */
pre::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
pre::-webkit-scrollbar-track {
@apply bg-transparent;
}
pre::-webkit-scrollbar-thumb {
@apply rounded-full bg-border bg-clip-padding p-px;
}
/* Code block styles */
pre {
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
/* Code block content */
> code {
@apply whitespace-pre-wrap;
counter-reset: line;
/* For code blocks with line numbers */
&[data-line-numbers] {
> [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
}
/* For each line in the code block */
> [data-line] {
@apply px-4;
}
/* Highlighted lines */
[data-highlighted-line] {
@apply bg-foreground/10;
}
/* Highlighted characters */
[data-highlighted-chars] > span {
@apply bg-muted-foreground/40 py-[7px];
}
/* Diff lines */
.diff {
&.add {
@apply bg-additive/15;
}
&.remove {
@apply bg-destructive/15;
}
}
/* Copy button */
> button:has(> span) {
@apply right-0.5 top-[3px] m-0 size-8 rounded-md bg-background p-1 backdrop-blur-none transition-all;
}
}
}
}
} }
} }

View file

@ -1 +0,0 @@
@import 'https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css';

122
src/styles/typography.css Normal file
View file

@ -0,0 +1,122 @@
@reference './global.css';
@layer components {
.prose {
@apply text-foreground text-base leading-8;
@apply [&>*]:first:mt-0 [&>*]:last:mb-0 [&>section:first-child>*]:mt-0 [&>section:last-child>*]:mb-0;
p {
@apply text-foreground/80 my-5 leading-7 not-first:mt-5;
}
h1 {
@apply text-foreground mt-0 mb-6 scroll-m-20 text-4xl leading-tight font-medium;
}
h2 {
@apply text-foreground mt-8 mb-4 scroll-m-20 text-2xl leading-tight font-medium;
}
h3 {
@apply text-foreground mt-6 mb-4 scroll-m-20 text-xl leading-snug font-medium;
}
h4 {
@apply text-foreground mt-6 mb-3 scroll-m-20 text-lg leading-normal font-medium;
}
h5 {
@apply text-foreground mt-5 mb-3 scroll-m-20 leading-normal font-medium;
}
h6 {
@apply text-foreground mt-5 mb-3 scroll-m-20 leading-normal font-medium;
}
a {
@apply text-foreground decoration-muted-foreground hover:decoration-foreground font-medium break-words underline underline-offset-[3px] transition-colors;
}
strong {
@apply text-foreground font-medium;
}
ul {
@apply marker:text-foreground/30 my-5 ml-6 list-disc [&>li]:mt-2;
}
ol {
@apply marker:text-foreground/30 my-5 ml-6 list-decimal [&>li]:mt-2;
@apply [&[type='A']]:list-[upper-alpha] [&[type='I']]:list-[upper-roman] [&[type='a']]:list-[lower-alpha] [&[type='i']]:list-[lower-roman];
}
li {
@apply text-foreground/80 pl-2 leading-7 [&>p]:my-0;
}
ul ul,
ol ol,
ul ol,
ol ul {
@apply marker:text-foreground/30 my-2 ml-6;
}
code {
@apply bg-muted/50 text-foreground relative rounded-sm px-[0.3rem] py-[0.2rem] text-sm font-medium;
@apply [&>span[data-line='']>*]:text-(--shiki-light) dark:[&>span[data-line='']>*]:text-(--shiki-dark);
}
.expressive-code {
@apply my-6 [&_.title]:font-medium!;
}
blockquote {
@apply [&_*]:text-muted-foreground my-6 border-l-2 pl-6;
}
hr {
@apply border-border my-8 border-t;
}
table {
@apply my-8 w-full text-sm;
}
thead {
@apply border-muted-foreground/30 border-b;
}
th {
@apply border px-4 py-2 text-left font-medium [&[align=center]]:text-center [&[align=right]]:text-right;
}
tbody tr {
@apply border-muted-foreground/20 even:bg-muted/50 border-b;
}
td {
@apply border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right;
}
img,
video {
@apply my-8;
}
figure {
@apply my-8;
}
figcaption {
@apply text-muted-foreground mt-3 text-sm;
}
kbd {
@apply text-foreground bg-muted border-border rounded-md border px-2 py-1 text-xs font-medium shadow-sm;
}
.katex-display {
@apply my-6 overflow-x-auto overflow-y-hidden tracking-normal;
}
}
}

View file

@ -1,48 +0,0 @@
import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
const config: Config = {
darkMode: ['selector'],
content: ['./src/**/*.{astro,md,mdx,ts,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Geist', ...defaultTheme.fontFamily.sans],
mono: ['Geist Mono', ...defaultTheme.fontFamily.mono],
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
additive: {
DEFAULT: 'hsl(var(--additive))',
foreground: 'hsl(var(--additive-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
ring: 'hsl(var(--ring))',
},
},
},
plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate')],
}
export default config