feat: tailwind v4, expressive-code migration (#12)
This commit is contained in:
commit
6cac15f53b
48 changed files with 8725 additions and 8843 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -1,27 +1,13 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
||||
teaser.pptx
|
||||
~$teaser.pptx
|
107
README.md
107
README.md
|
@ -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!
|
||||
|
||||
| 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) |
|
||||
| [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) |
|
||||
| [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) |
|
||||
| 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) |
|
||||
| [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) |
|
||||
| [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) |
|
||||
|
||||
## 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.
|
||||
- [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/).
|
||||
- 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.
|
||||
|
@ -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:
|
||||
|
||||
| Category | Technology Name |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| Framework | [Astro](https://astro.build/) |
|
||||
| Styling | [Tailwind](https://tailwindcss.com) |
|
||||
| Components | [shadcn/ui](https://ui.shadcn.com/) |
|
||||
| Content | [MDX](https://mdxjs.com/) |
|
||||
| Syntax Highlighting | [Shiki](https://github.com/shikijs/shiki) + [rehype-pretty-code](https://rehype-pretty.pages.dev/) |
|
||||
| Graphics | [Figma](https://www.figma.com/) |
|
||||
| Deployment | [Vercel](https://vercel.com) |
|
||||
| Category | Technology Name |
|
||||
| ---------- | ------------------------------------------------------------------------------------------ |
|
||||
| Framework | [Astro](https://astro.build/) |
|
||||
| Styling | [Tailwind](https://tailwindcss.com) |
|
||||
| Components | [shadcn/ui](https://ui.shadcn.com/) |
|
||||
| Content | [MDX](https://mdxjs.com/) |
|
||||
| Codeblocks | [Expressive Code](https://expressive-code.com/), [Shiki](https://github.com/shikijs/shiki) |
|
||||
| Graphics | [Figma](https://www.figma.com/) |
|
||||
| Deployment | [Vercel](https://vercel.com) |
|
||||
|
||||
## 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:
|
||||
|
||||
```css
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 80.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 80.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 80.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--additive: 112 50% 36%; /* Unique to astro-erudite */
|
||||
--additive-foreground: 0 0% 98%; /* Unique to astro-erudite */
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
}
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* ... */
|
||||
}
|
||||
.dark {
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
@ -169,15 +168,15 @@ draft: false
|
|||
|
||||
The blog post schema is defined as follows:
|
||||
|
||||
| Field | Type (Zod) | Requirements | Required |
|
||||
| ------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| `title` | `string` | Must be ≤60 characters. | Yes |
|
||||
| `description` | `string` | Must be ≤155 characters. | Yes |
|
||||
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
|
||||
| `image` | `image()` | Must be exactly 1200px × 630px. | Optional |
|
||||
| `tags` | `string[]` | Preferably use kebab-case for these. | Optional |
|
||||
| Field | Type (Zod) | Requirements | Required |
|
||||
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| `title` | `string` | Must be ≤60 characters. | Yes |
|
||||
| `description` | `string` | Must be ≤155 characters. | Yes |
|
||||
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
|
||||
| `image` | `image()` | Should be exactly 1200px × 630px. | 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 |
|
||||
| `draft` | `boolean` | Defaults to `false` if not provided. | Optional |
|
||||
| `draft` | `boolean` | Defaults to `false` if not provided. | Optional |
|
||||
|
||||
### Authors
|
||||
|
||||
|
@ -230,13 +229,13 @@ link: 'https://example.com'
|
|||
|
||||
The project schema is defined as follows:
|
||||
|
||||
| Field | Type (Zod) | Requirements | Required |
|
||||
| ------------- | -------------- | ------------------------------------- | -------- |
|
||||
| `name` | `string` | n/a | Yes |
|
||||
| `description` | `string` | n/a | Yes |
|
||||
| `tags` | `string[]` | n/a | Yes |
|
||||
| `image` | `image()` | Must be exactly 1200px × 630px. | Yes |
|
||||
| `link` | `string.url()` | Must be a valid URL. | Yes |
|
||||
| Field | Type (Zod) | Requirements | Required |
|
||||
| ------------- | -------------- | --------------------------------------- | -------- |
|
||||
| `name` | `string` | n/a | Yes |
|
||||
| `description` | `string` | n/a | Yes |
|
||||
| `tags` | `string[]` | n/a | Yes |
|
||||
| `image` | `image()` | Should be exactly 1200px × 630px. | Yes |
|
||||
| `link` | `string.url()` | Must be a valid URL. | Yes |
|
||||
|
||||
## License
|
||||
|
||||
|
|
112
astro.config.ts
112
astro.config.ts
|
@ -1,36 +1,85 @@
|
|||
import { rehypeHeadingIds } from '@astrojs/markdown-remark'
|
||||
import { defineConfig } from 'astro/config'
|
||||
|
||||
import mdx from '@astrojs/mdx'
|
||||
import react from '@astrojs/react'
|
||||
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'
|
||||
|
||||
// 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({
|
||||
site: 'https://astro-erudite.vercel.app',
|
||||
integrations: [
|
||||
tailwind({
|
||||
applyBaseStyles: false,
|
||||
expressiveCode({
|
||||
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(),
|
||||
react(),
|
||||
sitemap(),
|
||||
icon(),
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
server: {
|
||||
port: 1234,
|
||||
host: true,
|
||||
},
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
},
|
||||
markdown: {
|
||||
syntaxHighlight: false,
|
||||
rehypePlugins: [
|
||||
|
@ -43,32 +92,17 @@ export default defineConfig({
|
|||
],
|
||||
rehypeHeadingIds,
|
||||
rehypeKatex,
|
||||
sectionize,
|
||||
// sectionize,
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: {
|
||||
light: 'github-light-high-contrast',
|
||||
dark: 'github-dark-high-contrast',
|
||||
light: 'github-light',
|
||||
dark: 'github-dark',
|
||||
},
|
||||
transformers: [
|
||||
transformerNotationDiff(),
|
||||
transformerMetaHighlight(),
|
||||
transformerCopyButton({
|
||||
visibility: 'hover',
|
||||
feedbackDuration: 1000,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
remarkPlugins: [remarkToc, remarkMath, remarkEmoji],
|
||||
},
|
||||
server: {
|
||||
port: 1234,
|
||||
host: true,
|
||||
},
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
remarkPlugins: [remarkToc, remarkMath, remarkEmoji, remarkSectionize],
|
||||
},
|
||||
})
|
||||
|
|
15179
package-lock.json
generated
15179
package-lock.json
generated
File diff suppressed because it is too large
Load diff
60
package.json
60
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "astro-erudite",
|
||||
"type": "module",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
|
@ -9,53 +9,55 @@
|
|||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"prettier": "prettier --write ."
|
||||
"prettier": "prettier --write .",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/markdown-remark": "^6.1.0",
|
||||
"@astrojs/mdx": "^4.0.8",
|
||||
"@astrojs/react": "^4.2.0",
|
||||
"@astrojs/markdown-remark": "^6.3.1",
|
||||
"@astrojs/mdx": "^4.2.1",
|
||||
"@astrojs/react": "^4.2.1",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^6.0.0",
|
||||
"@hbsnow/rehype-sectionize": "^1.0.7",
|
||||
"@iconify-json/lucide": "^1.2.20",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@astrojs/sitemap": "^3.3.0",
|
||||
"@expressive-code/plugin-collapsible-sections": "^0.40.2",
|
||||
"@expressive-code/plugin-line-numbers": "^0.40.2",
|
||||
"@iconify-json/lucide": "^1.2.26",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@rehype-pretty/transformers": "^0.13.2",
|
||||
"@shikijs/transformers": "^1.24.4",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"astro": "^5.3.0",
|
||||
"astro-icon": "^1.1.4",
|
||||
"@tailwindcss/vite": "^4.0.7",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"astro": "^5.5.4",
|
||||
"astro-expressive-code": "^0.40.2",
|
||||
"astro-icon": "^1.1.5",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-pretty-code": "^0.14.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"remark-emoji": "^5.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-sectionize": "^2.1.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.7.2"
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier": "^3.5.1",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-astro-organize-imports": "^0.4.11",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9"
|
||||
"prettier-plugin-tailwindcss": "^0.6.11"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
|
|
13
patches/rehype-pretty-code+0.14.1.patch
Normal file
13
patches/rehype-pretty-code+0.14.1.patch
Normal 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";
|
|
@ -33,7 +33,7 @@ const socialLinks: SocialLink[] = [
|
|||
---
|
||||
|
||||
<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">
|
||||
<Link
|
||||
|
@ -48,21 +48,21 @@ const socialLinks: SocialLink[] = [
|
|||
className={cn(
|
||||
'size-32 rounded-md',
|
||||
!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>
|
||||
<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 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 && (
|
||||
<span class="text-sm text-muted-foreground">({pronouns})</span>
|
||||
<span class="text-muted-foreground text-sm">({pronouns})</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{bio}</p>
|
||||
<p class="text-muted-foreground text-sm">{bio}</p>
|
||||
</div>
|
||||
<SocialIcons links={socialLinks} />
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge'
|
|||
import { Separator } from '@/components/ui/separator'
|
||||
import { parseAuthors } from '@/lib/server-utils'
|
||||
import { formatDate, readingTime } from '@/lib/utils'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { Image } from 'astro:assets'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import Link from './Link.astro'
|
||||
|
@ -22,7 +23,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
|
|||
---
|
||||
|
||||
<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
|
||||
href={`/${entry.collection}/${entry.id}`}
|
||||
|
@ -30,7 +31,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
|
|||
>
|
||||
{
|
||||
entry.data.image && (
|
||||
<div class="max-w-[200px] sm:flex-shrink-0">
|
||||
<div class="max-w-[200px] sm:shrink-0">
|
||||
<Image
|
||||
src={entry.data.image}
|
||||
alt={entry.data.title}
|
||||
|
@ -41,15 +42,15 @@ const authors = await parseAuthors(entry.data.authors ?? [])
|
|||
</div>
|
||||
)
|
||||
}
|
||||
<div class="flex-grow">
|
||||
<h3 class="mb-1 text-lg font-semibold">
|
||||
<div class="grow">
|
||||
<h3 class="mb-1 text-lg font-medium">
|
||||
{entry.data.title}
|
||||
</h3>
|
||||
<p class="mb-2 text-sm text-muted-foreground">
|
||||
<p class="text-muted-foreground mb-2 text-sm">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
<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 && (
|
||||
|
@ -66,19 +67,22 @@ const authors = await parseAuthors(entry.data.authors ?? [])
|
|||
<span>{author.name}</span>
|
||||
</div>
|
||||
))}
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<span>{formattedDate}</span>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<span>{readTime}</span>
|
||||
</div>
|
||||
{
|
||||
entry.data.tags && (
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{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>
|
||||
)
|
||||
|
|
|
@ -8,4 +8,6 @@ interface 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>
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<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">
|
||||
© {new Date().getFullYear()} All rights reserved.
|
||||
</span>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<p class="text-muted-foreground text-center text-sm">
|
||||
Made with 🤍 by <Link
|
||||
href="https://github.com/jktrn"
|
||||
class="text-foreground"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import '../styles/global.css'
|
||||
import '../styles/katex.css'
|
||||
import '../styles/typography.css'
|
||||
|
||||
import { SITE } from '@/consts'
|
||||
import { ClientRouter } from 'astro:transitions'
|
||||
|
|
|
@ -9,14 +9,14 @@ import logo from '../../public/static/logo.svg'
|
|||
---
|
||||
|
||||
<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
|
||||
>
|
||||
<Container>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 py-4">
|
||||
<Link
|
||||
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" />
|
||||
{SITE.TITLE}
|
||||
|
@ -27,7 +27,7 @@ import logo from '../../public/static/logo.svg'
|
|||
NAV_LINKS.map((item) => (
|
||||
<Link
|
||||
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}
|
||||
</Link>
|
||||
|
|
|
@ -7,12 +7,12 @@ import { Icon } from 'astro-icon/components'
|
|||
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
|
||||
href={nextPost ? `/blog/${nextPost.id}` : '#'}
|
||||
class={cn(
|
||||
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',
|
||||
)}
|
||||
aria-disabled={!nextPost}
|
||||
|
@ -23,9 +23,9 @@ const { prevPost, nextPost } = Astro.props
|
|||
class="size-4 transition-transform group-hover:-translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-start overflow-hidden">
|
||||
<span class="text-left text-xs text-muted-foreground">Next Post</span>
|
||||
<span class="w-full truncate text-left text-sm"
|
||||
<div class="flex flex-col items-start text-wrap">
|
||||
<span class="text-muted-foreground text-left text-xs">Next Post</span>
|
||||
<span class="w-full text-left text-sm text-pretty text-ellipsis"
|
||||
>{nextPost?.data.title || 'Latest post!'}</span
|
||||
>
|
||||
</div>
|
||||
|
@ -34,15 +34,15 @@ const { prevPost, nextPost } = Astro.props
|
|||
href={prevPost ? `/blog/${prevPost.id}` : '#'}
|
||||
class={cn(
|
||||
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',
|
||||
)}
|
||||
aria-disabled={!prevPost}
|
||||
>
|
||||
<div class="flex flex-col items-end overflow-hidden">
|
||||
<span class="text-right text-xs text-muted-foreground">Previous Post</span
|
||||
<div class="flex flex-col items-end text-wrap">
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ const { project } = Astro.props
|
|||
---
|
||||
|
||||
<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
|
||||
href={project.data.link}
|
||||
|
@ -21,7 +21,7 @@ const { project } = Astro.props
|
|||
>
|
||||
{
|
||||
project.data.image && (
|
||||
<div class="max-w-[200px] sm:flex-shrink-0">
|
||||
<div class="max-w-[200px] sm:shrink-0">
|
||||
<Image
|
||||
src={project.data.image}
|
||||
alt={project.data.name}
|
||||
|
@ -32,20 +32,18 @@ const { project } = Astro.props
|
|||
</div>
|
||||
)
|
||||
}
|
||||
<div class="flex-grow">
|
||||
<h3 class="mb-1 text-lg font-semibold">
|
||||
<div class="grow">
|
||||
<h3 class="mb-1 text-lg font-medium">
|
||||
{project.data.name}
|
||||
</h3>
|
||||
<p class="mb-2 text-sm text-muted-foreground">
|
||||
<p class="text-muted-foreground mb-2 text-sm">
|
||||
{project.data.description}
|
||||
</p>
|
||||
{
|
||||
project.data.tags && (
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{project.data.tags.map((tag: string) => (
|
||||
<Badge variant="secondary" showHash={false}>
|
||||
{tag}
|
||||
</Badge>
|
||||
<Badge variant="secondary">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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) => {
|
||||
const { href, ariaLabel, iconName } = getSocialLink(link)
|
||||
|
|
|
@ -42,7 +42,7 @@ function buildToc(headings: Heading[]): Heading[] {
|
|||
class="group col-start-2 mx-4 block rounded-xl border p-4 xl:hidden"
|
||||
>
|
||||
<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
|
||||
<Icon
|
||||
|
@ -63,7 +63,7 @@ function buildToc(headings: Heading[]): Heading[] {
|
|||
</details>
|
||||
|
||||
<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">
|
||||
<ScrollArea client:load className="max-h-[calc(100vh-8rem)]" type="always">
|
||||
|
@ -72,7 +72,7 @@ function buildToc(headings: Heading[]): Heading[] {
|
|||
id="toc-container"
|
||||
>
|
||||
<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>
|
||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||
</ul>
|
||||
|
|
|
@ -6,17 +6,17 @@ const { heading } = Astro.props
|
|||
---
|
||||
|
||||
<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
|
||||
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}
|
||||
</Link>
|
||||
{
|
||||
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) => (
|
||||
<Astro.self heading={subheading} />
|
||||
))}
|
||||
|
|
|
@ -1,54 +1,51 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import * as React from 'react'
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
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
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex size-full items-center justify-center bg-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface AvatarComponentProps {
|
||||
src?: string
|
||||
alt?: string
|
||||
fallback?: string
|
||||
className?: string
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
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> = ({
|
||||
|
@ -66,3 +63,12 @@ const AvatarComponent: React.FC<AvatarComponentProps> = ({
|
|||
}
|
||||
|
||||
export default AvatarComponent
|
||||
|
||||
interface AvatarComponentProps {
|
||||
src?: string
|
||||
alt?: string
|
||||
fallback: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
|
|
|
@ -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 { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
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:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
'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 [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
@ -23,18 +25,21 @@ const badgeVariants = cva(
|
|||
},
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {
|
||||
showHash?: boolean
|
||||
}
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
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 (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props}>
|
||||
{showHash && <Hash className="size-3 -translate-x-0.5" />}
|
||||
{props.children}
|
||||
</div>
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,114 +1,108 @@
|
|||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import * as React from 'react'
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<'nav'> & {
|
||||
separator?: React.ReactNode
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||
Breadcrumb.displayName = 'Breadcrumb'
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<'ol'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbList.displayName = 'BreadcrumbList'
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbItem.displayName = 'BreadcrumbItem'
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<'a'> & {
|
||||
asChild?: boolean
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'a'> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'a'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn('transition-colors hover:text-foreground', className)}
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn('hover:text-foreground transition-colors', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
BreadcrumbLink.displayName = 'BreadcrumbLink'
|
||||
}
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<'span'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn('font-normal text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbPage.displayName = 'BreadcrumbPage'
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn('text-foreground font-normal', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'li'>) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('[&>svg]:size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'
|
||||
}: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('[&>svg]:size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis'
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
|
|
|
@ -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 { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-secondary/50',
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground over:bg-destructive/50',
|
||||
outline: 'border border-input bg-background hover:bg-secondary/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 bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
@ -31,24 +35,25 @@ const buttonVariants = cva(
|
|||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
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 }
|
||||
|
|
|
@ -1,72 +1,92 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import * as React from 'react'
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('rounded-xl border bg-background', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = 'Card'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@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,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-medium', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = 'CardContent'
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -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 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
|
||||
|
||||
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,
|
||||
function DropdownMenu({
|
||||
...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 (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@ export function ModeToggle() {
|
|||
className="group"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<Sun className="size-4 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute size-4 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
|
@ -1,122 +1,127 @@
|
|||
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 { type ButtonProps, buttonVariants } from '@/components/ui/button'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = 'Pagination'
|
||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<'ul'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = 'PaginationContent'
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn('', className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = 'PaginationItem'
|
||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
isDisabled?: boolean
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||
React.ComponentProps<'a'>
|
||||
|
||||
const PaginationLink = ({
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
isDisabled,
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
isDisabled && 'pointer-events-none opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = 'PaginationLink'
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
data-disabled={isDisabled}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
isDisabled && 'pointer-events-none opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PaginationPrevious = ({
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
isDisabled,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = 'PaginationPrevious'
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
const PaginationNext = ({
|
||||
function PaginationNext({
|
||||
className,
|
||||
isDisabled,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = 'PaginationNext'
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis'
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
baseUrl: string
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
|
|
|
@ -3,44 +3,54 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
|||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
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"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
import * as React from 'react'
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 90 KiB |
9
src/content/blog/2022-post/index.mdx
vendored
9
src/content/blog/2022-post/index.mdx
vendored
|
@ -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.
|
2
src/content/blog/2023-post/index.mdx
vendored
2
src/content/blog/2023-post/index.mdx
vendored
|
@ -2,7 +2,7 @@
|
|||
title: '2023 Post'
|
||||
description: 'This a dummy post written in the year 2023.'
|
||||
date: 2023-06-01
|
||||
tags: ['dummy', 'placeholder']
|
||||
tags: ['v1.0.0']
|
||||
image: './2023.png'
|
||||
authors: ['enscribe']
|
||||
---
|
||||
|
|
2
src/content/blog/2024-post/index.mdx
vendored
2
src/content/blog/2024-post/index.mdx
vendored
|
@ -2,7 +2,7 @@
|
|||
title: '2024 Post'
|
||||
description: 'This a dummy post written in the year 2024 (with multiple authors).'
|
||||
date: 2024-06-01
|
||||
tags: ['dummy', 'placeholder']
|
||||
tags: ['v1.0.0']
|
||||
image: './2024.png'
|
||||
authors: ['enscribe', 'jktrn']
|
||||
---
|
||||
|
|
BIN
src/content/blog/rehype-patch/1200x630.png
Normal file
BIN
src/content/blog/rehype-patch/1200x630.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
89
src/content/blog/rehype-patch/index.mdx
vendored
Normal file
89
src/content/blog/rehype-patch/index.mdx
vendored
Normal 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_modules⠀›⠀rehype-pretty-code⠀›⠀dist⠀›⠀index.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="patches⠀›⠀rehype-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!
|
132
src/content/blog/the-state-of-static-blogs/index.mdx
vendored
132
src/content/blog/the-state-of-static-blogs/index.mdx
vendored
|
@ -2,7 +2,7 @@
|
|||
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.'
|
||||
date: 2024-07-25
|
||||
tags: ['webdev', 'opinion']
|
||||
tags: ['v1.0.0']
|
||||
image: './1200x630.png'
|
||||
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:
|
||||
|
||||
- [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—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.
|
||||
- [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:
|
||||
- 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.
|
||||
- [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
|
||||
```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/
|
||||
````mdx showLineNumbers=false collapse={2-42}
|
||||
```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/
|
||||
@layer components {
|
||||
article {
|
||||
@apply prose-headings:scroll-mt-20;
|
||||
This results in a codeblock that looks like this:
|
||||
|
||||
.katex-display {
|
||||
@apply overflow-x-auto overflow-y-hidden;
|
||||
}
|
||||
```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!
|
||||
|
||||
/* Removes background from <mark> elements */
|
||||
mark {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
// This line should be marked as a diff addition
|
||||
// This line should be marked as a diff deletion
|
||||
// This line should be highlighted
|
||||
|
||||
/* Blanket syntax highlighting */
|
||||
code[data-theme*=' '] {
|
||||
span { /* [!code ++] */
|
||||
color: var(--shiki-light); /* [!code ++] */
|
||||
} /* [!code ++] */
|
||||
// The keyword "added" will be highlighted in green
|
||||
// The keyword "deleted" will be highlighted in red
|
||||
// The keyword "awesome" will be marked with gray
|
||||
|
||||
.dark & span { /* [!code --] */
|
||||
color: var(--shiki-dark); /* [!code --] */
|
||||
} /* [!code --] */
|
||||
}
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
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="src⠀›⠀lib⠀›⠀utils.ts" caption="A utility function for class name concatenation" showLineNumbers
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
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="src⠀›⠀components⠀›⠀Link.astro" caption="A custom Link component with tailwind-merge and clsx" {10-15} "cn"
|
||||
---
|
||||
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:
|
||||
|
||||
1. Concatenate whatever the user passed via the `class` prop to our base styles
|
||||
2. Conditionally add an underline if the `underline` prop is true
|
||||
1. Concatenate whatever the user passed via the `class{:astro}` prop to our base styles
|
||||
2. Conditionally add an underline if the `underline{:astro}` prop is true
|
||||
|
||||
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.
|
||||
- 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—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).
|
||||
- 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—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>{: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.
|
||||
- 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!
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<Header />
|
||||
<main class="flex-grow">
|
||||
<main class="grow">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
|
|
|
@ -16,10 +16,8 @@ import { cn } from '@/lib/utils'
|
|||
class="flex flex-col items-center justify-center gap-y-4 text-center"
|
||||
>
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-4 text-3xl font-bold">404: Page not found</h1>
|
||||
<p class="prose prose-neutral dark:prose-invert">
|
||||
Oops! The page you're looking for doesn't exist.
|
||||
</p>
|
||||
<h1 class="mb-4 text-3xl font-medium">404: Page not found</h1>
|
||||
<p class="prose">Oops! The page you're looking for doesn't exist.</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
|
|
|
@ -16,7 +16,7 @@ const projects = await getCollection('projects')
|
|||
|
||||
<section>
|
||||
<div class="min-w-full">
|
||||
<div class="prose prose-neutral mb-8 dark:prose-invert">
|
||||
<div class="prose mb-8">
|
||||
<p class="mb-4">
|
||||
astro-erudite is an opinionated, no-frills static blogging template
|
||||
that prioritizes simplicity and performance, built with <Link
|
||||
|
@ -47,7 +47,7 @@ const projects = await getCollection('projects')
|
|||
</p>
|
||||
</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">
|
||||
{projects.map((project) => <ProjectCard project={project} />)}
|
||||
</div>
|
||||
|
|
|
@ -44,10 +44,10 @@ const authorPosts = allPosts
|
|||
<AuthorCard author={author} linkDisabled />
|
||||
</section>
|
||||
<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 ? (
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
<ul class="flex flex-col gap-4">
|
||||
{authorPosts.filter(post => !post.data.draft).map((post) => (
|
||||
<li>
|
||||
<BlogCard entry={post} />
|
||||
|
|
|
@ -13,7 +13,7 @@ const authors = await getCollection('authors')
|
|||
<Breadcrumbs items={[{ label: 'Authors', icon: 'lucide:users' }]} />
|
||||
{
|
||||
authors.length > 0 ? (
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
<ul class="flex flex-col gap-4">
|
||||
{authors.map((author) => (
|
||||
<li>
|
||||
<AuthorCard author={author} />
|
||||
|
@ -21,7 +21,7 @@ const authors = await getCollection('authors')
|
|||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="text-center text-muted-foreground">No authors found.</p>
|
||||
<p class="text-muted-foreground text-center">No authors found.</p>
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
|
|
|
@ -60,7 +60,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
image={post.data.image?.src ?? '/static/1200x630.png'}
|
||||
>
|
||||
<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
|
||||
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">
|
||||
<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}
|
||||
</h1>
|
||||
|
||||
<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 && (
|
||||
|
@ -116,13 +116,13 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{formatDate(post.data.date)}</span>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<span>{readingTime(post.body!)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -134,12 +134,12 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
href={`/tags/${tag}`}
|
||||
class={badgeVariants({ variant: 'secondary' })}
|
||||
>
|
||||
<Icon name="lucide:hash" class="size-3 -translate-x-0.5" />
|
||||
<Icon name="lucide:hash" class="size-3" />
|
||||
{tag}
|
||||
</a>
|
||||
))
|
||||
) : (
|
||||
<span class="text-sm text-muted-foreground">
|
||||
<span class="text-muted-foreground text-sm">
|
||||
No tags available
|
||||
</span>
|
||||
)
|
||||
|
@ -152,9 +152,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
|
||||
{headings.length > 0 && <TableOfContents headings={headings} />}
|
||||
|
||||
<article
|
||||
class="prose prose-neutral col-start-2 max-w-none dark:prose-invert"
|
||||
>
|
||||
<article class="prose col-start-2 max-w-none">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
|
@ -164,7 +162,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
<Button
|
||||
variant="outline"
|
||||
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"
|
||||
title="Scroll to top"
|
||||
aria-label="Scroll to top"
|
||||
|
|
|
@ -47,8 +47,8 @@ const years = Object.keys(postsByYear).sort((a, b) => parseInt(b) - parseInt(a))
|
|||
{
|
||||
years.map((year) => (
|
||||
<section class="flex flex-col gap-y-4">
|
||||
<div class="font-semibold">{year}</div>
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
<div class="font-medium">{year}</div>
|
||||
<ul class="flex flex-col gap-4">
|
||||
{postsByYear[year].map((post) => (
|
||||
<li>
|
||||
<BlogCard entry={post} />
|
||||
|
|
|
@ -3,13 +3,6 @@ import BlogCard from '@/components/BlogCard.astro'
|
|||
import Container from '@/components/Container.astro'
|
||||
import Link from '@/components/Link.astro'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { SITE } from '@/consts'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getCollection } from 'astro:content'
|
||||
|
@ -23,16 +16,15 @@ const blog = (await getCollection('blog'))
|
|||
<Layout title="Home" description={SITE.DESCRIPTION}>
|
||||
<Container class="flex flex-col gap-y-6">
|
||||
<section>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">er·u·dite</CardTitle>
|
||||
<CardDescription
|
||||
>/ˈer(y)əˌdīt/ • <span class="font-semibold">adjective</span
|
||||
></CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p class="mb-2 text-sm text-muted-foreground">
|
||||
<div class="rounded-lg border">
|
||||
<div class="flex flex-col space-y-1.5 p-6">
|
||||
<h3 class="text-3xl leading-none font-medium">er·u·dite</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
/ˈer(y)əˌdīt/ • <span class="font-medium">adjective</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 pt-0">
|
||||
<p class="text-muted-foreground mb-2 text-sm">
|
||||
astro-erudite is an opinionated, no-frills static blogging template
|
||||
built with <Link
|
||||
href="https://astro.build"
|
||||
|
@ -56,7 +48,7 @@ const blog = (await getCollection('blog'))
|
|||
underline>Astro Micro</Link
|
||||
> theme.
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
To use this template, check out the <Link
|
||||
href="https://github.com/jktrn/astro-erudite"
|
||||
class="text-foreground"
|
||||
|
@ -69,12 +61,12 @@ const blog = (await getCollection('blog'))
|
|||
underline>The State of Static Blogs in 2024</Link
|
||||
>.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex flex-col gap-y-4">
|
||||
<h2 class="text-2xl font-bold">Latest posts</h2>
|
||||
<ul class="not-prose flex flex-col gap-y-4">
|
||||
<h2 class="text-2xl font-medium">Latest posts</h2>
|
||||
<ul class="flex flex-col gap-y-4">
|
||||
{
|
||||
blog.map((post) => (
|
||||
<li>
|
||||
|
|
|
@ -44,9 +44,9 @@ export async function getStaticPaths() {
|
|||
]}
|
||||
/>
|
||||
<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
|
||||
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}
|
||||
</span>
|
||||
|
@ -56,7 +56,7 @@ export async function getStaticPaths() {
|
|||
posts.map((post) => (
|
||||
<section class="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
<ul class="flex flex-col gap-4">
|
||||
<li>
|
||||
<BlogCard entry={post} />
|
||||
</li>
|
||||
|
|
|
@ -34,9 +34,9 @@ const tags = [...tagCounts.keys()].sort((a, b) => {
|
|||
href={`/tags/${tag}`}
|
||||
class={badgeVariants({ variant: 'secondary' })}
|
||||
>
|
||||
<Icon name="lucide:hash" class="size-3 -translate-x-0.5" />
|
||||
<Icon name="lucide:hash" class="size-3" />
|
||||
{tag}
|
||||
<span class="ml-1.5 text-muted-foreground">
|
||||
<span class="text-muted-foreground ml-1.5">
|
||||
({tagCounts.get(tag)})
|
||||
</span>
|
||||
</Link>
|
||||
|
|
|
@ -1,65 +1,96 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css';
|
||||
@import 'tailwindcss';
|
||||
|
||||
@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-family: 'Geist';
|
||||
src: url('/fonts/GeistVF.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
src: url('/fonts/GeistVF.woff2') format('woff2-variations');
|
||||
font-weight: 100 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('/fonts/GeistMonoVF.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations');
|
||||
font-weight: 100 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 80.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 80.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 80.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--additive: 112 50% 36%;
|
||||
--additive-foreground: 0 0% 9%;
|
||||
--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%;
|
||||
}
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@apply border-border;
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
@apply border-border outline-ring/50 tracking-tight;
|
||||
}
|
||||
|
||||
html {
|
||||
|
@ -77,115 +108,6 @@
|
|||
|
||||
.disable-transitions,
|
||||
.disable-transitions * {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@apply transition-none!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import 'https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css';
|
122
src/styles/typography.css
Normal file
122
src/styles/typography.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Add table
Reference in a new issue