├── .editorconfig ├── .github └── workflows │ ├── build.yml │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.mjs ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets └── preview.webp ├── astro.config.mjs ├── e2e ├── ports.test.ts └── themeSwitcher.test.ts ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── public ├── CNAME ├── embed.png ├── favicon.png ├── humans.txt ├── maintainers │ ├── 256x256 │ │ └── placeholder.webp │ └── 64x64 │ │ └── placeholder.webp ├── pronunciation.mp3 └── robots.txt ├── renovate.json ├── src ├── components │ ├── PageIntro.astro │ ├── Pills.svelte │ ├── ProfilePicture.svelte │ ├── ThemeSelect.svelte │ └── ThemeSwitcher.astro ├── content.config.ts ├── data │ ├── blog │ │ ├── celebrating-three-years-of-catppuccin.mdx │ │ ├── celebrating-three-years-of-catppuccin.png │ │ ├── state-of-catppuccin-2024.mdx │ │ ├── why-we-ditched-the-icon-for-external-links.mdx │ │ └── why-we-ditched-the-icon-for-external-links.png │ ├── governance.ts │ ├── icons.json │ ├── icons.ts │ ├── icons │ │ ├── logo-text.svg │ │ ├── logo.svg │ │ ├── lucide-user-round-x.svg │ │ ├── magnifying-glass.svg │ │ ├── org │ │ │ ├── core.svg │ │ │ ├── moderator.svg │ │ │ ├── staff.svg │ │ │ └── userstyles-staff.svg │ │ ├── ports │ │ │ ├── cursors.svg │ │ │ ├── folders.svg │ │ │ ├── gboard.svg │ │ │ ├── gitui.svg │ │ │ ├── hexchat.svg │ │ │ ├── lxqt.svg │ │ │ ├── minecraft.svg │ │ │ ├── qutebrowser.svg │ │ │ ├── windows-files.svg │ │ │ ├── zathura.svg │ │ │ └── zellij.svg │ │ └── speaker-high-volume.svg │ ├── links.ts │ ├── ports.ts │ ├── propertyBasedSet.ts │ └── scripts │ │ ├── convertIconsToJson.ts │ │ └── fetchMaintainerAvatars.ts ├── layouts │ ├── Default.astro │ ├── Landing.astro │ ├── Skeleton.astro │ └── components │ │ ├── AccentBar.astro │ │ ├── Footer.astro │ │ ├── Head.astro │ │ └── Navigation.astro ├── pages │ ├── 404 │ │ └── index.astro │ ├── _components │ │ └── LaptopIllustration.astro │ ├── blog │ │ ├── [id].astro │ │ ├── _components │ │ │ └── ArticleCard.astro │ │ ├── _logic │ │ │ ├── [id].test.ts │ │ │ └── [id].ts │ │ └── index.astro │ ├── community │ │ ├── _components │ │ │ ├── TeamName.astro │ │ │ └── UserCard.astro │ │ └── index.astro │ ├── index.astro │ ├── licensing │ │ └── index.astro │ ├── palette │ │ ├── _components │ │ │ ├── CopyToClipboardButton.svelte │ │ │ └── FlavorName.astro │ │ └── index.astro │ ├── ports │ │ ├── _components │ │ │ ├── PortCard.svelte │ │ │ ├── PortExplorer.svelte │ │ │ ├── PortGrid.svelte │ │ │ ├── PortMaintainers.svelte │ │ │ ├── SearchBar.svelte │ │ │ └── state.svelte.ts │ │ └── index.astro │ └── rss.xml.js └── styles │ ├── _buttons.scss │ ├── _palette.scss │ ├── _scaffolding.scss │ ├── _tables.scss │ ├── _typography.scss │ ├── _utils.scss │ └── global.scss ├── svelte.config.js ├── tsconfig.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # EditorConfig is awesome: https://EditorConfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_size = 2 10 | indent_style = space 11 | max_line_length = 120 12 | end_of_line = lf 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | 16 | # documentation, utils 17 | [*.{md,mdx,diff}] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Site 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, closed] 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.repository == 'catppuccin/website' }} 12 | permissions: 13 | pull-requests: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: withastro/action@v4 17 | - name: Build & Deploy To Surge 18 | if: ${{ env.surge_token != '' }} 19 | uses: afc163/surge-preview@a17bbee72e236f18b248797bbf21f1f9f06900e6 20 | id: preview_step 21 | with: 22 | surge_token: ${{ secrets.SURGE_TOKEN }} 23 | dist: dist 24 | build: echo "Website Already Built" 25 | teardown: "true" 26 | failOnError: "true" 27 | env: 28 | surge_token: ${{ secrets.SURGE_TOKEN }} 29 | - name: Output Preview URL 30 | if: ${{ env.surge_token != '' }} 31 | run: echo "url => ${{ steps.preview_step.outputs.preview_url }}" 32 | env: 33 | surge_token: ${{ secrets.SURGE_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Site 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: [main] 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: withastro/action@v4 21 | env: 22 | CATPPUCCIN_PROD: true 23 | 24 | deploy: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | steps: 31 | - name: Deploy to GitHub Pages 32 | id: deployment 33 | uses: actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: lts/* 19 | cache: pnpm 20 | - run: pnpm i --frozen-lockfile 21 | - run: pnpm dlx playwright install --with-deps 22 | - run: pnpm run maintainers 23 | - run: pnpm run test:e2e 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | coverage/ 4 | 5 | # generated types 6 | .astro/ 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | # logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # environment variables 18 | .env 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | 24 | # GitHub avatars 25 | src/data/maintainers 26 | public/maintainers/64x64/* 27 | public/maintainers/256x256/* 28 | !public/maintainers/64x64/placeholder.webp 29 | !public/maintainers/256x256/placeholder.webp 30 | 31 | # Playwright 32 | /test-results/ 33 | /playwright-report/ 34 | /blob-report/ 35 | /playwright/.cache/ 36 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm check && pnpm lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Content Collections 2 | **/*.mdx 3 | 4 | # Package Managers 5 | package-lock.json 6 | pnpm-lock.yaml 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | // .prettierrc.mjs 2 | /** @type {import("prettier").Config} */ 3 | export default { 4 | printWidth: 120, 5 | bracketSameLine: true, 6 | plugins: ["prettier-plugin-astro"], 7 | overrides: [ 8 | { 9 | files: "*.astro", 10 | options: { 11 | parser: "astro", 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[svelte]": { 10 | "editor.defaultFormatter": "svelte.svelte-vscode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Catppuccin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this website and associated documentation files (the "Website"), to deal in the 7 | Website without restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 9 | Website, and to permit persons to whom the Website is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Website. 14 | 15 | The Website is provided "as is", without warranty of any kind, express or 16 | implied, including but not limited to the warranties of merchantability, fitness 17 | for a particular purpose and noninfringement. In no event shall the authors or 18 | copyright holders be liable for any claim, damages or other liability, whether 19 | in an action of contract, tort or otherwise, araising from, out of or in 20 | connection with the Website or the use or other dealings in the Website. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo
3 | 4 | Catppuccin for World Wide Web 5 | 6 |

7 | 8 |

9 | 10 | 11 | 12 |

13 | 14 |

15 | 16 |

17 | 18 | ## Usage 19 | 20 | 1. Open your browser of choice 21 | 2. Put cursor in address bar 22 | 3. Type [catppuccin.com](https://catppuccin.com) 23 | 4. Press enter :D 24 | 25 | ## Development 26 | 27 | ``` 28 | pnpm install 29 | pnpm dev 30 | ``` 31 | 32 |   33 | 34 |

35 | 36 |

37 |

Copyright © 2021-present Catppuccin Org 38 |

39 | 40 |

41 | -------------------------------------------------------------------------------- /assets/preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/assets/preview.webp -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import sitemap from "@astrojs/sitemap"; 3 | import svelte from "@astrojs/svelte"; 4 | import mdx from "@astrojs/mdx"; 5 | import icon from "astro-icon"; 6 | import { rehypeHeadingIds } from "@astrojs/markdown-remark"; 7 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 8 | import yaml from "@rollup/plugin-yaml"; 9 | import astroExpressiveCode from "astro-expressive-code"; 10 | import getReadingTime from "reading-time"; 11 | import { toString } from "mdast-util-to-string"; 12 | 13 | const remarkReadingTime = () => { 14 | return function (tree, { data }) { 15 | const textOnPage = toString(tree); 16 | const readingTime = getReadingTime(textOnPage); 17 | data.astro.frontmatter.minutesRead = readingTime.text; 18 | }; 19 | }; 20 | 21 | // https://astro.build/config 22 | export default defineConfig({ 23 | site: "https://catppuccin.com", 24 | vite: { 25 | plugins: [yaml()], 26 | }, 27 | markdown: { 28 | rehypePlugins: [ 29 | rehypeHeadingIds, 30 | [ 31 | rehypeAutolinkHeadings, 32 | { 33 | behavior: "wrap", 34 | headingProperties: { 35 | className: ["rehype-heading"], 36 | }, 37 | properties: { 38 | className: ["rehype-heading-link"], 39 | }, 40 | }, 41 | ], 42 | ], 43 | }, 44 | integrations: [ 45 | astroExpressiveCode({ 46 | themes: ["catppuccin-latte", "catppuccin-mocha", "catppuccin-frappe", "catppuccin-macchiato"], 47 | themeCssSelector: (theme) => { 48 | const themeName = theme.name.split("-")[1]; 49 | const selector = `[data-theme='${themeName}']`; 50 | return selector; 51 | }, 52 | useDarkModeMediaQuery: true, 53 | // Stop it from auto-correcting colour contrast 54 | minSyntaxHighlightingColorContrast: 0, 55 | styleOverrides: { 56 | frames: { 57 | tooltipSuccessBackground: "var(--green)", 58 | tooltipSuccessForeground: "var(--base)", 59 | }, 60 | textMarkers: { 61 | insBackground: "color-mix(in oklab, var(--green) 25%, var(--mantle));", 62 | insBorderColor: "var(--surface0)", 63 | delBackground: "color-mix(in oklab, var(--red) 25%, var(--mantle));", 64 | delBorderColor: "var(--surface0)", 65 | }, 66 | codePaddingInline: "var(--space-md)", 67 | uiFontSize: "1.5rem", 68 | codeFontSize: "1.4rem", 69 | codeBackground: "var(--mantle)", 70 | }, 71 | }), 72 | sitemap(), 73 | icon({ 74 | iconDir: "src/data/icons", 75 | }), 76 | svelte(), 77 | mdx({ 78 | remarkPlugins: [remarkReadingTime], 79 | }), 80 | ], 81 | }); 82 | -------------------------------------------------------------------------------- /e2e/ports.test.ts: -------------------------------------------------------------------------------- 1 | import { categories, ports } from "@data/ports"; 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test("search filter works", async ({ page }) => { 5 | await page.goto("/ports"); 6 | 7 | await page.getByRole("textbox").fill("tailwindcss"); 8 | await page.waitForTimeout(100); 9 | 10 | expect(await page.locator(".port-card").count()).toBe(1); 11 | expect(new URL(page.url()).searchParams.get("q")).toBe("tailwindcss"); 12 | }); 13 | 14 | test("platform filter works", async ({ page }) => { 15 | await page.goto("/ports"); 16 | 17 | await page.getByLabel("Linux").evaluate((element) => (element as HTMLElement).click()); 18 | 19 | expect(await page.locator(".port-card").count()).toBe(ports.filter((port) => port.platform.includes("linux")).length); 20 | expect(new URL(page.url()).searchParams.get("p")).toBe("linux"); 21 | }); 22 | 23 | test("category filter works", async ({ page }) => { 24 | await page.goto("/ports"); 25 | 26 | await page.getByLabel("Userstyles").evaluate((element) => (element as HTMLElement).click()); 27 | 28 | expect(await page.locator(".port-card").count()).toBe(categories.find((cat) => cat.key === "userstyle")?.portCount); 29 | expect(new URL(page.url()).searchParams.get("c")).toBe("userstyle"); 30 | 31 | // Deselect the category on another click 32 | await page.getByLabel("Userstyles").evaluate((element) => (element as HTMLElement).click()); 33 | 34 | expect(await page.locator(".port-card").count()).toBe(ports.length); 35 | }); 36 | 37 | test("ports and userstyles are differentiated", async ({ page }) => { 38 | await page.goto("/ports"); 39 | 40 | await page.getByRole("textbox").fill("mdbook"); 41 | await page.waitForTimeout(200); 42 | 43 | expect(await page.locator(".port-card").count()).toBe(2); 44 | expect(new URL(page.url()).searchParams.get("q")).toBe("mdbook"); 45 | const portNames = page.locator(".port-name"); 46 | expect(portNames.nth(0)).toContainText(/mdBook \(userstyle\)/i); 47 | expect(portNames.nth(1)).toContainText(/mdBook/i); 48 | }); 49 | -------------------------------------------------------------------------------- /e2e/themeSwitcher.test.ts: -------------------------------------------------------------------------------- 1 | import { flavors } from "@catppuccin/palette"; 2 | import { test, expect, type Page } from "@playwright/test"; 3 | 4 | test.use({ colorScheme: "light" }); 5 | 6 | const assertCssVariable = async (page: Page, actual: string, expected: string) => { 7 | const cssVar = await page.evaluate( 8 | (varName) => getComputedStyle(document.documentElement).getPropertyValue(varName).trim(), 9 | actual, 10 | ); 11 | expect(cssVar).toBe(expected); 12 | }; 13 | 14 | test("changing theme works", async ({ page }) => { 15 | await page.goto("/"); 16 | await assertCssVariable(page, "--base", flavors.latte.colors.base.hex); 17 | expect(await page.evaluate(() => localStorage.getItem("theme"))).toBe(null); 18 | const themeSwitcher = page.locator("#themeSelector"); 19 | await expect(themeSwitcher).toBeVisible(); 20 | expect(await themeSwitcher.inputValue()).toBe("system"); 21 | 22 | await themeSwitcher.selectOption("frappe"); 23 | 24 | expect(await page.evaluate(() => localStorage.getItem("theme"))).toBe("frappe"); 25 | await assertCssVariable(page, "--base", flavors.frappe.colors.base.hex); 26 | }); 27 | 28 | test("changing theme with view transitions works", async ({ page }) => { 29 | await page.goto("/blog/"); 30 | await assertCssVariable(page, "--base", flavors.latte.colors.base.hex); 31 | expect(await page.evaluate(() => localStorage.getItem("theme"))).toBe(null); 32 | const themeSwitcher = page.locator("#themeSelector"); 33 | await expect(themeSwitcher).toBeVisible(); 34 | expect(await themeSwitcher.inputValue()).toBe("system"); 35 | 36 | await themeSwitcher.selectOption("macchiato"); 37 | 38 | expect(await page.evaluate(() => localStorage.getItem("theme"))).toBe("macchiato"); 39 | await assertCssVariable(page, "--base", flavors.macchiato.colors.base.hex); 40 | 41 | await page.goto("/blog/celebrating-three-years-of-catppuccin/"); 42 | await assertCssVariable(page, "--base", flavors.macchiato.colors.base.hex); 43 | 44 | await page.goto("/blog/"); 45 | await assertCssVariable(page, "--base", flavors.macchiato.colors.base.hex); 46 | }); 47 | 48 | test("theme switches on media query", async ({ page }) => { 49 | await page.goto("/"); 50 | await assertCssVariable(page, "--base", flavors.latte.colors.base.hex); 51 | expect(await page.evaluate(() => localStorage.getItem("theme"))).toBe(null); 52 | const themeSwitcher = page.locator("#themeSelector"); 53 | await expect(themeSwitcher).toBeVisible(); 54 | expect(await themeSwitcher.inputValue()).toBe("system"); 55 | 56 | await assertCssVariable(page, "--base", flavors.latte.colors.base.hex); 57 | await page.emulateMedia({ colorScheme: "dark" }); 58 | await assertCssVariable(page, "--base", flavors.mocha.colors.base.hex); 59 | }); 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "astro": "astro", 8 | "build": "pnpm run maintainers && pnpm run test:run && astro check && astro build", 9 | "check": "astro check", 10 | "dev": "astro dev", 11 | "fmt": "prettier --plugin=prettier-plugin-astro . --write", 12 | "icons": "pnpm tsx src/data/scripts/convertIconsToJson.ts", 13 | "maintainers": "pnpm tsx src/data/scripts/fetchMaintainerAvatars.ts", 14 | "prepare": "husky", 15 | "preview": "astro preview", 16 | "start": "astro dev", 17 | "test": "vitest", 18 | "test:run": "vitest run", 19 | "test:cov": "vitest run --coverage", 20 | "test:e2e": "pnpm exec playwright test", 21 | "test:e2e:ui": "pnpm exec playwright test --ui", 22 | "test:ci": "vitest run && pnpm run test:e2e" 23 | }, 24 | "lint-staged": { 25 | "src/**/*": "pnpm run fmt" 26 | }, 27 | "devDependencies": { 28 | "@astrojs/check": "^0.9.4", 29 | "@astrojs/markdown-remark": "^6.3.1", 30 | "@astrojs/mdx": "^4.2.5", 31 | "@astrojs/rss": "^4.0.10", 32 | "@astrojs/sitemap": "^3.4.0", 33 | "@astrojs/svelte": "^7.0.11", 34 | "@catppuccin/catppuccin": "github:catppuccin/catppuccin#7e4506607b8a6c298ce0876e385c52281e879245", 35 | "@catppuccin/palette": "^1.7.1", 36 | "@iconify-json/ph": "^1.2.1", 37 | "@iconify-json/simple-icons": "^1.2.33", 38 | "@iconify/svelte": "^5.0.0", 39 | "@iconify/tools": "^4.1.2", 40 | "@iconify/types": "^2.0.0", 41 | "@playwright/test": "^1.52.0", 42 | "@rollup/plugin-yaml": "^4.1.2", 43 | "@types/node": "^22.15.18", 44 | "@vitest/coverage-v8": "^3.1.3", 45 | "astro": "^5.7.13", 46 | "astro-expressive-code": "^0.41.0", 47 | "astro-icon": "1.1.5", 48 | "fuse.js": "^7.1.0", 49 | "husky": "^9.1.7", 50 | "lint-staged": "^16.0.0", 51 | "mdast-util-to-string": "^4.0.0", 52 | "playwright": "^1.52.0", 53 | "prettier": "^3.5.3", 54 | "prettier-plugin-astro": "^0.14.1", 55 | "reading-time": "^1.5.0", 56 | "rehype-autolink-headings": "^7.1.0", 57 | "sass": "^1.89.0", 58 | "sharp": "^0.34.0", 59 | "surge": "^0.24.6", 60 | "svelte": "^5.30.2", 61 | "svelte-intersection-observer-action": "^0.0.5", 62 | "tsx": "^4.19.2", 63 | "typescript": "^5.7.2", 64 | "vitest": "^3.1.3", 65 | "yaml": "^2.8.0" 66 | }, 67 | "packageManager": "pnpm@10.11.0", 68 | "engines": { 69 | "node": ">=22" 70 | }, 71 | "pnpm": { 72 | "onlyBuiltDependencies": [ 73 | "@parcel/watcher", 74 | "esbuild", 75 | "sharp" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | export default defineConfig({ 7 | testDir: "./e2e", 8 | /* Run tests in files in parallel */ 9 | fullyParallel: true, 10 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 11 | forbidOnly: !!process.env.CI, 12 | /* Retry on CI only */ 13 | retries: process.env.CI ? 2 : 0, 14 | /* Opt out of parallel tests on CI. */ 15 | workers: process.env.CI ? 1 : undefined, 16 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 17 | reporter: "html", 18 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 19 | use: { 20 | /* Base URL to use in actions like `await page.goto('/')`. */ 21 | baseURL: "http://localhost:4321/", 22 | 23 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 24 | trace: "on-first-retry", 25 | }, 26 | 27 | /* Configure projects for major browsers */ 28 | projects: [ 29 | { 30 | name: "chromium", 31 | use: { ...devices["Desktop Chrome"] }, 32 | }, 33 | { 34 | name: "firefox", 35 | use: { ...devices["Desktop Firefox"] }, 36 | }, 37 | ], 38 | 39 | webServer: { 40 | command: "pnpm dev", 41 | url: "http://localhost:4321/", 42 | reuseExistingServer: !process.env.CI, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | catppuccin.com -------------------------------------------------------------------------------- /public/embed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/public/embed.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/public/favicon.png -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | Catppuccin is not led by one person, 4 main teams are responsible for various different aspects of the project. 2 | And of course you! Whether you maintain your own port; have contributed via a pull request; or raised an issue. 3 | The community is at the heart and soul of Catppuccin. 4 | 5 | 6 | /* CORE */ 7 | Pocco 8 | GitHub: https://github.com/pocco81 9 | From: USA 10 | 11 | Hammy 12 | GitHub: https://github.com/sgoudham 13 | Website: https://goudham.com 14 | From: Scotland 15 | 16 | Lemon 17 | GitHub: https://github.com/unseen-ninja 18 | Website: https://unseen.ninja 19 | Discord: unseen.ninja 20 | From: Germany 21 | 22 | /* STAFF */ 23 | Spooky 24 | GitHub: https://github.com/ghostx31 25 | From: India 26 | 27 | Taka 28 | GitHub: https://github.com/taka0o 29 | From: Canada 30 | 31 | Isabel 32 | GitHub: https://github.com/isabelincorp 33 | From: USA 34 | 35 | pigeon 36 | GitHub: https://github.com/backwardspy 37 | Website: https://pigeon.life 38 | From: UK 39 | 40 | Amy 41 | GitHub: https://github.com/nullishamy 42 | From: UK 43 | 44 | /* USERSTYLES STAFF */ 45 | Bell 46 | GitHub: https://github.com/isabelroses 47 | Website: https://isabelroses.com 48 | From: UK 49 | 50 | uncenter 51 | GitHub: https://github.com/uncenter 52 | Website: https://uncenter.dev 53 | From: USA 54 | 55 | /* MODERATOR */ 56 | Nyx 57 | GitHub: https://github.com/nyxkrage 58 | E-Mail: carsten@kragelund.me 59 | From: Denmark 60 | 61 | Etzelia 62 | GitHub: https://github.com/jolheiser 63 | Website: https://jolheiser.com 64 | From: USA 65 | -------------------------------------------------------------------------------- /public/maintainers/256x256/placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/public/maintainers/256x256/placeholder.webp -------------------------------------------------------------------------------- /public/maintainers/64x64/placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/public/maintainers/64x64/placeholder.webp -------------------------------------------------------------------------------- /public/pronunciation.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/public/pronunciation.mp3 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | User-agent: GPTBot 4 | Disallow: / 5 | User-agent: ChatGPT-User 6 | Disallow: / 7 | User-agent: Google-Extended 8 | Disallow: / 9 | User-agent: CCBot 10 | Disallow: / 11 | User-agent: PerplexityBot 12 | Disallow: / 13 | 14 | Sitemap: https://catppuccin.com/sitemap-index.xml 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /src/components/PageIntro.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 |
10 |

{title}

11 | 12 | 13 |
14 | 15 | 24 | -------------------------------------------------------------------------------- /src/components/Pills.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 34 | -------------------------------------------------------------------------------- /src/components/ProfilePicture.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#if isPlaceholder} 16 | Placeholder Avatar 24 | {:else} 25 | {username}'s Avatar 31 | {/if} 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/ThemeSelect.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 65 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ThemeSelect from "@components/ThemeSelect.svelte"; 3 | --- 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from "astro:content"; 2 | import { glob } from "astro/loaders"; 3 | import type { AccentName } from "@catppuccin/palette"; 4 | 5 | export type BlogAuthor = { 6 | name: string; 7 | github: string; 8 | }; 9 | 10 | const blog = defineCollection({ 11 | loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/data/blog" }), 12 | schema: ({ image }) => 13 | z.object({ 14 | hero: z.object({ 15 | image: image(), 16 | alt: z.string(), 17 | author: z.string(), 18 | source: z.string(), 19 | }), 20 | title: z.string(), 21 | summary: z.string(), 22 | category: z.enum(["Announcement", "DevLog"]), 23 | accentColor: z.custom(), 24 | datePosted: z.coerce.date(), 25 | dateUpdated: z.coerce.date().optional(), 26 | featured: z.boolean().optional(), 27 | draft: z.boolean().optional(), 28 | authors: z.array(z.custom()), 29 | }), 30 | }); 31 | 32 | export const collections = { blog }; 33 | -------------------------------------------------------------------------------- /src/data/blog/celebrating-three-years-of-catppuccin.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | image: "./celebrating-three-years-of-catppuccin.png" 4 | alt: "A large field of sunflowers under a cloudy sky" 5 | author: "Jorgen Hendriksen" 6 | source: "https://unsplash.com/photos/a-large-field-of-sunflowers-under-a-cloudy-sky-H_cQ7I2FPqk" 7 | title: "Celebrating 3 Years of Catppuccin" 8 | summary: "Our first blog post to celebrate Catppuccin turning 3 years old :D" 9 | category: "Announcement" 10 | accentColor: yellow 11 | datePosted: "2024-12-05" 12 | authors: 13 | - name: "Hammy" 14 | github: "sgoudham" 15 | --- 16 | 17 | Welcome to Catppuccin's first (and very short) blog post! We'll try our best to 18 | publish important announcements, devlogs, general musings and more. 19 | 20 | ## Happy 3rd Anniversary 21 | 22 | Three years ago today, 5th December 2021, [Pocco](https://github.com/pocco81) 23 | created the 24 | [v0.1.0](https://github.com/catppuccin/catppuccin/releases/tag/v0.1.0) GitHub 25 | release. 26 | 27 | Pocco is the creator of Catppuccin. Unfortunately, he doesn't have the time to 28 | contribute to this theme anymore, but he's still around now and then so make 29 | sure to say hi if you see him! 30 | 31 | I don't think anyone could have predicted how popular Catppuccin would become, 32 | going from a simple theme built for Neovim to supporting over 394 applications 33 | and userstyles contributed by people all over the world. 34 | 35 | ## Thank You 36 | 37 | I sincerely believe that time is the most valuable resource that you can offer 38 | someone, **you can't get it back ;)** 39 | 40 | Thank you to anyone who's contributed a pull request or raised an issue, our 41 | active maintainers who have committed to maintaining a port, and of course all 42 | the people who continue to use the theme today! 43 | 44 | Here's to another amazing year \<3 45 | 46 | ### P.S 47 | 48 | Lookout for the "State of Catppuccin 2024" post, where I'll take a look back on 49 | what Catppuccin achieved in the past year and what to expect in 2025! 50 | -------------------------------------------------------------------------------- /src/data/blog/celebrating-three-years-of-catppuccin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/src/data/blog/celebrating-three-years-of-catppuccin.png -------------------------------------------------------------------------------- /src/data/blog/state-of-catppuccin-2024.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | image: "./celebrating-three-years-of-catppuccin.png" 4 | alt: "A large field of sunflowers under a cloudy sky" 5 | author: "Jorgen Hendriksen" 6 | source: "https://unsplash.com/photos/a-large-field-of-sunflowers-under-a-cloudy-sky-H_cQ7I2FPqk" 7 | title: "State of Catppuccin 2024" 8 | summary: "Look back on what we achieved in 2024, what we're currently working on and what to expect going forward in 2025!" 9 | category: "Announcement" 10 | accentColor: mauve 11 | datePosted: "2024-12-05" 12 | draft: true 13 | authors: 14 | - name: "Hammy" 15 | github: "sgoudham" 16 | --- 17 | 18 | Having felt quite inspired by Spotify Wrapped last year, I whipped up a short 19 | [presentation](https://github.com/catppuccin/community/blob/main/presentations/state-of-catppuccin-2023.pdf) 20 | to showcase statistics, highlight achievements and set loose goals for 2024. 21 | Everyone will be glad to hear that I won't be creating presentations anymore 22 | and instead use the blog to share this information. 23 | 24 | Let's see how we did in 2024 and compare them to 2023! 25 | 26 | ## Statistics 27 | 28 | TODO 29 | 30 | ## Highlights 31 | 32 | TODO 33 | 34 | ## The Future (2025) 35 | 36 | TODO 37 | 38 | ## Thank You \<3 39 | 40 | TODO 41 | -------------------------------------------------------------------------------- /src/data/blog/why-we-ditched-the-icon-for-external-links.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | image: "./why-we-ditched-the-icon-for-external-links.png" 4 | alt: "Aerial photography of concrete roads" 5 | author: "Denys Nevozhai" 6 | source: "https://unsplash.com/photos/aerial-photography-of-concrete-roads-7nrsVjvALnA" 7 | title: "Why We Ditched the Icon for External Links" 8 | summary: "The small arrow beside external links was nice... so why did we get rid of it?" 9 | category: "DevLog" 10 | accentColor: maroon 11 | datePosted: "2024-12-18" 12 | featured: true 13 | authors: 14 | - name: "Lemon" 15 | github: "unseen-ninja" 16 | - name: "Hammy" 17 | github: "sgoudham" 18 | --- 19 | 20 | You may or may not have already noticed it; The small arrow (`↗`) we used to 21 | indicate links with a target outside of `catppuccin.com` has been removed. 22 | 23 | Surprisingly, this ended up being a tough decision as we grew quite attached to 24 | the little guy, but ultimately the decision came down to two key concerns: 25 | 26 | 1. **Rise in technical complexity.** 27 | 2. **Icon ambiguity.** 28 | 29 | Let's dive in! 30 | 31 | ## The `` component 32 | 33 | For the initial release of the website, we created a `` component to be used 34 | throughout the codebase. This worked as expected, giving us control of the 35 | styling based on the props passed in or omitted. 36 | 37 | ```astro title="Link.astro" 38 | --- 39 | interface Props { 40 | href: string; 41 | external?: boolean; 42 | muted?: boolean; 43 | } 44 | 45 | const { href, external = false, muted = false } = Astro.props; 46 | --- 47 | 48 | 49 | {external ? : ""} 50 | 51 | 52 | 55 | ``` 56 | 57 | This isn't the only approach we could have taken to append the icon to all 58 | external links. For example, an alternative CSS based approach: 59 | 60 | ```scss title="_typography.scss" 61 | a[href^="https://"]::after { 62 | content: '\2197'; 63 | } 64 | ``` 65 | 66 | At first glance, this approach seems quite elegant. However, we decided against 67 | it as a number of exceptions (e.g. images/badges as links, etc) made this quite 68 | complex as well. 69 | 70 | With no real reason to revisit or refactor the approach after the initial 71 | launch, the `` component was left untouched - until a few weeks ago. 72 | 73 | ## External link icons in blog posts 74 | 75 | Recently, we made the decision to launch a blog and, thankfully, Astro made it 76 | _very easy_ to get one up and running. We added the official 77 | [@astrojs/mdx](https://github.com/withastro/astro/tree/main/packages/integrations/mdx/) 78 | integration, created the new blog page, and wrote [our first blog 79 | post!](/blog/celebrating-three-years-of-catppuccin) 🥳 80 | 81 | While writing the first blog post, we realised the `` component must be 82 | imported to specify an external link, which we weren't excited about: 83 | 84 | ```mdx title="celebrating-three-years-of-catppuccin.mdx" 85 | import Link from "../../components/Link.astro" 86 | 87 | Three years ago today, 5th December 2021, Pocco created the v0.1.0 GitHub release. 91 | ``` 92 | 93 | We tried mapping the `` tag to the `` component, but couldn't quite 94 | figure out how to pass in the `external` prop. 95 | 96 | It would be a bad idea to assume all links within a blog post are internal or 97 | external, so we ditched this idea and continued on with other tasks we wanted to 98 | accomplish. 99 | 100 | ## Auto detecting external links 101 | 102 | A few days later, we performed the upgrade to [Astro 5 and Svelte 103 | 5](https://github.com/catppuccin/website/pull/116), prompting us to reconsider 104 | and re-evaluate the structure of the codebase. When revisiting the `` 105 | component, we realised that we could use the `SITE` environment variable given 106 | by Astro to detect external links. 107 | 108 | Here's the new rewritten `` component: 109 | 110 | ```svelte title="Link.svelte" 111 | 126 | 127 | 128 | 129 | {#snippet externalLinkIcon()} 130 | {#if externalIcon}{/if} 131 | {/snippet} 132 | 133 | {#if !href.includes(domain) && !href.startsWith("/") && !href.startsWith("#")} 134 | {@render children()}{@render externalLinkIcon()} 135 | {:else} 136 | {@render children()} 137 | {/if} 138 | 139 | 142 | ``` 143 | 144 | We again took the opportunity to add the mapping between the `` tag and the 145 | new `` component: 146 | 147 | ```astro title="blog/[id].astro" ins="components={{ a: Link }}" 148 | 149 | ``` 150 | 151 | Allowing the import to be removed in favour of normal Markdown: 152 | 153 | ```diff title="celebrating-three-years-of-catppuccin.mdx" lang="mdx" 154 | - import Link from "../../components/Link.astro" 155 | 156 | - Three years ago today, 5th December 2021, Pocco created the v0.1.0 GitHub release. 160 | + Three years ago today, 5th December 2021, [Pocco](https://github.com/pocco81) 161 | + created the 162 | + [v0.1.0](https://github.com/catppuccin/catppuccin/releases/tag/v0.1.0) GitHub 163 | + release. 164 | ``` 165 | 166 | Of course, this meant the external link icon couldn't be disabled in blog posts 167 | but that was a trade-off we were okay with. 168 | 169 | The new approach is much better right...? Well, actually, we didn't think so. 170 | The implementation is a lot more complicated and convoluted than we ever 171 | imagined a "simple" `` component to be. 172 | 173 | External links were now identified based on the value of `href`, resulting in an 174 | extra `externalIcon` prop to override this behaviour. For example, the "Powered 175 | By Vercel" badge in the footer is an external link but should not display the 176 | icon. 177 | 178 | At this point, we were suitably fed up of the extra complexity we were bringing 179 | into this codebase, but the real breaking point was yet to come... 180 | 181 | ## Adding links to blog headings 182 | 183 | This breaking point came when we started work on adding anchor links to blog 184 | headings. Once again, Astro comes to the rescue by making it relatively painless 185 | to configure and set up anchor links. 186 | 187 | Astro supports plugins within the 188 | [rehype](https://docs.astro.build/en/guides/markdown-content/#adding-remark-and-rehype-plugins) 189 | ecosystem, meaning we can import 190 | [rehype-autolink-headings](https://github.com/rehypejs/rehype-autolink-headings) 191 | and have headings automatically wrapped, prepended, appended, etc with anchor 192 | tags. 193 | 194 | Unfortunately, much to our disappointment, rehype was unable to inject the 195 | anchor tags into the headings because our own `` component was being applied 196 | afterwards, removing the crucial CSS classes needed to style the headings. 197 | 198 | Looking back, we're quite happy that rehype didn't support this as it only would 199 | have resulted in more overrides and complexity. The component was originally 200 | designed to unify our approach but with so many overrides, it ended up evolving 201 | into nothing but a pretty big headache. 202 | 203 | It hurt knowing that we engineered ourselves into this little corner of 204 | complexity and that our problems would practically vanish if the external link 205 | icon was removed, therefore removing the need for a `` component too. 206 | 207 | ## Ambiguity around the external link icon 208 | 209 | Leaving the technical complexity aside for one moment, an important question we 210 | didn't ask ourselves at the beginning was "What does the external link icon 211 | _really_ mean?" 212 | 213 | This kind of iconography has existed for a long time and while we personally 214 | thought the meaning of the icon was clear, it turns out not everyone thinks 215 | about it the same way we do. 216 | 217 | Does it indicate an external link – _like we used it on this website_ – or would 218 | it indicate that the link – _no matter the domain_ – opens in a new tab – or... 219 | _both_? 220 | 221 | There are numerous forums online ~~debating~~ discussing what this icon's 222 | behaviour should be. One of the most interesting sources of information we found 223 | in favour of removing it was from the UK government, who decided to remove the 224 | icon from their design system back in 2016: 225 | 226 | > We’d assumed that there was a clear need for the external link icon, but in 4 227 | > years we’ve seen no evidence that backs this up [...] 228 | > 229 | > We drew on the experience of the design and research communities in government 230 | > and asked them if they’d ever observed users making use of the icon, which they 231 | > hadn’t. [...] 232 | > 233 | > ~ Tim Paul on [designnotes.blog.gov.uk](https://designnotes.blog.gov.uk/2016/11/28/removing-the-external-link-icon-from-gov-uk/) 234 | 235 | Similarly, the [United States Web Design System 236 | (USWDS)](https://designsystem.digital.gov/) team carried out a [research 237 | study](https://github.com/uswds/uswds/wiki/2021-07-29-External-Link-Indicator-Research-Findings) 238 | involving users being exposed to consistent usage of an external link icon and 239 | arrived at the following conclusions: 240 | 241 | > Users didn't consistently understand the external link icon or the "Exit" badge. [...] 242 | > 243 | > Users are more likely to ignore link icons and badges than think about their 244 | > meaning. Majority of the participants didn't mention the external link 245 | > indicators when asked to read aloud the paragraph that contained external links. [...] 246 | > 247 | > ~ USWDS team via [External Link Indicator Research Findings](https://github.com/uswds/uswds/wiki/2021-07-29-External-Link-Indicator-Research-Findings) 248 | 249 | The more we researched, the more we understood what we had to do. 250 | 251 | ## Saying farewell 252 | 253 | At last, we've arrived at present day. 254 | 255 | With the release of this blog post, the external link icon has been removed and 256 | the custom `` component has been deleted. Normal `` tags are used 257 | throughout the codebase and the CSS is defined globally, allowing it to be 258 | overridden at a local level. 259 | 260 | ```scss title="_typography.scss" 261 | a { 262 | text-decoration: none; 263 | color: var(--blue); 264 | 265 | &:hover, 266 | &:focus { 267 | text-decoration: underline; 268 | } 269 | } 270 | ``` 271 | 272 | There are some life lessons here about avoiding unnecessary complexity and 273 | actually understanding user behaviour, but we'll leave you to come to your own 274 | conclusions. 275 | 276 | Farewell little `↗`, you will be missed. 277 | -------------------------------------------------------------------------------- /src/data/blog/why-we-ditched-the-icon-for-external-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/website/34e28ce3b625acfa9a288af2744f67454b03d89e/src/data/blog/why-we-ditched-the-icon-for-external-links.png -------------------------------------------------------------------------------- /src/data/governance.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "yaml"; 2 | 3 | /** 4 | * Information about all the teams leading Catppuccin. 5 | * 6 | * @minItems 1 7 | */ 8 | export type AllLeadershipTeams = [LeadershipTeam, ...LeadershipTeam[]]; 9 | /** 10 | * The display name of the team. 11 | */ 12 | export type TeamName = string; 13 | /** 14 | * The identifier of the team, which is the a machine-readable version of the team name. 15 | */ 16 | export type Identifier = string; 17 | /** 18 | * The Catppuccin accent color attributed to the team. 19 | */ 20 | export type Color = 21 | | "rosewater" 22 | | "flamingo" 23 | | "pink" 24 | | "mauve" 25 | | "red" 26 | | "maroon" 27 | | "peach" 28 | | "yellow" 29 | | "green" 30 | | "teal" 31 | | "sky" 32 | | "sapphire" 33 | | "blue" 34 | | "lavender"; 35 | /** 36 | * A short description of what the team is. 37 | */ 38 | export type Description = string; 39 | /** 40 | * A list of items that the team is responsible for. 41 | */ 42 | export type Responsibilities = string; 43 | /** 44 | * The display name of the member. 45 | */ 46 | export type DisplayName = string; 47 | /** 48 | * The GitHub profile link of the member. 49 | */ 50 | export type GitHubProfile = string; 51 | /** 52 | * List of all members in this team. 53 | */ 54 | export type CurrentMembers = { 55 | name: DisplayName; 56 | url: GitHubProfile; 57 | }[]; 58 | /** 59 | * List of all members who used to be part of the team. 60 | */ 61 | export type PastMembers = { 62 | name: DisplayName; 63 | url: GitHubProfile; 64 | }[]; 65 | 66 | export interface GovernanceSchema { 67 | leadership: AllLeadershipTeams; 68 | } 69 | export interface LeadershipTeam { 70 | name: TeamName; 71 | identifier: Identifier; 72 | color: Color; 73 | description: Description; 74 | responsibilities: Responsibilities; 75 | "current-members": CurrentMembers; 76 | "past-members": PastMembers; 77 | } 78 | 79 | const governanceYml = (await fetch( 80 | "https://raw.githubusercontent.com/catppuccin/.github/refs/heads/main/governance/governance.yml", 81 | ) 82 | .then((r) => r.text()) 83 | .then((t) => parse(t))) as GovernanceSchema; 84 | 85 | export const leadership = governanceYml.leadership; 86 | -------------------------------------------------------------------------------- /src/data/icons.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyIcon, IconifyJSONIconsData } from "@iconify/types"; 2 | 3 | import customIconsJson from "./icons.json" with { type: "json" }; 4 | import simpleIconsJson from "@iconify-json/simple-icons/icons.json" with { type: "json" }; 5 | import phIconsJson from "@iconify-json/ph/icons.json" with { type: "json" }; 6 | 7 | const customIcons = customIconsJson as IconifyJSONIconsData; 8 | const simpleIcons = simpleIconsJson as IconifyJSONIconsData; 9 | const phIcons = phIconsJson as IconifyJSONIconsData; 10 | 11 | const DEFAULT_VIEWBOX = 16; 12 | 13 | const phosphorIcon = (name: string) => { 14 | const icon = phIcons.icons[name]; 15 | return { 16 | body: icon.body, 17 | width: icon.width ?? phIcons.width ?? DEFAULT_VIEWBOX, 18 | height: icon.height ?? phIcons.height ?? DEFAULT_VIEWBOX, 19 | }; 20 | }; 21 | 22 | const simpleIcon = (name: string) => { 23 | const icon = simpleIcons.icons[name]; 24 | return { 25 | body: icon.body, 26 | width: icon.width ?? simpleIcons.width ?? DEFAULT_VIEWBOX, 27 | height: icon.height ?? simpleIcons.height ?? DEFAULT_VIEWBOX, 28 | }; 29 | }; 30 | 31 | const customIcon = (name: string): IconifyIcon => { 32 | const icon = customIcons.icons[name]; 33 | return { 34 | body: icon.body, 35 | width: icon.width ?? customIcons.width ?? DEFAULT_VIEWBOX, 36 | height: icon.height ?? customIcons.height ?? DEFAULT_VIEWBOX, 37 | }; 38 | }; 39 | 40 | export const portIcon = (name: string | undefined): IconifyIcon => { 41 | if (!name) { 42 | return phosphorIcon("cube-fill"); 43 | } 44 | if (name.endsWith(".svg")) { 45 | return customIcon(name.split(".")[0]); 46 | } 47 | if (name in simpleIcons.icons) { 48 | return simpleIcon(name); 49 | } 50 | // When a simple icon exists as an alias for the port, the parent must be used to get the body of the SVG. 51 | if (simpleIcons.aliases && name in simpleIcons.aliases) { 52 | return simpleIcon(simpleIcons.aliases[name].parent); 53 | } 54 | return phosphorIcon("cube-fill"); 55 | }; 56 | 57 | export const icon = (identifier: string): IconifyIcon => { 58 | const [prefix, name] = identifier.split(":"); 59 | const lib = [customIcons, simpleIcons, phIcons].find((lib) => lib.prefix === prefix); 60 | if (lib) { 61 | const icon = lib.icons[name]; 62 | if (icon) { 63 | return { 64 | body: icon.body, 65 | width: icon.width ?? lib.width ?? DEFAULT_VIEWBOX, 66 | height: icon.height ?? lib.height ?? DEFAULT_VIEWBOX, 67 | }; 68 | } 69 | } 70 | throw new Error(`Icon '${name}' for identifier '${identifier}' could not be found.`); 71 | }; 72 | -------------------------------------------------------------------------------- /src/data/icons/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | Catppuccin Logo Text 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/data/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | Catppuccin Logo Pepperjack 3 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/data/icons/lucide-user-round-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/data/icons/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /src/data/icons/org/core.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /src/data/icons/org/moderator.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /src/data/icons/org/staff.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /src/data/icons/org/userstyles-staff.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /src/data/icons/ports/cursors.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/folders.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/gboard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/gitui.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/hexchat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/lxqt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/minecraft.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/qutebrowser.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/windows-files.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/zathura.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/ports/zellij.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/icons/speaker-high-volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/data/links.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "yaml"; 2 | 3 | export interface SocialsSchema { 4 | socials: SocialLinks; 5 | } 6 | /** 7 | * Links to all of the Catppuccin accounts on various social media platforms. 8 | */ 9 | export interface SocialLinks { 10 | [k: string]: Social; 11 | } 12 | 13 | export interface Social { 14 | /** 15 | * The name of the social media platform. 16 | */ 17 | name: string; 18 | /** 19 | * The link to the Catppuccin account on that social media platform. 20 | */ 21 | url: string; 22 | } 23 | 24 | const socials = ( 25 | (await fetch("https://raw.githubusercontent.com/catppuccin/.github/refs/heads/main/socials/socials.yml") 26 | .then((r) => r.text()) 27 | .then((t) => parse(t))) as SocialsSchema 28 | ).socials; 29 | 30 | export const navigationLinks = [ 31 | { 32 | title: "Ports", 33 | target: "/ports", 34 | accent: "peach", 35 | }, 36 | { 37 | title: "Palette", 38 | target: "/palette", 39 | accent: "mauve", 40 | }, 41 | { 42 | title: "Community", 43 | target: "/community", 44 | accent: "green", 45 | }, 46 | { 47 | title: "Blog", 48 | target: "/blog", 49 | accent: "blue", 50 | }, 51 | ]; 52 | 53 | export const footerLinks = [ 54 | { 55 | categoryTitle: "Project", 56 | categoryLinks: [ 57 | ...navigationLinks, 58 | { 59 | title: "Open Collective", 60 | target: "https://opencollective.com/catppuccin", 61 | }, 62 | { 63 | title: "GitHub Sponsors", 64 | target: "https://github.com/sponsors/catppuccin", 65 | }, 66 | ], 67 | }, 68 | { 69 | categoryTitle: "Social", 70 | categoryLinks: [ 71 | socials.github, 72 | socials.discord, 73 | socials.mastodon, 74 | socials.twitter, 75 | socials.bsky, 76 | socials.spotify, 77 | ].map((link) => ({ 78 | title: link.name, 79 | target: link.url, 80 | })), 81 | }, 82 | ]; 83 | 84 | export const githubUrl = socials.github.url; 85 | -------------------------------------------------------------------------------- /src/data/ports.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBasedSet } from "./propertyBasedSet"; 2 | import { portIcon } from "./icons"; 3 | import type { IconifyIcon } from "@iconify/types"; 4 | import type { 5 | Category, 6 | Collaborator, 7 | Collaborators, 8 | PlatformKey, 9 | Port, 10 | PortsPorcelainSchema, 11 | } from "@catppuccin/catppuccin/resources/types/ports.porcelain.schema"; 12 | 13 | export type PortWithIcons = Port & { icon: IconifyIcon }; 14 | export type CategoryWithPortCount = Category & { portCount: number }; 15 | export type Platforms = Array<{ 16 | key: PlatformKey; 17 | name: string; 18 | }>; 19 | 20 | // Trust upstream (catppuccin/catppuccin) has validated against the JSONSchema 21 | export const porcelain = (await fetch( 22 | "https://raw.githubusercontent.com/catppuccin/catppuccin/main/resources/ports.porcelain.json", 23 | ) 24 | .then((r) => r.text()) 25 | .then((t) => JSON.parse(t))) as PortsPorcelainSchema; 26 | 27 | // Sort items & get the icon strings for each port 28 | export const ports = [...porcelain.ports] 29 | .sort((a, b) => a.key.localeCompare(b.key)) 30 | .map((port) => { 31 | return { 32 | ...port, 33 | icon: portIcon(port.icon), 34 | } as PortWithIcons; 35 | }); 36 | 37 | export const platforms: Platforms = [ 38 | { key: "linux", name: "Linux" }, 39 | { key: "macos", name: "macOS" }, 40 | { key: "windows", name: "Windows" }, 41 | { key: "android", name: "Android" }, 42 | { key: "ios", name: "iOS" }, 43 | { key: "web", name: "Web" }, 44 | ]; 45 | 46 | export const categories = porcelain.categories.map((category) => { 47 | const portCount = ports.filter((port) => port.categories.some((c) => c.key === category.key)).length; 48 | return { 49 | ...category, 50 | portCount, 51 | }; 52 | }); 53 | categories.sort((a, b) => b.portCount - a.portCount); 54 | 55 | // We need the current maintainers for both userstyles and ports 56 | export const currentMaintainers: Collaborators = new PropertyBasedSet( 57 | (m) => m.url, 58 | [...porcelain.ports.map((p) => p.repository)].flatMap((p) => p["current-maintainers"]), 59 | ).sorted(); 60 | -------------------------------------------------------------------------------- /src/data/propertyBasedSet.ts: -------------------------------------------------------------------------------- 1 | export class PropertyBasedSet { 2 | private items: T[] = []; 3 | private getKey: (item: T) => string; 4 | 5 | constructor(getKey: (item: T) => string, items: T[] = []) { 6 | this.getKey = getKey; 7 | for (const item of items) { 8 | this.add(item); 9 | } 10 | } 11 | 12 | add(item: T): void { 13 | const key = this.getKey(item); 14 | if (!this.items.some((existing) => this.getKey(existing) === key)) { 15 | this.items.push(item); 16 | } 17 | } 18 | 19 | values(): T[] { 20 | return [...this.items]; 21 | } 22 | 23 | sorted() { 24 | return this.values().sort((a, b) => this.getKey(a).localeCompare(this.getKey(b))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/data/scripts/convertIconsToJson.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import { importDirectory, cleanupSVG, runSVGO, parseColors, isEmptyColor } from "@iconify/tools"; 3 | 4 | const source = "src/data/icons"; 5 | const target = "src/data/icons.json"; 6 | 7 | const coloredIcons = ["magnifying-glass", "logo", "logo-text"]; 8 | 9 | (async () => { 10 | const iconSet = await importDirectory(source, { 11 | prefix: "ctp", 12 | }); 13 | 14 | // Validate, clean up, fix palette and optimise 15 | iconSet.forEach((name, type) => { 16 | if (type !== "icon") { 17 | return; 18 | } 19 | 20 | const svg = iconSet.toSVG(name); 21 | if (!svg) { 22 | iconSet.remove(name); 23 | return; 24 | } 25 | 26 | try { 27 | cleanupSVG(svg); 28 | 29 | // Skip the colored SVGs from being monochrome 30 | if (!coloredIcons.includes(name)) { 31 | // Assume icon is monotone: replace color with currentColor, add if missing 32 | parseColors(svg, { 33 | defaultColor: "currentColor", 34 | callback: (_, colorStr, color) => { 35 | return !color || isEmptyColor(color) ? colorStr : "currentColor"; 36 | }, 37 | }); 38 | } 39 | 40 | runSVGO(svg); 41 | } catch (err) { 42 | console.error(`Error parsing ${name}:`, err); 43 | iconSet.remove(name); 44 | return; 45 | } 46 | 47 | iconSet.fromSVG(name, svg); 48 | }); 49 | 50 | const exported = JSON.stringify(iconSet.export(), null, "\t") + "\n"; 51 | await fs.writeFile(target, exported, "utf8"); 52 | })(); 53 | -------------------------------------------------------------------------------- /src/data/scripts/fetchMaintainerAvatars.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import { currentMaintainers } from "../ports"; 3 | import sharp from "sharp"; 4 | import type { Collaborator } from "@catppuccin/catppuccin/resources/types/ports.porcelain.schema"; 5 | 6 | const MAINTAINERS_DIR = "src/data/maintainers"; 7 | const PUBLIC_MAINTAINERS_DIR = "public/maintainers"; 8 | const IMAGE_QUALITY = 80; 9 | const REQUEST_SIZE = 256; 10 | const SIZES = [64, 256]; 11 | 12 | const maintainersWithoutAvatars: string[] = []; 13 | 14 | async function maintainersToFetch() { 15 | const existingFiles = await fs.readdir(`${PUBLIC_MAINTAINERS_DIR}/${REQUEST_SIZE}x${REQUEST_SIZE}`); 16 | const existingMaintainers = new Set(existingFiles.map((file) => file.split(".")[0])); 17 | return currentMaintainers.filter((maintainer) => !existingMaintainers.has(maintainer.username)); 18 | } 19 | 20 | async function fetchAndProcessImage(maintainer: Collaborator) { 21 | const response = await fetch(`${maintainer.url}.png?size=${REQUEST_SIZE}`); 22 | if (!response.ok) { 23 | console.warn(`Failed to fetch ${maintainer.url}: ${response.status} ${response.statusText}`); 24 | maintainersWithoutAvatars.push(maintainer.username); 25 | return; 26 | } 27 | 28 | const buffer = await response.arrayBuffer(); 29 | 30 | await Promise.all( 31 | SIZES.map((size) => 32 | sharp(buffer) 33 | .resize(size, size) 34 | .webp({ quality: IMAGE_QUALITY }) 35 | .toFile(`${PUBLIC_MAINTAINERS_DIR}/${size}x${size}/${maintainer.username}.webp`), 36 | ), 37 | ); 38 | } 39 | 40 | try { 41 | await fs.mkdir(MAINTAINERS_DIR, { recursive: true }); 42 | 43 | console.info(`[INFO]: ${currentMaintainers.length} total maintainers`); 44 | const maintainers = await maintainersToFetch(); 45 | console.info(`[INFO]: ${currentMaintainers.length - maintainers.length} maintainers already fetched`); 46 | console.info(`[INFO]: fetching ${maintainers.length} maintainers`); 47 | 48 | await Promise.all(maintainers.map((maintainer) => fetchAndProcessImage(maintainer))); 49 | 50 | await fs.writeFile(`${MAINTAINERS_DIR}/maintainersWithoutAvatars.json`, JSON.stringify(maintainersWithoutAvatars)); 51 | } catch (e) { 52 | console.error("Processing failed: ", e); 53 | process.exit(1); 54 | } 55 | -------------------------------------------------------------------------------- /src/layouts/Default.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { AccentName } from "@catppuccin/palette"; 3 | 4 | import Skeleton from "./Skeleton.astro"; 5 | 6 | import Navigation from "./components/Navigation.astro"; 7 | import Head from "./components/Head.astro"; 8 | import Footer from "./components/Footer.astro"; 9 | import AccentBar from "./components/AccentBar.astro"; 10 | 11 | interface Props { 12 | title: string; 13 | description: string; 14 | ogImage?: string; 15 | accent?: AccentName; 16 | enableViewTransition?: boolean; 17 | } 18 | 19 | const { title, description, ogImage, accent, enableViewTransition } = Astro.props; 20 | --- 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 | 41 |
42 | 43 | 50 | -------------------------------------------------------------------------------- /src/layouts/Landing.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { AccentName } from "@catppuccin/palette"; 3 | 4 | import Skeleton from "./Skeleton.astro"; 5 | 6 | import Head from "./components/Head.astro"; 7 | import Footer from "./components/Footer.astro"; 8 | import AccentBar from "./components//AccentBar.astro"; 9 | 10 | import LogoText from "@data/icons/logo-text.svg"; 11 | import Logo from "@data/icons/logo.svg"; 12 | 13 | interface Props { 14 | title: string; 15 | description: string; 16 | ogImage?: string; 17 | accent?: AccentName; 18 | } 19 | 20 | const { title, description, ogImage, accent } = Astro.props; 21 | --- 22 | 23 | 24 | 25 |
26 |
27 |
28 | 32 | 33 |
34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 | 47 |
48 | 49 | 176 | -------------------------------------------------------------------------------- /src/layouts/Skeleton.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import "@styles/global.scss"; 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/layouts/components/AccentBar.astro: -------------------------------------------------------------------------------- 1 |
2 | 3 | 78 | -------------------------------------------------------------------------------- /src/layouts/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import SpeakerHighVolume from "@data/icons/speaker-high-volume.svg"; 3 | import ThemeSwitcher from "@components/ThemeSwitcher.astro"; 4 | 5 | import { footerLinks } from "@data/links"; 6 | --- 7 | 8 |
9 | { 10 | footerLinks.map(({ categoryTitle, categoryLinks }) => ( 11 | 23 | )) 24 | } 25 | 26 |
27 |
28 |

Catppuccin

29 |

30 | /ˌkætpʊˈtʃiːn/ 31 | 37 | 38 |
39 |

40 | catppuccin.com is built and maintained by the community at catppuccin/website. 43 |

44 |

45 | © {new Date(Date.now()).getFullYear()} • Licensing 46 |

47 | 48 |
49 | 50 | 98 |
99 | -------------------------------------------------------------------------------- /src/layouts/components/Head.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { AccentName } from "@catppuccin/palette"; 3 | import { flavors } from "@catppuccin/palette"; 4 | import { ClientRouter } from "astro:transitions"; 5 | 6 | interface Props { 7 | title: string; 8 | description: string; 9 | ogImage?: string; 10 | accent?: AccentName; 11 | enableViewTransition?: boolean; 12 | } 13 | 14 | const { 15 | title, 16 | description, 17 | ogImage = "/embed.png", 18 | accent = "mauve", 19 | enableViewTransition = false, 20 | } = Astro.props as Props; 21 | 22 | const isProduction = import.meta.env.CATPPUCCIN_PROD; 23 | --- 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {isProduction && 31 | 32 | 50 | 51 | 70 | -------------------------------------------------------------------------------- /src/pages/palette/_components/FlavorName.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { FlavorName } from "@catppuccin/palette"; 3 | import { flavors } from "@catppuccin/palette"; 4 | 5 | interface Props { 6 | flavor: FlavorName; 7 | bold?: boolean; 8 | } 9 | 10 | const { flavor, bold } = Astro.props as Props; 11 | --- 12 | 13 | { 14 | bold === false ? ( 15 | {flavors[flavor].name} 16 | ) : ( 17 | {flavors[flavor].name} 18 | ) 19 | } 20 | 21 | 35 | -------------------------------------------------------------------------------- /src/pages/palette/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { flavorEntries, type ColorFormat } from "@catppuccin/palette"; 3 | 4 | import { githubUrl } from "@data/links"; 5 | 6 | import Default from "@layouts/Default.astro"; 7 | 8 | import PageIntro from "@components/PageIntro.astro"; 9 | 10 | import CopyToClipboardIcon from "./_components/CopyToClipboardButton.svelte"; 11 | import FlavorName from "./_components/FlavorName.astro"; 12 | 13 | const toRgb = (rgb: ColorFormat["rgb"]) => { 14 | return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; 15 | }; 16 | const toHsl = (hsl: ColorFormat["hsl"]) => { 17 | return `hsl(${Math.round(hsl.h)}deg, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`; 18 | }; 19 | --- 20 | 21 | 24 | 25 |

26 | Catppuccin consists of 4 beautiful pastel color palettes, named flavors. The theme comes in one 27 | light and three dark variants: 28 |

    29 |
  • 30 | : Our lightest theme harmoniously inverting the essence of Catppuccin's dark 31 | themes. 32 |
  • 33 |
  • 34 | : A less vibrant alternative using subdued colors for a muted aesthetic. 35 |
  • 36 |
  • 37 | : Medium contrast with gentle colors creating a soothing atmosphere. 38 |
  • 39 |
  • 40 | : The Original — Our darkest variant offering a cozy feeling with color-rich 41 | accents. 42 |
  • 43 |
44 |

45 | If you'd like to use them for your own project, refer to our 46 | style guide 47 | for general use cases and guidelines. Additionally, you can find integrations with popular frameworks and tools in 48 | catppuccin/palette. 49 |

50 |

51 | 52 |
53 | { 54 | flavorEntries.map(([flavorName, flavor]) => ( 55 |
56 |
57 | 58 |

59 | 60 |

61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {Object.values(flavor.colors).map(({ hex, rgb, hsl, name }) => ( 73 | 74 | 77 | 82 | 87 | 92 | 93 | ))} 94 | 95 |
ColorHexRGBHSL
75 |
{name}
76 |
78 | 79 | {hex} 80 | 81 | 83 | 84 | {toRgb(rgb)} 85 | 86 | 88 | 89 | {toHsl(hsl)} 90 | 91 |
96 |
97 |
98 | )) 99 | } 100 |
101 |
102 | 103 | 164 |
165 | -------------------------------------------------------------------------------- /src/pages/ports/_components/PortCard.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 |
25 |

{port.name}

26 | 35 |
36 | 37 |
38 | 39 | 104 | -------------------------------------------------------------------------------- /src/pages/ports/_components/PortExplorer.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 |
e.preventDefault()}> 56 | 57 |
58 | Platforms 59 | {#each platforms as platform (platform.key)} 60 | 72 | {/each} 73 |
74 |
75 | Categories 76 | {#each categories as category, i (category.key)} 77 | 92 | {/each} 93 |
94 | 95 |
96 | 97 |
98 |
99 | 100 | 221 | -------------------------------------------------------------------------------- /src/pages/ports/_components/PortGrid.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if portGrid.length === 0} 14 |
15 |

Sorry, we couldn't find any ports matching your search :(

16 |

17 | You can request a port to be themed by raising a 18 | port request in catppuccin/catppuccin. 21 |

22 |
23 | {:else if portGrid.length > 0} 24 | {#each portGrid as port (port.key)} 25 | 26 | {/each} 27 | {/if} 28 |
29 | -------------------------------------------------------------------------------- /src/pages/ports/_components/PortMaintainers.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if port.repository["current-maintainers"].length > 0} 10 |
11 | {#each port.repository["current-maintainers"] as maintainer} 12 | 13 | {/each} 14 |
15 | {:else} 16 |
17 | {@html NoMaintainerIcon} 18 |
19 | {/if} 20 | 21 | 26 | -------------------------------------------------------------------------------- /src/pages/ports/_components/SearchBar.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | 91 | -------------------------------------------------------------------------------- /src/pages/ports/_components/state.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { CategoryKey, PlatformKey } from "@catppuccin/catppuccin/resources/types/ports.porcelain.schema"; 2 | import type { PortWithIcons } from "@data/ports"; 3 | import Fuse from "fuse.js/min-basic"; 4 | 5 | type UrlParamsState = { 6 | query: string | null; 7 | platforms: PlatformKey[]; 8 | category: CategoryKey | null; 9 | }; 10 | 11 | export const url = new URL(window.location.href); 12 | 13 | const fuse = new Fuse([] as PortWithIcons[], { 14 | keys: [ 15 | { name: "key", weight: 1 }, 16 | { name: "name", weight: 0.5 }, 17 | { name: "categories.name", weight: 0.25 }, 18 | { name: "repository.current-maintainers.username", weight: 0.1 }, 19 | ], 20 | includeScore: false, 21 | threshold: 0.2, 22 | }); 23 | 24 | export const urlParams: UrlParamsState = $state({ 25 | query: url.searchParams.get("q"), 26 | platforms: url.searchParams.getAll("p") as PlatformKey[], 27 | category: url.searchParams.get("c") as CategoryKey | null, 28 | }); 29 | 30 | export const updateQueryUrlParams = () => { 31 | if (urlParams.query) { 32 | url.searchParams.set("q", urlParams.query); 33 | } else { 34 | url.searchParams.delete("q"); 35 | } 36 | window.history.replaceState(null, "", url.toString()); 37 | }; 38 | 39 | export const updatePlatformsUrlParams = () => { 40 | url.searchParams.delete("p"); 41 | if (urlParams.platforms.length > 0) { 42 | urlParams.platforms.forEach((platform) => { 43 | url.searchParams.append("p", platform); 44 | }); 45 | } 46 | window.history.replaceState(null, "", url.toString()); 47 | }; 48 | 49 | export const updateCategoryUrlParams = () => { 50 | if (urlParams.category) { 51 | url.searchParams.set("c", urlParams.category); 52 | } else { 53 | url.searchParams.delete("c"); 54 | } 55 | window.history.replaceState(null, "", url.toString()); 56 | }; 57 | 58 | export function queryPorts(ports: PortWithIcons[]) { 59 | fuse.setCollection(ports); 60 | return urlParams.query ? fuse.search(urlParams.query).map((result) => result.item) : ports; 61 | } 62 | 63 | export function filterPorts(ports: PortWithIcons[]): PortWithIcons[] { 64 | const { platforms, category } = urlParams; 65 | let filtered = ports; 66 | 67 | if (platforms.length > 0) { 68 | filtered = filtered.filter((port) => platforms.every((platform) => port.platform.includes(platform))); 69 | } 70 | 71 | if (category) { 72 | filtered = filtered.filter((port) => port.categories.map((c) => c.key).includes(category)); 73 | } 74 | 75 | return filtered; 76 | } 77 | 78 | /** 79 | * Some ports also have userstyles. Unfortunately, the keys, so far, are different for ports and userstyles. 80 | * 81 | * This function will append "(userstyle)" to the userstyle if the port and userstyle are both present. 82 | */ 83 | export function differentiateUserstyles(ports: PortWithIcons[]): PortWithIcons[] { 84 | const portKeys = ports.map((port) => port.key); 85 | const userstylesToPorts = { 86 | mdbook: "mdBook", 87 | "keybr.com": "keybr", 88 | }; 89 | let results = ports; 90 | 91 | for (const [userstyleDir, portSlug] of Object.entries(userstylesToPorts)) { 92 | if (portKeys.includes(userstyleDir) && portKeys.includes(portSlug)) { 93 | const index = portKeys.indexOf(userstyleDir); 94 | results = results.with(index, { ...ports[index], name: `${ports[index].name} (userstyle)` }); 95 | } 96 | } 97 | 98 | return results; 99 | } 100 | 101 | /** 102 | * Scroll to the top of the ports explorer if it's not already in view. 103 | * 104 | * The bottom edge of the ports description is used because the filters 105 | * box has top padding which can cause layout shifts when the user is 106 | * typing in the search box. 107 | */ 108 | export function scrollToTop() { 109 | const portsExplorer = document.getElementById("ports-explorer"); 110 | const portsDescription = document.getElementById("ports-description"); 111 | 112 | if (!portsExplorer || !portsDescription) { 113 | return; 114 | } 115 | 116 | if (portsExplorer.getBoundingClientRect().top < 0) { 117 | window.scrollTo({ 118 | top: window.scrollY + portsDescription.getBoundingClientRect().bottom, 119 | behavior: "auto", 120 | }); 121 | } 122 | } 123 | 124 | export function debounce(deps: () => D, action: () => T, initialValue: T, delay: number): () => T { 125 | let value = $state(initialValue); 126 | let timer: ReturnType | undefined; 127 | 128 | $effect(() => { 129 | deps(); 130 | 131 | if (timer) clearTimeout(timer); 132 | 133 | timer = setTimeout(() => { 134 | value = action(); 135 | }, delay); 136 | 137 | return () => { 138 | if (timer) clearTimeout(timer); 139 | }; 140 | }); 141 | 142 | return () => value; 143 | } 144 | -------------------------------------------------------------------------------- /src/pages/ports/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ports, platforms, categories } from "@data/ports"; 3 | 4 | import Default from "@layouts/Default.astro"; 5 | 6 | import PageIntro from "@components/PageIntro.astro"; 7 | 8 | import PortExplorer from "./_components/PortExplorer.svelte"; 9 | import PortCard from "./_components/PortCard.svelte"; 10 | --- 11 | 12 | 15 | 16 |

17 | Catppuccin provides {ports.length} ports, covering a wide range of applications, tools, websites, 18 | and just about anything you can imagine! 19 |

20 |
21 | 22 | 23 | 28 | 29 | 30 | 31 |
32 | 33 | 53 | -------------------------------------------------------------------------------- /src/pages/rss.xml.js: -------------------------------------------------------------------------------- 1 | import rss from "@astrojs/rss"; 2 | import { getCollection } from "astro:content"; 3 | 4 | import { title } from "./blog/index.astro"; 5 | import { description } from "./blog/index.astro"; 6 | 7 | export async function GET(context) { 8 | const blog = await getCollection("blog"); 9 | return rss({ 10 | title, 11 | description, 12 | site: `${context.site}blog`, 13 | xmlns: { 14 | media: "http://search.yahoo.com/mrss/", 15 | atom: "http://www.w3.org/2005/Atom", 16 | dc: "http://purl.org/dc/elements/1.1/", 17 | }, 18 | customData: ``, 19 | items: blog 20 | .filter((post) => !post.data.draft) 21 | .map((post) => ({ 22 | title: post.data.title, 23 | description: post.data.summary, 24 | pubDate: post.data.datePosted, 25 | link: `/blog/${post.id}/`, 26 | customData: ` 32 | ${post.data.authors.map((author) => `${author.name}`).join("\n")} 33 | `, 34 | })), 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/_buttons.scss: -------------------------------------------------------------------------------- 1 | @use "utils"; 2 | 3 | @keyframes btnFadeOut { 4 | to { 5 | background-color: var(--surface0); 6 | 7 | color: var(--text); 8 | } 9 | } 10 | 11 | .btn-group { 12 | display: flex; 13 | gap: var(--space-xs); 14 | flex-wrap: wrap; 15 | } 16 | 17 | .btn { 18 | @include utils.containerPadding(xs-y); 19 | 20 | border-radius: 9999px; 21 | border: none; 22 | background-color: var(--surface0); 23 | 24 | font-size: 1.6rem; 25 | font-weight: 500; 26 | color: var(--text); 27 | 28 | transition: all 350ms ease-in-out; 29 | cursor: pointer; 30 | 31 | &-small { 32 | @include utils.containerPadding(xxs); 33 | 34 | border-radius: var(--border-radius-normal); 35 | } 36 | 37 | &-transparent { 38 | background-color: transparent; 39 | } 40 | 41 | &-transparent:hover, 42 | &-transparent:focus { 43 | background-color: color-mix(in srgb, transparent, var(--text) 10%); 44 | } 45 | 46 | &-has-icon { 47 | display: flex; 48 | align-items: center; 49 | gap: var(--space-xs); 50 | } 51 | 52 | &-peach { 53 | background-color: var(--peach); 54 | background-image: linear-gradient(120deg, var(--peach), var(--red)); 55 | background-size: 150% 100%; 56 | background-position: top left; 57 | 58 | font-weight: 600; 59 | color: var(--inverted-text); 60 | 61 | &:hover { 62 | background-position: top right; 63 | } 64 | } 65 | 66 | &-mauve { 67 | background-color: var(--mauve); 68 | background-image: linear-gradient(120deg, var(--pink), var(--mauve)); 69 | background-size: 150% 100%; 70 | background-position: top left; 71 | 72 | font-weight: 600; 73 | color: var(--inverted-text); 74 | 75 | &:hover { 76 | background-position: top right; 77 | } 78 | } 79 | 80 | &-green { 81 | background-color: var(--green); 82 | background-image: linear-gradient(120deg, var(--teal), var(--green)); 83 | background-size: 150% 100%; 84 | background-position: top left; 85 | 86 | font-weight: 600; 87 | color: var(--inverted-text); 88 | 89 | &:hover { 90 | background-position: top right; 91 | } 92 | } 93 | 94 | &-blue { 95 | background-color: var(--blue); 96 | background-image: linear-gradient(120deg, var(--blue), var(--sky)); 97 | background-size: 150% 100%; 98 | background-position: top left; 99 | 100 | font-weight: 600; 101 | color: var(--inverted-text); 102 | 103 | &:hover { 104 | background-position: top right; 105 | } 106 | } 107 | 108 | &-success { 109 | background-color: var(--green); 110 | 111 | color: var(--inverted-text); 112 | 113 | animation: btnFadeOut 350ms linear forwards; 114 | animation-delay: 500ms; 115 | } 116 | } 117 | 118 | .btn, 119 | .btn * { 120 | transition: background-position 350ms ease-in-out; 121 | } 122 | -------------------------------------------------------------------------------- /src/styles/_palette.scss: -------------------------------------------------------------------------------- 1 | @use "@catppuccin/palette/scss/catppuccin" as *; 2 | @use "sass:string"; 3 | @use "sass:map"; 4 | 5 | $theme-data: ( 6 | "latte": ( 7 | "scheme": light, 8 | "inverted-text": var(--base), 9 | "selection-color": var(--blue), 10 | ), 11 | "macchiato": ( 12 | "scheme": dark, 13 | "inverted-text": var(--crust), 14 | "selection-color": var(--mauve), 15 | ), 16 | "frappe": ( 17 | "scheme": dark, 18 | "inverted-text": var(--crust), 19 | "selection-color": var(--mauve), 20 | ), 21 | "mocha": ( 22 | "scheme": dark, 23 | "inverted-text": var(--crust), 24 | "selection-color": var(--mauve), 25 | ), 26 | ); 27 | 28 | @mixin theme($flavor) { 29 | @each $color, $value in map.get($palette, $flavor) { 30 | --#{$color}: #{$value}; 31 | } 32 | --inverted-text: #{map.get(map.get($theme-data, $flavor), "inverted-text")}; 33 | --selection-color: #{map.get(map.get($theme-data, $flavor), "selection-color")}; 34 | } 35 | 36 | :where([data-theme="system"]) { 37 | color-scheme: light dark; 38 | @include theme("latte"); 39 | @media (prefers-color-scheme: dark) { 40 | @include theme("mocha"); 41 | } 42 | } 43 | 44 | @each $flavor, $data in $theme-data { 45 | :where([data-theme="#{$flavor}"]) { 46 | color-scheme: map.get($data, "scheme"); 47 | @include theme($flavor); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/styles/_scaffolding.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-size: 62.5%; 9 | 10 | scroll-behavior: smooth; 11 | } 12 | 13 | @media (prefers-reduced-motion) { 14 | *, 15 | *::before, 16 | *::after { 17 | transition: none !important; 18 | } 19 | html { 20 | scroll-behavior: auto; 21 | } 22 | } 23 | 24 | :root { 25 | --website-max-width: 1600px; 26 | 27 | --border-radius-normal: 6px; 28 | --border-radius-large: 12px; 29 | 30 | --base-unit: 1em; 31 | 32 | --space-xxs: calc(0.25 * var(--base-unit)); 33 | --space-xs: calc(0.5 * var(--base-unit)); 34 | --space-sm: calc(0.75 * var(--base-unit)); 35 | --space-md: calc(1.25 * var(--base-unit)); 36 | --space-lg: calc(2 * var(--base-unit)); 37 | --space-xl: calc(3.25 * var(--base-unit)); 38 | --space-xxl: calc(5.25 * var(--base-unit)); 39 | 40 | --padding-body: clamp(20px, 5vw, 500px); 41 | } 42 | 43 | body { 44 | margin: 0; 45 | padding: 0; 46 | 47 | min-height: 100vh; 48 | 49 | background-color: var(--base); 50 | } 51 | 52 | section + section { 53 | margin-block-start: var(--space-xl); 54 | } 55 | 56 | .content-wrapper { 57 | container-type: inline-size; 58 | width: 100%; 59 | max-width: calc(2 * var(--padding-body) + var(--website-max-width)); 60 | padding: var(--space-md) var(--padding-body); 61 | margin-inline: auto; 62 | } 63 | 64 | .no-padding { 65 | padding-block: 0; 66 | } 67 | 68 | .header-container { 69 | padding-block: var(--space-md); 70 | } 71 | 72 | .footer-container { 73 | margin-block-start: var(--space-xl); 74 | 75 | background-color: var(--mantle); 76 | 77 | .content-wrapper { 78 | padding-block: var(--space-lg); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/styles/_tables.scss: -------------------------------------------------------------------------------- 1 | @use "./utils"; 2 | 3 | table { 4 | width: 100%; 5 | min-width: max-content; 6 | overflow-y: scroll; 7 | 8 | text-align: left; 9 | 10 | tr:nth-child(odd):not(:first-child) { 11 | background-color: color-mix(in srgb, var(--mantle), var(--crust) 50%); 12 | } 13 | 14 | th, 15 | td { 16 | @include utils.containerPadding(xs-y); 17 | } 18 | 19 | th + th, 20 | td + td { 21 | padding-inline-start: var(--space-lg); 22 | } 23 | 24 | th { 25 | border-bottom: 1px solid var(--surface0); 26 | 27 | font-size: 80%; 28 | font-weight: 500; 29 | color: var(--subtext0); 30 | } 31 | 32 | tr th:not(:first-child), 33 | tr td:not(:first-child) { 34 | text-align: right; 35 | } 36 | 37 | tbody td { 38 | padding-right: 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | @use "./utils"; 2 | 3 | body { 4 | font-family: 5 | -apple-system, 6 | BlinkMacSystemFont, 7 | avenir next, 8 | avenir, 9 | segoe ui, 10 | helvetica neue, 11 | Cantarell, 12 | Ubuntu, 13 | roboto, 14 | noto, 15 | helvetica, 16 | arial, 17 | sans-serif; 18 | font-size: clamp(15px, 4vw, 2rem); 19 | color: var(--subtext1); 20 | font-weight: 400; 21 | line-height: 1.5; 22 | } 23 | 24 | ::selection { 25 | background-color: color-mix(in srgb, var(--selection-color), transparent 70%); 26 | } 27 | 28 | button, 29 | input, 30 | optgroup, 31 | select, 32 | textarea { 33 | font-family: inherit; 34 | } 35 | 36 | b { 37 | font-weight: 600; 38 | } 39 | 40 | h1 { 41 | font-weight: 900; 42 | line-height: 1.2; 43 | } 44 | h2, 45 | h3, 46 | h4 { 47 | font-weight: 600; 48 | line-height: 1.2; 49 | } 50 | 51 | h1, 52 | h2, 53 | h3, 54 | h4, 55 | h5 { 56 | margin: 0; 57 | padding-block: var(--space-xs) var(--space-xs); 58 | 59 | color: var(--text); 60 | } 61 | 62 | h1 { 63 | font-size: clamp(28px, 6vw, 250%); 64 | } 65 | h2 { 66 | font-size: clamp(24px, 5vw, 175%); 67 | } 68 | h3 { 69 | font-size: clamp(22px, 4.25vw, 125%); 70 | } 71 | h4 { 72 | font-size: clamp(20px, 4vw, 100%); 73 | } 74 | h5 { 75 | font-size: clamp(16px, 3.2vw, 80%); 76 | } 77 | 78 | summary { 79 | h1, 80 | h2, 81 | h3, 82 | h4, 83 | h5, 84 | h6 { 85 | display: inline-block; 86 | 87 | margin: 0; 88 | padding: 0; 89 | } 90 | } 91 | 92 | p, 93 | ul, 94 | ol { 95 | margin-block: 0 var(--space-xs); 96 | } 97 | li { 98 | margin: 0; 99 | } 100 | 101 | a { 102 | text-decoration: none; 103 | 104 | color: var(--blue); 105 | 106 | &:hover, 107 | &:focus { 108 | text-decoration: underline; 109 | } 110 | } 111 | 112 | blockquote { 113 | margin-inline: 0; 114 | margin-block-end: 2em; 115 | padding-inline-start: 2em; 116 | position: relative; 117 | overflow: hidden; 118 | quotes: "\201C" "\201D" "\2018" "\2019"; 119 | 120 | p, 121 | cite { 122 | margin: 0; 123 | } 124 | 125 | &::before, 126 | &::after { 127 | position: absolute; 128 | color: var(--accent-color); 129 | } 130 | 131 | &::before { 132 | content: "\275E"; 133 | left: 0; 134 | font-size: 1.5em; 135 | } 136 | 137 | &::after { 138 | content: ""; 139 | left: 0.6em; 140 | top: 2em; 141 | width: 1px; 142 | height: 100%; 143 | background-color: currentColor; 144 | } 145 | } 146 | 147 | blockquote cite { 148 | display: block; 149 | margin-top: 1em; 150 | font-size: 80%; 151 | } 152 | 153 | code { 154 | padding-inline: var(--space-xxs); 155 | max-width: 100%; 156 | 157 | border-radius: var(--border-radius-normal); 158 | background-color: var(--surface0); 159 | 160 | font-family: monospace; 161 | font-size: 80%; 162 | 163 | overflow-x: scroll; 164 | } 165 | 166 | @media (min-width: 110ch) { 167 | .rehype-heading { 168 | margin-left: -1ch; 169 | } 170 | 171 | .rehype-heading-link::before { 172 | content: "#"; 173 | color: var(--subtext0); 174 | visibility: hidden; 175 | opacity: 0; 176 | transition: opacity 0.3s ease-in-out; 177 | position: relative; 178 | right: 0.6ch; 179 | } 180 | 181 | .rehype-heading:hover .rehype-heading-link::before { 182 | visibility: visible; 183 | opacity: 0.8; 184 | } 185 | } 186 | 187 | .rehype-heading { 188 | padding-block: var(--space-md) var(--space-xs); 189 | } 190 | 191 | .rehype-heading-link { 192 | color: var(--text) !important; 193 | &:hover, 194 | &:focus { 195 | text-decoration: none !important; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | @mixin containerPadding($size: md) { 2 | @if $size == xxs { 3 | padding: var(--space-xxs); 4 | } 5 | @if $size == xxs-y { 6 | padding: var(--space-xxs) var(--space-xs); 7 | } 8 | @if $size == xs { 9 | padding: var(--space-xs); 10 | } 11 | @if $size == xs-y { 12 | padding: var(--space-xs) var(--space-md); 13 | } 14 | @if $size == sm { 15 | padding: var(--space-sm); 16 | } 17 | @if $size == md { 18 | padding: var(--space-md); 19 | } 20 | @if $size == lg { 21 | padding: var(--space-lg); 22 | } 23 | @if $size == xl { 24 | padding: var(--space-xl) var(--space-xxl); 25 | } 26 | } 27 | 28 | @mixin flex($direction: row, $gap: var(--space-md)) { 29 | display: flex; 30 | flex-direction: $direction; 31 | flex-wrap: wrap; 32 | gap: $gap; 33 | } 34 | 35 | @mixin grid($column: 250px, $gap: var(--space-md)) { 36 | display: grid; 37 | grid-template-rows: auto; 38 | grid-template-columns: repeat(auto-fit, minmax($column, 1fr)); 39 | gap: $gap; 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @use "./palette"; 2 | @use "./typography"; 3 | 4 | @use "./scaffolding"; 5 | 6 | @use "./tables"; 7 | @use "./buttons"; 8 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@astrojs/svelte"; 2 | // import path from "node:path"; 3 | // import { fileURLToPath } from "node:url"; 4 | 5 | // const _dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | export default { 8 | preprocess: vitePreprocess({ 9 | style: { 10 | css: { 11 | preprocessorOptions: { 12 | // For some reason, this is causing a "sass [undefined]" error so 13 | // until that's fixed, keep using relative paths in the Svelte style tags 14 | // scss: { 15 | // importer: [ 16 | // (url) => { 17 | // if (url.startsWith("@styles")) { 18 | // return { 19 | // file: url.replace(/^\@styles/, path.join(dirname, "src", "styles")), 20 | // }; 21 | // } 22 | // return url; 23 | // }, 24 | // ], 25 | // }, 26 | }, 27 | }, 28 | }, 29 | }), 30 | }; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "strictNullChecks": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@components/*": ["src/components/*"], 10 | "@layouts/*": ["src/layouts/*"], 11 | "@data/*": ["src/data/*"], 12 | "@styles/*": ["src/styles/*"] 13 | }, 14 | "esModuleInterop": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { getViteConfig } from "astro/config"; 3 | 4 | export default getViteConfig({ 5 | test: { 6 | include: ["src\/**\/*.test.ts"], 7 | }, 8 | }); 9 | --------------------------------------------------------------------------------