Standard dependency updates (#1)
All checks were successful
Deploy / deploy-production (push) Successful in 1m35s
Deploy / deploy-preview (push) Has been skipped
Deploy / cleanup-preview (push) Has been skipped

Reviewed-on: #1
Co-authored-by: Zeke Abshire <zekeabshire@gmail.com>
Co-committed-by: Zeke Abshire <zekeabshire@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-04-11 19:47:19 -05:00
committed by Zyrrus
parent 065ef3a61f
commit 3f632c98f3
19 changed files with 5368 additions and 6718 deletions

View File

@@ -1,15 +1,20 @@
import tseslint from "typescript-eslint";
import nextVitals from "eslint-config-next/core-web-vitals";
/** @type {import("eslint").Linter.Config} */
const config = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
...nextVitals,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
},
},
plugins: {
"@typescript-eslint": tseslint.plugin,
},
plugins: ["@typescript-eslint"],
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
],
rules: {
// These opinionated rules are enabled in stylistic-type-checked above.
// Feel free to reconfigure them to your own preference.

View File

@@ -34,7 +34,7 @@ jobs:
bash .gitea/scripts/pangolin-upsert.sh \
--subdomain "${{ vars.PROD_SUBDOMAIN }}" \
--port "${{ vars.PROD_PORT }}" \
--resource-name "${{ vars.APP_NAME }}-production"
--resource-name "${{ vars.APP_NAME }}-production" \
--target-ip "${{ secrets.PANGOLIN_TARGET_IP }}"
env:
PANGOLIN_API_URL: ${{ secrets.PANGOLIN_API_URL }}
@@ -63,11 +63,11 @@ jobs:
docker build \
--build-arg NEXT_PUBLIC_APP_ENV=preview \
-t ${{ vars.APP_NAME }}:${{ steps.slug.outputs.slug }} \
-f Dockerfile .
-f dockerfile .
- name: Deploy preview container
run: |
bash scripts/deploy.sh \
bash .gitea/scripts/deploy.sh \
--name "${{ vars.APP_NAME }}" \
--tag "${{ steps.slug.outputs.slug }}" \
--port "${{ steps.slug.outputs.port }}" \
@@ -75,10 +75,11 @@ jobs:
- name: Register Pangolin preview resource
run: |
bash scripts/pangolin-upsert.sh \
bash .gitea/scripts/pangolin-upsert.sh \
--subdomain "${{ steps.slug.outputs.slug }}.${{ vars.APP_NAME }}" \
--port "${{ steps.slug.outputs.port }}" \
--resource-name "${{ vars.APP_NAME }}-${{ steps.slug.outputs.slug }}"
--resource-name "${{ vars.APP_NAME }}-${{ steps.slug.outputs.slug }}" \
--target-ip "${{ secrets.PANGOLIN_TARGET_IP }}"
env:
PANGOLIN_API_URL: ${{ secrets.PANGOLIN_API_URL }}
PANGOLIN_API_KEY: ${{ secrets.PANGOLIN_API_KEY }}
@@ -103,7 +104,7 @@ jobs:
- name: Remove Pangolin resource
run: |
bash scripts/pangolin-delete.sh \
bash .gitea/scripts/pangolin-delete.sh \
--resource-name "${{ vars.APP_NAME }}-${{ steps.slug.outputs.slug }}"
env:
PANGOLIN_API_URL: ${{ secrets.PANGOLIN_API_URL }}

View File

@@ -5,14 +5,10 @@
await import("./src/env.mjs");
import createMDX from "@next/mdx";
import rehypePrettyCode from "rehype-pretty-code";
import remarkFrontmatter from "remark-frontmatter";
import remarkGfm from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
/** @type {import("next").NextConfig} */
const config = {
output: 'standalone',
output: "standalone",
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
images: {
remotePatterns: [
@@ -39,9 +35,13 @@ const rehypePrettyCodeOptions = {
const withMDX = createMDX({
options: {
remarkPlugins: [remarkGfm, remarkFrontmatter, remarkMdxFrontmatter],
remarkPlugins: [
"remark-gfm",
"remark-frontmatter",
"remark-mdx-frontmatter",
],
// @ts-ignore
rehypePlugins: [[rehypePrettyCode, rehypePrettyCodeOptions]],
rehypePlugins: [["rehype-pretty-code", rehypePrettyCodeOptions]],
},
});

11356
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"name": "zyrrus-website",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"db:push": "dotenv drizzle-kit push:mysql",
@@ -11,80 +12,77 @@
"start": "next start"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@formkit/auto-animate": "^0.8.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@formkit/auto-animate": "^0.9.0",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"@next/mdx": "^13.5.6",
"@planetscale/database": "^1.11.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@t3-oss/env-nextjs": "^0.7.0",
"@tanstack/react-query": "^4.32.6",
"@trpc/client": "^10.37.1",
"@trpc/next": "^10.37.1",
"@trpc/react-query": "^10.37.1",
"@trpc/server": "^10.37.1",
"@types/mdx": "^2.0.9",
"@vercel/analytics": "^1.1.1",
"class-variance-authority": "^0.7.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@t3-oss/env-nextjs": "^0.13.11",
"@tanstack/react-query": "^5.99.0",
"@trpc/client": "^11.16.0",
"@trpc/next": "^11.16.0",
"@trpc/react-query": "^11.16.0",
"@trpc/server": "^11.16.0",
"@types/mdx": "^2.0.13",
"class-variance-authority": "^0.7.1",
"clsx": "^2.0.0",
"cmdk": "^1.0.0",
"drizzle-orm": "^0.28.5",
"cmdk": "^1.1.1",
"drizzle-orm": "^0.45.2",
"exifr": "^7.1.3",
"framer-motion": "^10.16.4",
"lucide-react": "^0.288.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.8.0",
"mini-svg-data-uri": "^1.4.4",
"next": "^14.2.3",
"next-mdx-remote": "^4.4.1",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.11.0",
"rehype-pretty-code": "^0.10.2",
"next": "^16.2.3",
"next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-icons": "^5.6.0",
"rehype-pretty-code": "^0.14.3",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
"remark-mdx-frontmatter": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.2.0",
"seedrandom": "^3.0.5",
"sharp": "^0.32.6",
"shiki": "^0.14.5",
"superjson": "^1.13.1",
"swiper": "^11.0.7",
"tailwind-merge": "^1.14.0",
"sharp": "^0.34.5",
"shiki": "^4.0.2",
"superjson": "^2.2.6",
"swiper": "^12.1.3",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"@types/eslint": "^8.44.2",
"@types/node": "^18.16.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/seedrandom": "^3.0.7",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"autoprefixer": "^10.4.14",
"dotenv-cli": "^7.3.0",
"drizzle-kit": "^0.19.13",
"eslint": "^8.47.0",
"eslint-config-next": "^13.5.4",
"mysql2": "^3.6.1",
"postcss": "^8.4.27",
"prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.5.1",
"tailwindcss": "^3.3.3",
"typescript": "^5.1.6"
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19",
"@types/eslint": "^9.6.1",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/seedrandom": "^3.0.8",
"@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/parser": "^8.58.1",
"autoprefixer": "^10.4.27",
"dotenv-cli": "^11.0.0",
"eslint": "^10.2.0",
"eslint-config-next": "^16.2.3",
"postcss": "^8.5.9",
"prettier": "^3.8.2",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2"
},
"ct3aMetadata": {
"initVersion": "7.22.0"

View File

@@ -1,7 +1,6 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
};

1
src/additional.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "swiper/css";

View File

@@ -118,19 +118,17 @@ const MotionCell = ({
width={end.width}
height={end.height}
rx={radius}
// Need to set initial position to start well
initial
x={start.x}
y={start.y}
// Setting origin to (0, 0) fixes rotation pivot point
style={{ originX: 0, originY: 0 }}
// Translation is relative to the initial (x, y)
// Start in position immediately
initial={{ x: start.x, y: start.y, rotate: rotation }}
// Use absolute positions so clipPath rects and shadow rects stay aligned
animate={
shouldReduceMotion
? { rotate: [rotation] }
? { x: start.x, y: start.y, rotate: rotation }
: {
x: [0, end.x - start.x],
y: [0, end.y - start.y],
x: [start.x, end.x],
y: [start.y, end.y],
rotate: [rotation],
}
}

View File

@@ -11,7 +11,6 @@ import {
TooltipTrigger,
} from "~/app/components/ui/tooltip";
import { type IconType } from "react-icons/lib";
import { SidebarCommand } from "~/app/components/navigation/sidebar-command";
import { ThemeToggle } from "~/app/components/theme/theme-toggle";
import { Button } from "~/app/components/ui/button";
import { type PropsWithChildren } from "react";
@@ -62,22 +61,22 @@ const links: {
{
tooltip: "Home",
href: "/",
icon: GoHome,
icon: GoHome as IconType,
},
{
tooltip: "About",
href: "/about",
icon: GoPerson,
icon: GoPerson as IconType,
},
{
tooltip: "Resources",
href: "/resources",
icon: GoTerminal,
icon: GoTerminal as IconType,
},
{
tooltip: "Writing",
href: "/writing",
icon: GoPencil,
icon: GoPencil as IconType,
},
];

View File

@@ -1,8 +1,8 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@@ -1,6 +1,5 @@
import "~/styles/globals.css";
import { Analytics } from "@vercel/analytics/react";
import { Inter } from "next/font/google";
import { headers } from "next/headers";
import { TRPCReactProvider } from "~/trpc/react";
@@ -19,13 +18,14 @@ export const metadata = {
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default function RootLayout({ children }: PropsWithChildren) {
export default async function RootLayout({ children }: PropsWithChildren) {
const hdrs = await headers();
return (
<html lang="en">
<body
className={`font-sans ${inter.variable} relative bg-neutral-50 text-primary bg-dot-neutral-400/[0.2] selection:bg-accent selection:text-neutral-900 dark:bg-neutral-800 dark:bg-dot-neutral-600/[0.2]`}
>
<TRPCReactProvider headers={headers()}>
<TRPCReactProvider headers={hdrs}>
<ThemeProvider
attribute="class"
defaultTheme="system"
@@ -35,7 +35,6 @@ export default function RootLayout({ children }: PropsWithChildren) {
{children}
<Toaster />
</ThemeProvider>
<Analytics />
</TRPCReactProvider>
</body>
</html>

View File

@@ -1,6 +1,6 @@
"use client";
import React from "react";
import React, { RefObject } from "react";
import { X, Trash2 } from "lucide-react";
import { MultiSelect } from "~/app/components/multiselect";
import { Badge } from "~/app/components/ui/badge";
@@ -184,7 +184,7 @@ const SelectedTimeZones = ({
interface TimeZoneViewerProps {
timeZones: Option[];
scrollRef: React.RefObject<HTMLDivElement>;
scrollRef: RefObject<HTMLDivElement | null>;
startTimeString: string;
endTimeString?: string;
times: string[];

View File

@@ -0,0 +1,79 @@
import plugin from "tailwindcss/plugin";
import svgToDataUri from "mini-svg-data-uri";
interface ColorPalette {
[key: string]: string | ColorPalette;
}
const flattenColorPalette = (
colors: ColorPalette | null | undefined,
): Record<string, string> => {
if (!colors) return {};
const flattened: Record<string, string> = {};
Object.entries(colors).forEach(([color, values]) => {
if (typeof values === "object") {
const nestedFlatten = flattenColorPalette(values);
Object.entries(nestedFlatten).forEach(([number, hex]) => {
flattened[`${color}${number === "DEFAULT" ? "" : `-${number}`}`] = hex;
});
} else {
flattened[color] = values;
}
});
return flattened;
};
export default plugin(function ({ matchComponents, matchUtilities, theme }) {
// Display text background headings
matchComponents({
"display-text": (value) => ({
position: "relative",
"&::before": {
content: value,
position: "absolute",
fontWeight: "800",
fontSize: "min(12.5rem, calc(1.5rem + 10vw))",
lineHeight: "1",
letterSpacing: "0.09em",
left: "clamp(-17.5rem, calc(-43vw + 20rem), 0px)",
top: "0",
transform: "translateY(-50%)",
zIndex: "-30",
color: "var(--color-neutral-150)",
},
":is(.dark, .dark *) &::before": {
color: "var(--color-neutral-750)",
},
}),
});
// Background pattern utilities (grids + dots)
matchUtilities(
{
"bg-grid": (value: string) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
)}")`,
}),
"bg-grid-small": (value: string) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="8" height="8" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
)}")`,
}),
"bg-dot": (value: string) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16" fill="none"><circle fill="${value}" id="pattern-circle" cx="10" cy="10" r="1.6257413380501518"></circle></svg>`,
)}")`,
}),
},
{
values: flattenColorPalette(
theme("backgroundColor") as ColorPalette | null | undefined,
),
type: "color",
},
);
});

View File

@@ -1,3 +1,149 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "tailwindcss-animate";
@plugin "../plugins/custom-utilities.ts";
@source "../**/*.{ts,tsx,mdx}";
/* Class-based dark mode (for next-themes) */
@variant dark (&:where(.dark, .dark *));
/* ── Theme ────────────────────────────────────────────────────────── */
@theme {
--font-sans: "InterVariable", "Inter", ui-sans-serif, system-ui,
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--breakpoint-sm: 588px;
--color-accent: #77aca9;
--color-neutral-*: initial;
--color-neutral-50: #fefefe;
--color-neutral-100: #f8f8f8;
--color-neutral-150: #f4f4f4;
--color-neutral-200: #e5e5e5;
--color-neutral-300: #d4d4d4;
--color-neutral-400: #a3a3a3;
--color-neutral-500: #737373;
--color-neutral-600: #525252;
--color-neutral-700: #404040;
--color-neutral-750: #333333;
--color-neutral-800: #262626;
--color-neutral-850: #222222;
--color-neutral-900: #202020;
--color-neutral-950: #131313;
--color-gruv-red-fg: #88403c;
--color-gruv-red-bg: #ea6962;
--color-gruv-orange-fg: #855231;
--color-gruv-orange-bg: #e78a4e;
--color-gruv-yellow-fg: #795f34;
--color-gruv-yellow-bg: #d8a657;
--color-gruv-green-fg: #585e36;
--color-gruv-green-bg: #a9b665;
--color-gruv-blue-fg: #405852;
--color-gruv-blue-bg: #7daea3;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--duration-2000: 2000ms;
--shadow-cutout-depth-0: 0px 25px 15px 0px rgba(0, 0, 0, 0.15) inset;
--shadow-cutout-depth-1: 0px 25px 15px 0px rgba(0, 0, 0, 0.15) inset,
0px 48px 15px 0px rgba(0, 0, 0, 0.05) inset;
--ease-bounce-up: cubic-bezier(0.5, 2.5, 0.7, 0.7);
}
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
/* ── Container override ───────────────────────────────────────────── */
@utility container {
width: 100%;
margin-inline: auto;
padding-inline: 1.5rem;
@media (width >= 588px) {
max-width: 588px;
}
}
/* ── Semantic color utilities ─────────────────────────────────────── */
@utility text-primary {
color: var(--color-neutral-900);
.dark & {
color: var(--color-neutral-50);
}
}
@utility text-secondary {
color: var(--color-neutral-500);
.dark & {
color: var(--color-neutral-400);
}
}
@utility bg-0 {
background-color: var(--color-neutral-50);
.dark & {
background-color: var(--color-neutral-800);
}
}
@utility bg-1 {
background-color: var(--color-neutral-100);
.dark & {
background-color: var(--color-neutral-850);
}
}
@utility bg-2 {
background-color: var(--color-neutral-150);
.dark & {
background-color: var(--color-neutral-900);
}
}
/* ── Typography / prose overrides ─────────────────────────────────── */
.prose code::before,
.prose code::after {
content: none;
}
.prose code:not(:is(pre code)) {
padding: 0.2em 0.4em;
white-space: break-spaces;
font-size: 85%;
font-weight: 400;
border-radius: 6px;
color: var(--color-neutral-700);
background-color: var(--color-neutral-200);
}
.dark .prose code:not(:is(pre code)) {
color: var(--color-neutral-300);
background-color: var(--color-neutral-950);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { loggerLink, httpBatchStreamLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { useState } from "react";
@@ -18,14 +18,14 @@ export function TRPCReactProvider(props: {
const [trpcClient] = useState(() =>
api.createClient({
transformer,
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
httpBatchStreamLink({
transformer,
url: getUrl(),
headers() {
const heads = new Map(props.headers);

View File

@@ -1,25 +1,26 @@
import {
createTRPCProxyClient,
createTRPCClient,
loggerLink,
unstable_httpBatchStreamLink,
httpBatchStreamLink,
} from "@trpc/client";
import { headers } from "next/headers";
import { type AppRouter } from "~/server/api/root";
import { getUrl, transformer } from "./shared";
export const api = createTRPCProxyClient<AppRouter>({
transformer,
export const api = createTRPCClient<AppRouter>({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
httpBatchStreamLink({
transformer,
url: getUrl(),
headers() {
const heads = new Map(headers());
async headers() {
const hdrs = await headers();
const heads = new Map(hdrs);
heads.set("x-trpc-source", "rsc");
return Object.fromEntries(heads);
},

View File

@@ -3,10 +3,18 @@ import path from "path";
import { compileMDX } from "next-mdx-remote/rsc";
import { type PostFrontmatter } from "~/utils/server/mdx/types";
const rootDir = path.join(process.cwd(), "src", "app", "(home)", "writing", "(posts)");
const rootDir = path.join(
process.cwd(),
"src",
"app",
"(home)",
"writing",
"(posts)",
);
const isValidMdxPage = (dirent: Dirent) => {
const { name: fileName, path: filePath } = dirent;
const fileName = dirent.name;
const filePath = dirent.parentPath;
const subFiles = fs.readdirSync(path.join(filePath, fileName), {
withFileTypes: true,

View File

@@ -1,217 +1,3 @@
import defaultTheme from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin";
import { type PluginAPI } from "tailwindcss/types/config";
import type { Config } from "tailwindcss";
import svgToDataUri from "mini-svg-data-uri";
export default {
darkMode: "class",
content: ["./src/**/*.{ts,tsx,mdx}"],
theme: {
fontFamily: {
sans: ["InterVariable", "Inter", ...defaultTheme.fontFamily.sans],
},
container: {
center: true,
padding: "1.5rem",
screens: {
sm: "588px",
},
},
extend: {
screens: {
sm: "588px",
},
colors: {
accent: "#77ACA9", // Selection
neutral: {
50: "#fefefe", // (Dark theme) primary text | (Light theme) bg
100: "#f8f8f8", // (Light theme) card depth 0, display text
150: "#f4f4f4", // (Light theme) display text, card depth 1,
200: "#e5e5e5", // (Light theme) borders, code bg
300: "#d4d4d4", // (Dark theme) code text
400: "#a3a3a3", // (Dark theme) secondary text, icons
500: "#737373",
600: "#525252", // (Light theme) secondary text, icons
700: "#404040", // (Dark theme) borders, display text (25%) | (Light theme) code text
750: "#333333", // (Dark theme) card highlight
800: "#262626", // (Light theme) primary button fill | (Dark theme) bg
850: "#222222", // (Dark theme) card depth 0
900: "#202020", // (Dark theme) card depth 1 | (Light theme) primary text
950: "#131313", // (Dark theme) code bg
},
"gruv-red": {
fg: "#88403c",
bg: "#EA6962",
},
"gruv-orange": {
fg: "#855231",
bg: "#E78A4E",
},
"gruv-yellow": {
fg: "#795F34",
bg: "#D8A657",
},
"gruv-green": {
fg: "#585E36",
bg: "#A9B665",
},
"gruv-blue": {
fg: "#405852",
bg: "#7DAEA3",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
transitionDuration: {
"2000": "2000ms",
},
boxShadow: {
"cutout-depth-0": "0px 25px 15px 0px rgba(0, 0, 0, 0.15) inset",
"cutout-depth-1":
"0px 25px 15px 0px rgba(0, 0, 0, 0.15) inset, 0px 48px 15px 0px rgba(0, 0, 0, 0.05) inset",
},
transitionTimingFunction: {
"bounce-up": "cubic-bezier(0.5, 2.5, 0.7, 0.7)",
},
typography(theme: PluginAPI["theme"]) {
return {
DEFAULT: {
css: {
"code::before": { content: "none" },
"code::after": { content: "none" },
// Inline code blocks
"code:not(:is(pre code))": {
padding: "0.2em 0.4em",
whiteSpace: "break-spaces",
fontSize: "85%",
fontWeight: "400",
borderRadius: "6px",
// Light theme
color: theme("colors.neutral.700"),
backgroundColor: theme("colors.neutral.200"),
},
".dark code:not(:is(pre code))": {
color: theme("colors.neutral.300"),
backgroundColor: theme("colors.neutral.950"),
},
},
},
};
},
},
},
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
plugin(function ({ matchComponents, addUtilities, theme }) {
matchComponents({
"display-text": (value) => ({
position: "relative",
"&:before": {
content: value,
position: "absolute",
fontWeight: "800",
fontSize: "min(12.5rem, calc(1.5rem + 10vw))",
lineHeight: "1",
letterSpacing: "0.09em",
left: "clamp(-17.5rem, calc(-43vw + 20rem), 0px)",
top: "0",
transform: "translateY(-50%)",
zIndex: "-30",
"@apply text-neutral-150": {},
"@apply dark:text-neutral-750": {},
},
}),
});
addUtilities({
".text-primary": {
color: theme("colors.neutral.900"),
"@apply dark:text-neutral-50": {},
},
".text-secondary": {
color: theme("colors.neutral.500"),
"@apply dark:text-neutral-400": {},
},
// Background elevation surface (0 is base, 1 is card, ...)
".bg-0": {
backgroundColor: theme("colors.neutral.50"),
"@apply dark:bg-neutral-800": {},
},
".bg-1": {
backgroundColor: theme("colors.neutral.100"),
"@apply dark:bg-neutral-850": {},
},
".bg-2": {
backgroundColor: theme("colors.neutral.150"),
"@apply dark:bg-neutral-900": {},
},
});
}),
plugin(function ({ matchUtilities, theme }) {
matchUtilities(
{
"bg-grid": (value: string) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
)}")`,
}),
"bg-grid-small": (value: string) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="8" height="8" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
)}")`,
}),
"bg-dot": (value: string) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16" fill="none"><circle fill="${value}" id="pattern-circle" cx="10" cy="10" r="1.6257413380501518"></circle></svg>`,
)}")`,
}),
},
{
values: flattenColorPalette(theme("backgroundColor")),
type: "color",
},
);
}),
],
} satisfies Config;
// Background Grids + Dots
interface ColorPalette {
[key: string]: string | ColorPalette;
}
const flattenColorPalette = (
colors: ColorPalette | null | undefined,
): Record<string, string> => {
if (!colors) return {};
const flattened: Record<string, string> = {};
Object.entries(colors).forEach(([color, values]) => {
if (typeof values === "object") {
const nestedFlatten = flattenColorPalette(values);
Object.entries(nestedFlatten).forEach(([number, hex]) => {
flattened[`${color}${number === "DEFAULT" ? "" : `-${number}`}`] = hex;
});
} else {
flattened[color] = values;
}
});
return flattened;
};
export default {} satisfies Config;

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"target": "es2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
@@ -10,17 +14,22 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
"~/*": [
"./src/*"
]
},
"plugins": [{ "name": "next" }]
"plugins": [
{
"name": "next"
}
]
},
"include": [
".eslintrc.cjs",
@@ -30,7 +39,10 @@
"**/*.cjs",
"**/*.mjs",
"**/*.mdx",
".next/types/**/*.ts"
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules"
]
}