Compare commits

...

33 Commits

Author SHA1 Message Date
dependabot[bot]
3285553419 Bump diff from 5.2.0 to 5.2.2 in /src
Bumps [diff](https://github.com/kpdecker/jsdiff) from 5.2.0 to 5.2.2.
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/v5.2.0...v5.2.2)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 5.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 19:07:12 +00:00
95081b8b77 Omit drafts from build 2025-11-11 09:28:59 -08:00
40b6359d8f Add public pgp key; update astro 2025-11-11 09:19:03 -08:00
d61080722d push latest 2025-09-15 10:38:26 -07:00
5117218a1a Fix code block formatting on mobile devices 2025-09-15 10:04:35 -07:00
f355373ba1 For now sunset resources work 2025-09-11 08:55:05 -07:00
384cb82efb create 0101-welcome-to-the-terminal 2025-08-27 12:46:17 -07:00
7ff6f6542b create video player 2025-08-27 11:19:18 -07:00
9ad08dc85d background animations: 2025-08-27 08:53:56 -07:00
12631dbd42 Animations 2025-08-27 08:27:22 -07:00
1758dc3153 Fixed 2025-08-22 23:08:39 -07:00
9496030d41 Broken 2025-08-21 22:53:37 -07:00
30f264a6bb Thinking of ways to build out a presentation system 2025-08-21 22:18:04 -07:00
7992fcbd49 Add a resources layout 2025-08-18 13:28:55 -07:00
60a9fb0339 back at it 2025-08-16 13:57:37 -07:00
6711de5eb6 Update mdx command component to conform to mobile container size 2025-04-23 12:26:34 -07:00
f2b4660300 keep working on coreboot guide 2025-04-23 12:00:44 -07:00
97608e983c update readme 2025-04-22 13:29:29 -07:00
ce812e8466 Add commands mdx component; continue work on coreboot post 2025-04-22 12:20:19 -07:00
d44988b39c Set comment feed to load eagerly 2025-04-22 09:26:43 -07:00
d885ea4e6b Fix requestAnimationFrame blurring background after switching views 2025-04-22 09:07:47 -07:00
6cfa4c5b7d Remove cursor; update background 2025-04-22 09:04:21 -07:00
f2e85dc6d8 Add cursor trail; fix giscuss 2025-04-21 14:45:44 -07:00
fce17d397e hide custom cursor om mobile devices 2025-04-21 14:26:17 -07:00
7cc954ae07 Add custom cursor; improve pointer events 2025-04-21 14:15:08 -07:00
c6aa014d29 Fix content typography sizes 2025-04-21 13:25:48 -07:00
a9cbbb7e8e Update dockerfile 2025-04-21 12:31:57 -07:00
788eb84488 Update entrypoint to support ssr 2025-04-21 12:26:19 -07:00
4fc5a07249 Update Caddyfile.release 2025-04-21 12:21:15 -07:00
aca5d53bd1 fix proxy issue 2025-04-21 12:17:57 -07:00
f1af80afaf Update compose 2025-04-21 12:14:40 -07:00
257000e81d Update container name 2025-04-21 12:13:32 -07:00
8b30228c4a update compose 2025-04-21 12:10:48 -07:00
29 changed files with 1833 additions and 859 deletions

View File

@@ -35,14 +35,13 @@ RUN pnpm run build
FROM node:22-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
# Install serve
RUN npm install -g http-server
# Copy built files # Copy built files
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# Expose port 3000 # Expose port 3000
EXPOSE 3000 EXPOSE 3000
# Deployment command # Deployment command
CMD ["http-server", "dist", "-a", "127.0.0.1", "-p", "3000"] CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,6 +1,6 @@
PROJECT_NAME := "timmypidashev.dev" PROJECT_NAME := "timmypidashev.dev"
PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>" PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>"
PROJECT_VERSION := "v1.0.2" PROJECT_VERSION := "v2.1.1"
PROJECT_LICENSE := "MIT" PROJECT_LICENSE := "MIT"
PROJECT_SOURCES := "https://github.com/timmypidashev/web" PROJECT_SOURCES := "https://github.com/timmypidashev/web"
PROJECT_REGISTRY := "ghcr.io/timmypidashev" PROJECT_REGISTRY := "ghcr.io/timmypidashev"

View File

@@ -1 +1,3 @@
![Badge](https://hitscounter.dev/api/hit?url=https%3A%2F%2Ftimmypidashev.dev&label=Visits&icon=eye-fill&color=%23198754)
<img src=".github/preview.jpeg" title="Preview"/> <img src=".github/preview.jpeg" title="Preview"/>

View File

@@ -21,7 +21,7 @@ services:
command: --interval 120 --cleanup --label-enable command: --interval 120 --cleanup --label-enable
timmypidashev.dev: timmypidashev.dev:
container_name: timmypidashev container_name: timmypidashev.dev
image: ghcr.io/timmypidashev/timmypidashev.dev:latest image: ghcr.io/timmypidashev/timmypidashev.dev:latest
networks: networks:
- proxy_network - proxy_network

View File

@@ -10,6 +10,10 @@ import sitemap from "@astrojs/sitemap";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
output: "server", output: "server",
server: {
host: true,
port: 3000,
},
adapter: node({ adapter: node({
mode: "standalone", mode: "standalone",
}), }),

View File

@@ -8,32 +8,36 @@
"preview": "astro preview" "preview": "astro preview"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/react": "^4.2.4", "@astrojs/react": "^4.4.0",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.3.20", "@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6", "@types/react-dom": "^18.3.6",
"astro": "^5.7.4", "astro": "^5.14.1",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.2.4", "@astrojs/mdx": "^4.3.6",
"@astrojs/node": "^9.2.0", "@astrojs/node": "^9.4.4",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.3.0", "@astrojs/sitemap": "^3.6.0",
"@giscus/react": "^3.1.0", "@giscus/react": "^3.1.0",
"@pilcrowjs/object-parser": "^0.0.4", "@pilcrowjs/object-parser": "^0.0.4",
"@react-hook/intersection-observer": "^3.1.2", "@react-hook/intersection-observer": "^3.1.2",
"@rehype-pretty/transformers": "^0.13.2",
"arctic": "^3.6.0", "arctic": "^3.6.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"marked": "^15.0.8", "marked": "^15.0.8",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-responsive": "^10.0.1", "react-responsive": "^10.0.1",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.1", "rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"schema-dts": "^1.1.5", "schema-dts": "^1.1.5",
"typewriter-effect": "^2.21.0" "shiki": "^3.12.2",
"typewriter-effect": "^2.21.0",
"unist-util-visit": "^5.0.0"
} }
} }

1115
src/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

65
src/public/pgp.asc Normal file
View File

@@ -0,0 +1,65 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=T4Wx
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -18,6 +18,8 @@ interface Cell {
transitioning: boolean; transitioning: boolean;
transitionComplete: boolean; transitionComplete: boolean;
rippleEffect: number; // For ripple animation rippleEffect: number; // For ripple animation
rippleStartTime: number; // When ripple started
rippleDistance: number; // Distance from ripple center
} }
interface Grid { interface Grid {
@@ -42,16 +44,19 @@ interface BackgroundProps {
position?: 'left' | 'right'; position?: 'left' | 'right';
} }
const CELL_SIZE = 25; const CELL_SIZE_MOBILE = 15;
const CELL_SIZE_DESKTOP = 25;
const TARGET_FPS = 60; // Target frame rate
const CYCLE_TIME = 3000; // 3 seconds per full cycle, regardless of FPS
const TRANSITION_SPEED = 0.05; const TRANSITION_SPEED = 0.05;
const SCALE_SPEED = 0.05; const SCALE_SPEED = 0.05;
const CYCLE_FRAMES = 180;
const INITIAL_DENSITY = 0.15; const INITIAL_DENSITY = 0.15;
const SIDEBAR_WIDTH = 240; const SIDEBAR_WIDTH = 240;
const MOUSE_INFLUENCE_RADIUS = 150; // Radius of mouse influence in pixels const MOUSE_INFLUENCE_RADIUS = 150; // Radius of mouse influence in pixels
const COLOR_SHIFT_AMOUNT = 30; // Maximum color shift amount const COLOR_SHIFT_AMOUNT = 30; // Maximum color shift amount
const RIPPLE_SPEED = 0.2; // Speed of ripple propagation const RIPPLE_SPEED = 0.02; // Speed of ripple propagation
const ELEVATION_FACTOR = 15; // Max height for 3D effect const RIPPLE_ELEVATION_FACTOR = 4; // Height of ripple wave
const ELEVATION_FACTOR = 8; // Max height for 3D effect - reduced for more subtle effect
const Background: React.FC<BackgroundProps> = ({ const Background: React.FC<BackgroundProps> = ({
layout = 'index', layout = 'index',
@@ -60,7 +65,8 @@ const Background: React.FC<BackgroundProps> = ({
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const gridRef = useRef<Grid>(); const gridRef = useRef<Grid>();
const animationFrameRef = useRef<number>(); const animationFrameRef = useRef<number>();
const frameCount = useRef(0); const lastUpdateTimeRef = useRef<number>(0);
const lastCycleTimeRef = useRef<number>(0);
const resizeTimeoutRef = useRef<NodeJS.Timeout>(); const resizeTimeoutRef = useRef<NodeJS.Timeout>();
const mouseRef = useRef<MousePosition>({ const mouseRef = useRef<MousePosition>({
x: -1000, x: -1000,
@@ -83,11 +89,18 @@ const Background: React.FC<BackgroundProps> = ({
return colors[Math.floor(Math.random() * colors.length)]; return colors[Math.floor(Math.random() * colors.length)];
}; };
const getCellSize = () => {
// Check if we're on mobile based on screen width
const isMobile = window.innerWidth <= 768;
return isMobile ? CELL_SIZE_MOBILE : CELL_SIZE_DESKTOP;
};
const calculateGridDimensions = (width: number, height: number) => { const calculateGridDimensions = (width: number, height: number) => {
const cols = Math.floor(width / CELL_SIZE); const cellSize = getCellSize();
const rows = Math.floor(height / CELL_SIZE); const cols = Math.floor(width / cellSize);
const offsetX = Math.floor((width - (cols * CELL_SIZE)) / 2); const rows = Math.floor(height / cellSize);
const offsetY = Math.floor((height - (rows * CELL_SIZE)) / 2); const offsetX = Math.floor((width - (cols * cellSize)) / 2);
const offsetY = Math.floor((height - (rows * cellSize)) / 2);
return { cols, rows, offsetX, offsetY }; return { cols, rows, offsetX, offsetY };
}; };
@@ -114,7 +127,9 @@ const Background: React.FC<BackgroundProps> = ({
targetElevation: 0, targetElevation: 0,
transitioning: false, transitioning: false,
transitionComplete: false, transitionComplete: false,
rippleEffect: 0 rippleEffect: 0,
rippleStartTime: 0,
rippleDistance: 0
}; };
}) })
); );
@@ -224,7 +239,7 @@ const Background: React.FC<BackgroundProps> = ({
}; };
const createRippleEffect = (grid: Grid, centerX: number, centerY: number) => { const createRippleEffect = (grid: Grid, centerX: number, centerY: number) => {
const maxDistance = Math.max(grid.cols, grid.rows) / 2; const currentTime = Date.now();
for (let i = 0; i < grid.cols; i++) { for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
@@ -237,15 +252,9 @@ const Background: React.FC<BackgroundProps> = ({
// Only apply ripple to visible cells // Only apply ripple to visible cells
if (cell.opacity > 0.1) { if (cell.opacity > 0.1) {
// Delayed animation based on distance from center cell.rippleStartTime = currentTime + distance * 100; // Delayed start based on distance
setTimeout(() => { cell.rippleDistance = distance;
cell.rippleEffect = 1; // Start ripple cell.rippleEffect = 0;
// After a short time, reset ripple
setTimeout(() => {
cell.rippleEffect = 0;
}, 300 + distance * 50);
}, distance * 100);
} }
} }
} }
@@ -272,51 +281,51 @@ const Background: React.FC<BackgroundProps> = ({
} }
}; };
const updateCellAnimations = (grid: Grid) => { const updateCellAnimations = (grid: Grid, deltaTime: number) => {
const mouseX = mouseRef.current.x; const mouseX = mouseRef.current.x;
const mouseY = mouseRef.current.y; const mouseY = mouseRef.current.y;
const cellSize = getCellSize();
// Adjust transition speeds based on time
const transitionFactor = TRANSITION_SPEED * (deltaTime / (1000 / TARGET_FPS));
const scaleFactor = SCALE_SPEED * (deltaTime / (1000 / TARGET_FPS));
for (let i = 0; i < grid.cols; i++) { for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; const cell = grid.cells[i][j];
// Smooth transitions // Smooth transitions
cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED; cell.opacity += (cell.targetOpacity - cell.opacity) * transitionFactor;
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED; cell.scale += (cell.targetScale - cell.scale) * scaleFactor;
cell.elevation += (cell.targetElevation - cell.elevation) * SCALE_SPEED; cell.elevation += (cell.targetElevation - cell.elevation) * scaleFactor;
// Apply mouse interaction // Apply mouse interaction
const cellCenterX = grid.offsetX + i * CELL_SIZE + CELL_SIZE / 2; const cellCenterX = grid.offsetX + i * cellSize + cellSize / 2;
const cellCenterY = grid.offsetY + j * CELL_SIZE + CELL_SIZE / 2; const cellCenterY = grid.offsetY + j * cellSize + cellSize / 2;
const dx = cellCenterX - mouseX; const dx = cellCenterX - mouseX;
const dy = cellCenterY - mouseY; const dy = cellCenterY - mouseY;
const distanceToMouse = Math.sqrt(dx * dx + dy * dy); const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
// Color wave effect based on mouse position // 3D hill effect based on mouse position
if (distanceToMouse < MOUSE_INFLUENCE_RADIUS && cell.opacity > 0.1) { if (distanceToMouse < MOUSE_INFLUENCE_RADIUS && cell.opacity > 0.1) {
// Calculate color adjustment based on distance // Calculate height based on distance - peak at center, gradually decreasing
const influenceFactor = 1 - (distanceToMouse / MOUSE_INFLUENCE_RADIUS); const influenceFactor = Math.cos((distanceToMouse / MOUSE_INFLUENCE_RADIUS) * Math.PI / 2);
// Only positive elevation (growing upward)
cell.targetElevation = ELEVATION_FACTOR * influenceFactor * influenceFactor; // squared for more pronounced effect
// Wave effect with sine function // Slight color shift as cells rise
const waveOffset = (frameCount.current * 0.05 + distanceToMouse * 0.05) % (Math.PI * 2); const colorShift = influenceFactor * COLOR_SHIFT_AMOUNT * 0.5;
const waveFactor = (Math.sin(waveOffset) * 0.5 + 0.5) * influenceFactor;
// Adjust color based on wave
cell.color = [ cell.color = [
Math.min(255, Math.max(0, cell.baseColor[0] + COLOR_SHIFT_AMOUNT * waveFactor)), Math.min(255, Math.max(0, cell.baseColor[0] + colorShift)),
Math.min(255, Math.max(0, cell.baseColor[1] - COLOR_SHIFT_AMOUNT * waveFactor)), Math.min(255, Math.max(0, cell.baseColor[1] + colorShift)),
Math.min(255, Math.max(0, cell.baseColor[2] + COLOR_SHIFT_AMOUNT * waveFactor)) Math.min(255, Math.max(0, cell.baseColor[2] + colorShift))
] as [number, number, number]; ] as [number, number, number];
// 3D elevation effect when mouse is close
cell.targetElevation = ELEVATION_FACTOR * influenceFactor;
} else { } else {
// Gradually return to base color when mouse is away // Gradually return to base color and zero elevation when mouse is away
cell.color[0] += (cell.baseColor[0] - cell.color[0]) * 0.1; cell.color[0] += (cell.baseColor[0] - cell.color[0]) * 0.1;
cell.color[1] += (cell.baseColor[1] - cell.color[1]) * 0.1; cell.color[1] += (cell.baseColor[1] - cell.color[1]) * 0.1;
cell.color[2] += (cell.baseColor[2] - cell.color[2]) * 0.1; cell.color[2] += (cell.baseColor[2] - cell.color[2]) * 0.1;
// Reset elevation when mouse moves away
cell.targetElevation = 0; cell.targetElevation = 0;
} }
@@ -339,9 +348,34 @@ const Background: React.FC<BackgroundProps> = ({
} }
} }
// Gradually decrease ripple effect // Handle ripple animation
if (cell.rippleEffect > 0) { if (cell.rippleStartTime > 0) {
cell.rippleEffect = Math.max(0, cell.rippleEffect - RIPPLE_SPEED); const elapsedTime = Date.now() - cell.rippleStartTime;
if (elapsedTime > 0) {
// Calculate ripple progress (0 to 1)
const rippleProgress = elapsedTime / 1000; // 1 second for full animation
if (rippleProgress < 1) {
// Create a smooth wave effect
const wavePhase = rippleProgress * Math.PI * 2;
const waveHeight = Math.sin(wavePhase) * Math.exp(-rippleProgress * 4);
// Apply wave height to cell elevation only if it's not being overridden by mouse
if (distanceToMouse >= MOUSE_INFLUENCE_RADIUS) {
cell.rippleEffect = waveHeight;
cell.targetElevation = RIPPLE_ELEVATION_FACTOR * waveHeight;
} else {
cell.rippleEffect = waveHeight * 0.3; // Reduced effect when mouse is influencing
}
} else {
// Reset ripple effects
cell.rippleEffect = 0;
cell.rippleStartTime = 0;
if (distanceToMouse >= MOUSE_INFLUENCE_RADIUS) {
cell.targetElevation = 0;
}
}
}
} }
} }
} }
@@ -354,6 +388,7 @@ const Background: React.FC<BackgroundProps> = ({
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left; const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top; const mouseY = e.clientY - rect.top;
const cellSize = getCellSize();
mouseRef.current.isDown = true; mouseRef.current.isDown = true;
mouseRef.current.lastClickTime = Date.now(); mouseRef.current.lastClickTime = Date.now();
@@ -361,8 +396,8 @@ const Background: React.FC<BackgroundProps> = ({
const grid = gridRef.current; const grid = gridRef.current;
// Calculate which cell was clicked // Calculate which cell was clicked
const cellX = Math.floor((mouseX - grid.offsetX) / CELL_SIZE); const cellX = Math.floor((mouseX - grid.offsetX) / cellSize);
const cellY = Math.floor((mouseY - grid.offsetY) / CELL_SIZE); const cellY = Math.floor((mouseY - grid.offsetY) / cellSize);
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) { if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
mouseRef.current.cellX = cellX; mouseRef.current.cellX = cellX;
@@ -381,13 +416,37 @@ const Background: React.FC<BackgroundProps> = ({
}; };
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (!canvasRef.current) return; if (!canvasRef.current || !gridRef.current) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const cellSize = getCellSize();
mouseRef.current.x = e.clientX - rect.left; mouseRef.current.x = e.clientX - rect.left;
mouseRef.current.y = e.clientY - rect.top; mouseRef.current.y = e.clientY - rect.top;
// Drawing functionality - place cells while dragging
if (mouseRef.current.isDown) {
const grid = gridRef.current;
// Calculate which cell the mouse is over
const cellX = Math.floor((mouseRef.current.x - grid.offsetX) / cellSize);
const cellY = Math.floor((mouseRef.current.y - grid.offsetY) / cellSize);
// Only draw if we're on a new cell
if (cellX !== mouseRef.current.cellX || cellY !== mouseRef.current.cellY) {
mouseRef.current.cellX = cellX;
mouseRef.current.cellY = cellY;
// Spawn cell at this position if it's empty
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
const cell = grid.cells[cellX][cellY];
if (!cell.alive && !cell.transitioning) {
spawnCellAtPosition(grid, cellX, cellY);
}
}
}
}
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
@@ -439,12 +498,15 @@ const Background: React.FC<BackgroundProps> = ({
const ctx = setupCanvas(canvas, displayWidth, displayHeight); const ctx = setupCanvas(canvas, displayWidth, displayHeight);
if (!ctx) return; if (!ctx) return;
frameCount.current = 0; lastUpdateTimeRef.current = 0;
lastCycleTimeRef.current = 0;
const cellSize = getCellSize();
// Only initialize new grid if one doesn't exist or dimensions changed // Only initialize new grid if one doesn't exist or dimensions changed
if (!gridRef.current || if (!gridRef.current ||
gridRef.current.cols !== Math.floor(displayWidth / CELL_SIZE) || gridRef.current.cols !== Math.floor(displayWidth / cellSize) ||
gridRef.current.rows !== Math.floor(displayHeight / CELL_SIZE)) { gridRef.current.rows !== Math.floor(displayHeight / cellSize)) {
gridRef.current = initGrid(displayWidth, displayHeight); gridRef.current = initGrid(displayWidth, displayHeight);
} }
}, 250); }, 250);
@@ -467,18 +529,52 @@ const Background: React.FC<BackgroundProps> = ({
canvas.addEventListener('mouseup', handleMouseUp, { signal }); canvas.addEventListener('mouseup', handleMouseUp, { signal });
canvas.addEventListener('mouseleave', handleMouseLeave, { signal }); canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
const animate = () => { const handleVisibilityChange = () => {
if (document.hidden) {
// Tab is hidden
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
} else {
// Tab is visible again
if (!animationFrameRef.current) {
// Reset timing references to prevent catching up
lastUpdateTimeRef.current = performance.now();
lastCycleTimeRef.current = performance.now();
animationFrameRef.current = requestAnimationFrame(animate);
}
}
};
const animate = (currentTime: number) => {
if (signal.aborted) return; if (signal.aborted) return;
frameCount.current++; // Initialize timing if first frame
if (!lastUpdateTimeRef.current) {
lastUpdateTimeRef.current = currentTime;
lastCycleTimeRef.current = currentTime;
}
// Calculate time since last frame
const deltaTime = currentTime - lastUpdateTimeRef.current;
// Limit delta time to prevent large jumps when tab becomes active again
const clampedDeltaTime = Math.min(deltaTime, 100);
lastUpdateTimeRef.current = currentTime;
// Calculate time since last cycle update
const cycleElapsed = currentTime - lastCycleTimeRef.current;
if (gridRef.current) { if (gridRef.current) {
// Every CYCLE_FRAMES, compute the next state // Check if it's time for the next life cycle
if (frameCount.current % CYCLE_FRAMES === 0) { if (cycleElapsed >= CYCLE_TIME) {
computeNextState(gridRef.current); computeNextState(gridRef.current);
lastCycleTimeRef.current = currentTime;
} }
updateCellAnimations(gridRef.current); updateCellAnimations(gridRef.current, clampedDeltaTime);
} }
// Draw frame // Draw frame
@@ -487,8 +583,9 @@ const Background: React.FC<BackgroundProps> = ({
if (gridRef.current) { if (gridRef.current) {
const grid = gridRef.current; const grid = gridRef.current;
const cellSize = CELL_SIZE * 0.8; const cellSize = getCellSize();
const roundness = cellSize * 0.2; const displayCellSize = cellSize * 0.8;
const roundness = displayCellSize * 0.2;
for (let i = 0; i < grid.cols; i++) { for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
@@ -496,71 +593,38 @@ const Background: React.FC<BackgroundProps> = ({
// Draw all transitioning cells, even if they're fading out // Draw all transitioning cells, even if they're fading out
if ((cell.alive || cell.targetOpacity > 0) && cell.opacity > 0.01) { if ((cell.alive || cell.targetOpacity > 0) && cell.opacity > 0.01) {
const [r, g, b] = cell.color; const [r, g, b] = cell.color;
ctx.fillStyle = `rgb(${r},${g},${b})`;
// Apply ripple and elevation effects to opacity // Base opacity
const rippleBoost = cell.rippleEffect * 0.4; // Boost opacity during ripple ctx.globalAlpha = cell.opacity * 0.9;
ctx.globalAlpha = Math.min(1, cell.opacity * 0.8 + rippleBoost);
const scaledSize = cellSize * cell.scale; const scaledSize = displayCellSize * cell.scale;
const xOffset = (cellSize - scaledSize) / 2; const xOffset = (displayCellSize - scaledSize) / 2;
const yOffset = (cellSize - scaledSize) / 2; const yOffset = (displayCellSize - scaledSize) / 2;
// Apply 3D elevation effect // Apply 3D elevation effect
const elevationOffset = cell.elevation; const elevationOffset = cell.elevation;
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset; const x = grid.offsetX + i * cellSize + (cellSize - displayCellSize) / 2 + xOffset;
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset - elevationOffset; const y = grid.offsetY + j * cellSize + (cellSize - displayCellSize) / 2 + yOffset - elevationOffset;
const scaledRoundness = roundness * cell.scale; const scaledRoundness = roundness * cell.scale;
// Draw shadow for 3D effect if cell has elevation // Draw shadow for 3D effect when cell is elevated
if (elevationOffset > 1) { if (elevationOffset > 0.5) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillStyle = `rgba(0, 0, 0, ${0.2 * (elevationOffset / ELEVATION_FACTOR)})`;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y + elevationOffset + 5); ctx.moveTo(x + scaledRoundness, y + elevationOffset * 1.1);
ctx.lineTo(x + scaledSize - scaledRoundness, y + elevationOffset + 5); ctx.lineTo(x + scaledSize - scaledRoundness, y + elevationOffset * 1.1);
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset + 5, x + scaledSize, y + elevationOffset + 5 + scaledRoundness); ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset * 1.1, x + scaledSize, y + elevationOffset * 1.1 + scaledRoundness);
ctx.lineTo(x + scaledSize, y + elevationOffset + 5 + scaledSize - scaledRoundness); ctx.lineTo(x + scaledSize, y + elevationOffset * 1.1 + scaledSize - scaledRoundness);
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset + 5 + scaledSize, x + scaledSize - scaledRoundness, y + elevationOffset + 5 + scaledSize); ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset * 1.1 + scaledSize, x + scaledSize - scaledRoundness, y + elevationOffset * 1.1 + scaledSize);
ctx.lineTo(x + scaledRoundness, y + elevationOffset + 5 + scaledSize); ctx.lineTo(x + scaledRoundness, y + elevationOffset * 1.1 + scaledSize);
ctx.quadraticCurveTo(x, y + elevationOffset + 5 + scaledSize, x, y + elevationOffset + 5 + scaledSize - scaledRoundness); ctx.quadraticCurveTo(x, y + elevationOffset * 1.1 + scaledSize, x, y + elevationOffset * 1.1 + scaledSize - scaledRoundness);
ctx.lineTo(x, y + elevationOffset + 5 + scaledRoundness); ctx.lineTo(x, y + elevationOffset * 1.1 + scaledRoundness);
ctx.quadraticCurveTo(x, y + elevationOffset + 5, x + scaledRoundness, y + elevationOffset + 5); ctx.quadraticCurveTo(x, y + elevationOffset * 1.1, x + scaledRoundness, y + elevationOffset * 1.1);
ctx.fill();
// Draw side of elevated cell
const sideHeight = elevationOffset;
ctx.fillStyle = `rgba(${r*0.7}, ${g*0.7}, ${b*0.7}, ${ctx.globalAlpha})`;
// Left side
ctx.beginPath();
ctx.moveTo(x, y + scaledRoundness);
ctx.lineTo(x, y + scaledSize - scaledRoundness + sideHeight);
ctx.lineTo(x + scaledRoundness, y + scaledSize + sideHeight);
ctx.lineTo(x + scaledRoundness, y + scaledSize);
ctx.lineTo(x, y + scaledSize - scaledRoundness);
ctx.fill();
// Right side
ctx.beginPath();
ctx.moveTo(x + scaledSize, y + scaledRoundness);
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness + sideHeight);
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize + sideHeight);
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize);
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
ctx.fill();
// Bottom side
ctx.fillStyle = `rgba(${r*0.5}, ${g*0.5}, ${b*0.5}, ${ctx.globalAlpha})`;
ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y + scaledSize);
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize);
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize + sideHeight);
ctx.lineTo(x + scaledRoundness, y + scaledSize + sideHeight);
ctx.fill(); ctx.fill();
} }
// Draw main cell with original color // Draw main cell
ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y); ctx.moveTo(x + scaledRoundness, y);
@@ -574,9 +638,9 @@ const Background: React.FC<BackgroundProps> = ({
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y); ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
ctx.fill(); ctx.fill();
// Draw highlight on top for 3D effect // Draw highlight on elevated cells
if (elevationOffset > 1) { if (elevationOffset > 0.5) {
ctx.fillStyle = `rgba(255, 255, 255, ${0.2 * elevationOffset / ELEVATION_FACTOR})`; ctx.fillStyle = `rgba(255, 255, 255, ${0.1 * (elevationOffset / ELEVATION_FACTOR)})`;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y); ctx.moveTo(x + scaledRoundness, y);
ctx.lineTo(x + scaledSize - scaledRoundness, y); ctx.lineTo(x + scaledSize - scaledRoundness, y);
@@ -588,23 +652,7 @@ const Background: React.FC<BackgroundProps> = ({
ctx.fill(); ctx.fill();
} }
// Draw ripple effect // No need for separate ripple drawing since the elevation handles the 3D ripple effect
if (cell.rippleEffect > 0) {
const rippleRadius = cell.rippleEffect * cellSize * 2;
const rippleAlpha = (1 - cell.rippleEffect) * 0.5;
ctx.strokeStyle = `rgba(255, 255, 255, ${rippleAlpha})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(
x + scaledSize / 2,
y + scaledSize / 2,
rippleRadius,
0,
Math.PI * 2
);
ctx.stroke();
}
} }
} }
} }
@@ -615,11 +663,13 @@ const Background: React.FC<BackgroundProps> = ({
animationFrameRef.current = requestAnimationFrame(animate); animationFrameRef.current = requestAnimationFrame(animate);
}; };
document.addEventListener('visibilitychange', handleVisibilityChange, { signal });
window.addEventListener('resize', handleResize, { signal }); window.addEventListener('resize', handleResize, { signal });
animate(); animate(performance.now());
return () => { return () => {
controller.abort(); controller.abort();
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animationFrameRef.current);
@@ -645,7 +695,8 @@ const Background: React.FC<BackgroundProps> = ({
<div className={getContainerClasses()}> <div className={getContainerClasses()}>
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="w-full h-full bg-black cursor-pointer" className="w-full h-full bg-black"
style={{ cursor: 'default' }} // Changed from cursor-pointer to default
/> />
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" /> <div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
</div> </div>

View File

@@ -26,7 +26,7 @@ export const Comments = () => {
emitMetadata="0" emitMetadata="0"
inputPosition="bottom" inputPosition="bottom"
lang="en" lang="en"
loading="lazy" loading="eager"
/> />
) : null} ) : null}
</div> </div>

View File

@@ -12,8 +12,8 @@ export default function Footer({ fixed = false }) {
)); ));
return ( return (
<footer className={`w-full font-bold ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}> <footer className={`w-full font-bold pointer-events-none ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}>
<div className="flex flex-row px-2 py-1 text-lg lg:px-6 lg:py-1.5 lg:text-3xl md:text-2xl justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20"> <div className="flex flex-row px-2 py-1 text-lg lg:px-6 lg:py-1.5 lg:text-3xl md:text-2xl justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 pointer-events-none [&_a]:pointer-events-auto">
{footerLinks} {footerLinks}
</div> </div>
</footer> </footer>

View File

@@ -87,16 +87,19 @@ export default function Header() {
fixed z-50 top-0 left-0 right-0 fixed z-50 top-0 left-0 right-0
font-bold font-bold
transition-transform duration-300 transition-transform duration-300
pointer-events-none
${visible ? "translate-y-0" : "-translate-y-full"} ${visible ? "translate-y-0" : "-translate-y-full"}
`} `}
> >
<div className={` <div className={`
w-full flex flex-row items-center justify-center w-full flex flex-row items-center justify-center
pointer-events-none
${!isIndexPage ? 'bg-black md:bg-transparent' : ''} ${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
`}> `}>
<div className={` <div className={`
w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl
items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2 items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2
pointer-events-none [&_a]:pointer-events-auto
${!isIndexPage ? 'bg-black md:px-20' : ''} ${!isIndexPage ? 'bg-black md:px-20' : ''}
`}> `}>
{headerLinks} {headerLinks}

View File

@@ -72,8 +72,8 @@ export default function Hero() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen"> <div className="flex justify-center items-center min-h-screen pointer-events-none">
<div className="text-4xl font-bold text-center"> <div className="text-4xl font-bold text-center pointer-events-none [&_a]:pointer-events-auto">
<Typewriter <Typewriter
options={typewriterOptions} options={typewriterOptions}
onInit={handleInit} onInit={handleInit}

View File

@@ -0,0 +1,264 @@
import React, { useState } from 'react';
import { Terminal, Copy, Check } from 'lucide-react';
// Import all required icons from react-icons
import { FaDebian, FaFedora } from 'react-icons/fa6';
import { SiGentoo, SiNixos, SiArchlinux } from 'react-icons/si';
// Component for multi-line command sequences
const CommandSequence = ({
commands,
description,
shell = "bash"
}) => {
const [copied, setCopied] = useState(false);
// Join the commands with newlines for copying
const fullCommandText = Array.isArray(commands)
? commands.join('\n')
: commands;
const copyToClipboard = () => {
navigator.clipboard.writeText(fullCommandText)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
};
return (
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
{/* Header with Terminal Icon and Copy Button */}
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
<div className="flex items-center">
<Terminal size={20} className="mr-2 text-yellow-bright" />
<div className="text-sm font-comic-code">
{description || "Terminal Commands"}
</div>
</div>
<button
onClick={copyToClipboard}
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
>
{copied ? (
<>
<Check size={14} className="mr-1 text-green-bright" />
</>
) : (
<>
<Copy size={14} className="mr-1 text-foreground/70" />
</>
)}
</button>
</div>
{/* Command Display */}
<div className="text-foreground p-3 overflow-x-auto">
<div className="font-comic-code text-sm">
{Array.isArray(commands)
? commands.map((cmd, index) => (
<div key={index} className="flex items-start mb-2 last:mb-0">
<span className="text-orange-bright mr-2 flex-shrink-0">$</span>
<span className="text-purple-bright overflow-x-auto whitespace-nowrap">
{cmd}
</span>
</div>
))
: (
<div className="flex items-start">
<span className="text-orange-bright mr-2 flex-shrink-0">$</span>
<span className="text-purple-bright overflow-x-auto whitespace-nowrap">
{commands}
</span>
</div>
)
}
</div>
</div>
</div>
);
};
// Original Commands component with tabs for different distros
const Commands = ({
commandId,
description,
archCommand,
debianCommand,
fedoraCommand,
gentooCommand,
nixCommand
}) => {
const [activeTab, setActiveTab] = useState('arch');
const [copied, setCopied] = useState(false);
const distros = [
{
id: 'arch',
name: 'Arch',
icon: SiArchlinux,
command: archCommand || 'echo "No command specified for Arch"'
},
{
id: 'debian',
name: 'Debian/Ubuntu',
icon: FaDebian,
command: debianCommand || 'echo "No command specified for Debian/Ubuntu"'
},
{
id: 'fedora',
name: 'Fedora',
icon: FaFedora,
command: fedoraCommand || 'echo "No command specified for Fedora"'
},
{
id: 'gentoo',
name: 'Gentoo',
icon: SiGentoo,
command: gentooCommand || 'echo "No command specified for Gentoo"'
},
{
id: 'nix',
name: 'NixOS',
icon: SiNixos,
command: nixCommand || 'echo "No command specified for NixOS"'
}
];
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
};
return (
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
{/* Header with Terminal Icon and Copy Button */}
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
<div className="flex items-center">
<Terminal size={20} className="mr-2 text-yellow-bright" />
<div className="text-sm font-comic-code">
{description || "Terminal Command"}
</div>
</div>
<button
onClick={() => copyToClipboard(distros.find(d => d.id === activeTab).command)}
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
>
{copied ? (
<>
<Check size={14} className="mr-1 text-green-bright" />
<span>Copied</span>
</>
) : (
<>
<Copy size={14} className="mr-1 text-foreground/70" />
<span>Copy</span>
</>
)}
</button>
</div>
{/* Tabs */}
<div className="flex flex-wrap border-b border-foreground/20 bg-background">
{distros.map((distro) => {
const IconComponent = distro.icon;
return (
<button
key={distro.id}
className={`px-3 py-2 text-sm font-medium flex items-center ${
activeTab === distro.id
? 'bg-background border-b-2 border-blue-bright text-blue-bright'
: 'text-foreground/80 hover:text-foreground hover:bg-foreground/5'
}`}
onClick={() => setActiveTab(distro.id)}
>
<span className="mr-1 inline-flex items-center">
<IconComponent size={16} />
</span>
{distro.name}
</button>
);
})}
</div>
{/* Command Display with Horizontal Scrolling */}
<div className="text-foreground p-3 overflow-x-auto">
<div className="flex items-center font-comic-code text-sm whitespace-nowrap">
<span className="text-orange-bright mr-2">$</span>
<span className="text-purple-bright">
{distros.find(d => d.id === activeTab).command}
</span>
</div>
</div>
</div>
);
};
// Single command component
const Command = ({
command,
description,
shell = "bash"
}) => {
const [copied, setCopied] = useState(false);
const copyToClipboard = () => {
navigator.clipboard.writeText(command)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
};
return (
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
{/* Header with Terminal Icon and Copy Button */}
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
<div className="flex items-center">
<Terminal size={20} className="mr-2 text-yellow-bright" />
<div className="text-sm font-comic-code">
{description || "Terminal Command"}
</div>
</div>
<button
onClick={copyToClipboard}
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
>
{copied ? (
<>
<Check size={14} className="mr-1 text-green-bright" />
</>
) : (
<>
<Copy size={14} className="mr-1 text-foreground/70" />
</>
)}
</button>
</div>
{/* Command Display with Horizontal Scrolling */}
<div className="text-foreground p-3 overflow-x-auto">
<div className="flex items-center font-comic-code text-sm whitespace-nowrap">
<span className="text-orange-bright mr-2">$</span>
<span className="text-purple-bright">
{command}
</span>
</div>
</div>
</div>
);
};
export { Commands, Command, CommandSequence };

View File

@@ -0,0 +1,68 @@
// src/components/mdx/Video.tsx
import React, { useRef } from "react";
import { Play } from "lucide-react";
type VideoProps = {
url: string;
title: string;
attribution?: string;
className?: string;
};
export function Video({ url, title, attribution, className }: VideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const overlayRef = useRef<HTMLButtonElement>(null);
const handlePlay = () => {
if (!videoRef.current || !overlayRef.current) return;
// Show browser native controls on play
videoRef.current.controls = true;
videoRef.current.play();
// Hide the overlay
overlayRef.current.style.display = "none";
};
return (
<figure className={`w-full ${className ?? ""}`}>
<div className="relative w-full bg-background rounded-lg overflow-hidden">
<video
ref={videoRef}
className="w-full h-auto bg-black cursor-pointer rounded-lg block"
preload="metadata"
playsInline
title={title}
>
<source src={url} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Big overlay play button */}
<button
ref={overlayRef}
onClick={handlePlay}
className="absolute inset-0 flex items-center justify-center bg-background/90 text-foreground hover:text-yellow-bright transition"
aria-label={`Play ${title}`}
>
<Play size={64} strokeWidth={1.5} />
</button>
</div>
{/* Title + attribution */}
<figcaption className="mt-2 text-xs text-foreground flex justify-between items-center">
<span>{title}</span>
{attribution && (
<a
href={attribution}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-blue-bright"
>
{attribution}
</a>
)}
</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,9 @@
---
title: Breaking the Chromebook Cage
description: From breaking Chromebooks as a student to breaking Chromebooks to stop students from breaking Chromebooks
author: Timothy Pidashev
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
date: 2025-09-15
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
isDraft: true
---

View File

@@ -0,0 +1,104 @@
import React from 'react';
interface T440pAdProps {
className?: string;
}
const Advertisement: React.FC<T440pAdProps> = ({ className = '' }) => {
return (
<div className={`bg-gradient-to-br from-blue-50 to-indigo-100 border-2 border-blue-200 rounded-lg p-6 my-8 shadow-lg ${className}`}>
<div className="flex items-start gap-4">
{/* Icon/Logo placeholder */}
<div className="flex-shrink-0 w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center">
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<h3 className="text-xl font-bold text-gray-900">
Custom Corebooted ThinkPad T440p
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Available Now
</span>
</div>
<p className="text-gray-700 mb-4">
Skip the technical complexity and get a professionally corebooted ThinkPad T440p
built to your specifications. Each laptop is carefully modified and tested to ensure
optimal performance and reliability.
</p>
<div className="grid md:grid-cols-2 gap-4 mb-4">
<div>
<h4 className="font-semibold text-gray-900 mb-2"> What's Included:</h4>
<ul className="text-sm text-gray-700 space-y-1">
<li> Coreboot firmware pre-installed</li>
<li> IPS 1080p display upgrade</li>
<li> RAM options: 4GB, 8GB, or 16GB</li>
<li> CPU choice available</li>
<li> Battery upgrade option</li>
<li> Thorough testing & quality assurance</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-2">🔧 Benefits:</h4>
<ul className="text-sm text-gray-700 space-y-1">
<li> Faster boot times</li>
<li> Open-source BIOS</li>
<li> Enhanced security</li>
<li> No proprietary firmware</li>
<li> Full hardware control</li>
<li> Professional installation</li>
</ul>
</div>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pt-4 border-t border-blue-200">
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-blue-600">$500</span>
<span className="text-sm text-gray-600">USD (base configuration)</span>
</div>
<div className="flex gap-3">
<a
href="https://ebay.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.5 2A1.5 1.5 0 007 3.5v1A1.5 1.5 0 008.5 6h7A1.5 1.5 0 0017 4.5v-1A1.5 1.5 0 0015.5 2h-7zM10 3h4v1h-4V3z"/>
<path d="M6 7.5A1.5 1.5 0 017.5 6h9A1.5 1.5 0 0118 7.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 016 16.5v-9z"/>
</svg>
Order on eBay
</a>
<button className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 font-medium rounded-lg hover:bg-blue-50 transition-colors">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Ask Questions
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default Advertisement;

View File

@@ -0,0 +1,308 @@
---
title: Thinkpad T440p Coreboot Guide
description: The definitive guide on corebooting a Thinkpad T440p
author: Timothy Pidashev
tags: [t440p, coreboot, thinkpad]
date: 2025-01-15
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
isDraft: true
---
import { Commands, Command, CommandSequence } from "@/components/mdx/command";
import Advertisement from '@/content/blog/components/thinkpad-t440p-coreboot-guide/advertisement';
> **Interactive Script Available!**
> Want to skip the manual steps in this guide?
> I've created an interactive script that can automate the entire process step by step as you follow along.
> This script supports Arch, Debian, Fedora, Gentoo, and Nix!
<Command
description="Interactive script"
command="curl -fsSL https://timmypidashev.dev/scripts/run.sh | sh -s -- -t coreboot-t440p"
client:load
/>
Don't pipe anyone's scripts to **sh** blindly, including mine - <a href="https://github.com/timmypidashev/scripts" target="_blank" rel="noopener noreferrer">audit the source</a>.
## Getting Started
The Thinkpad T440p is a powerful and versatile laptop that can be further enhanced by installing coreboot,
an open-source BIOS replacement. This guide will walk you through the process of corebooting your T440p,
including flashing the BIOS chip and installing the necessary software.
## What You'll Need
Before getting started corebooting your T440p, make sure you have the following:
- **Thinkpad T440p**: This guide is specifically for the T440p model.
- **CH341A Programmer**: This is a USB device used to flash the BIOS chip.
- **Screwdriver**: A torx screwdriver is needed to open the laptop.
## Installing Dependencies
Install the following programs. These will be needed to compile coreboot and flash the BIOS.
<Commands
description="Install prerequisite packages"
archCommand="sudo pacman -S base-devel curl git gcc-ada ncurses zlib nasm sharutils unzip flashrom"
debianCommand="sudo apt install build-essential curl git gnat libncurses-dev zlib1g-dev nasm sharutils unzip flashrom"
fedoraCommand="sudo dnf install @development-tools curl git gcc-gnat ncurses-devel zlib-devel nasm sharutils unzip flashrom"
gentooCommand="sudo emerge --ask sys-devel/base-devel net-misc/curl dev-vcs/git sys-devel/gcc ncurses dev-libs/zlib dev-lang/nasm app-arch/sharutils app-arch/unzip sys-apps/flashrom"
nixCommand="nix-env -i stdenv curl git gcc gnat ncurses zlib nasm sharutils unzip flashrom"
client:load
/>
## Disassembling the Laptop
1. **Power off your laptop**: Make sure your T440p is completely powered off and unplugged from any power source.
2. **Remove the battery**: Flip the laptop over and remove the battery by sliding the latch to the unlock position and lifting it out.
3. **Unscrew the back panel**: Use a torx screwdriver to remove the screws securing the back panel.
## Locating the EEPROM Chips
In order to flash the laptop, you will need to have access to two EEPROM chips located next to the sodimm RAM.
![EEPROM Chips Location](/blog/thinkpad-t440p-coreboot-guide/eeprom_chips_location.png)
## Assembling the SPI Flasher
Place the SPI flasher ribbon cable into the correct slot and make sure its the 3.3v variant
![SPI Flasher Assembly](/blog/thinkpad-t440p-coreboot-guide/spi_flasher_assembly.png)
After the flasher is ready, connect it to your machine and ensure its ready to use:
<Command
description="Ensure the CH341A flasher is being detected"
command="flashrom --programmer ch341a_spi"
/>
Flashrom should report that programmer initialization was a success.
## Extracting Original BIOS
To begin, first create a clean directory where all work to coreboot
the T440p will be done.
<Command
description="Create a directory where all work will be done"
command="mkdir ~/t440p-coreboot"
client:load
/>
Next, extract the original rom from both EEPROM chips. This is
done by attaching the programmer to the correct chip and running
the subsequent commands. It may take longer than expected, and
ensuring the bios was properly extracted is important before proceeding
further.
<CommandSequence
commands={[
"sudo flashrom --programmer ch341a_spi -r 4mb_backup1.bin",
"sudo flashrom --programmer ch341a_spi -r 4mb_backup2.bin",
"diff 4mb_backup1.bin 4mb_backup2.bin"
]}
description="Backup and verify 4MB chip"
client:load
/>
<CommandSequence
commands={[
"sudo flashrom --programmer ch341a_spi -r 8mb_backup1.bin",
"sudo flashrom --programmer ch341a_spi -r 8mb_backup2.bin",
"diff 8mb_backup1.bin 8mb_backup2.bin"
]}
description="Backup and verify 8MB chip"
client:load
/>
If the diff checks pass, combine both files into one ROM.
<Command
description="Combine 4MB & 8MB into one ROM"
command="cat 8mb_backup_1.bin 4mb_backup1.bin > t440p-original.rom"
client:load
/>
## Building Required Tools
Now that the original bios has been successfuly extracted, it is time
to clone the coreboot repository and build every tool needed to build
a new bios image.
<CommandSequence
commands={[
"git clone https://review.coreboot.org/coreboot ~/t440p-coreboot/coreboot",
"cd ~/t440p-coreboot/coreboot",
"git checkout e1e762716cf925c621d58163133ed1c3e006a903",
"git submodule update --init --checkout"
]}
description="Clone coreboot and checkout to the correct commit"
client:load
/>
We will need to build `idftool`, which will be used to export all necessary blobs
from our original bios, and `cbfstool`, which will be used to extract __mrc.bin__(a blob
from a haswell chromebook peppy image).
<Command
description="Build util/ifdtool"
command="cd ~/t440p-coreboot/coreboot/util/ifdtool && make"
client:load
/>
<Command
description="Build util/cbfstool"
command="cd ~/t440p-coreboot/coreboot/ && make -C util/cbfstool"
client:load
/>
## Exporting Firmware Blobs
Once the necessary tools have been built, we can export the
3 flash regions from our original bios image.
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot/util/ifdtool",
"./ifdtool -x ~/t440p-coreboot/t440p-original.rom",
"mv flashregion_0_flashdescriptor.bin ~/t440p-coreboot/ifd.bin",
"mv flashregion_2_intel_me.bin ~/t440p-coreboot/me.bin",
"mv flashregion_3_gbe.bin ~/t440p-coreboot/gbe.bin"
]}
description="Export firmware blobs"
client:load
/>
## Obtaining mrc.bin
In order to obtain __mrc.bin__, we need the chromeos peppy image.
This can be pulled by running the `crosfirmware.sh` script found in util/chromeos.
<Command
description="Download peppy chromeos image"
command="cd ~/t440p-coreboot/coreboot/util/chromeos && ./crosfirmware.sh peppy"
client:load
/>
We can now obtain __mrc.bin__ using cbfstool to extract the blob from the image.
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot/util/chromeos",
"../cbfstool/cbfstool coreboot-*.bin extract -f mrc.bin -n mrc.bin -r RO_SECTION",
"mv mrc.bin ~/t440p-coreboot/mrc.bin"
]}
description="Extract mrc.bin using cbfstool"
client:load
/>
## Configuring Coreboot
Configuring coreboot is really where most of your time will be spent. To help out,
I've created several handy configs that should suit most use cases, and can be easily
tweaked to your liking. Here is a list of whats available:
1. GRUB2
This configuration features GRUB2 as the bootloader, and contains 3 secondary payloads,
which the user can opt in/out of:
* memtest built in
* nvramcui built in
* coreinfo built in
This configuration also includes the dGPU option rom as well for T440p's featuring the gt730m on board.
2. SeaBIOS
3. edk2
> NOTE: Show the user how to choose the appropriate config, as well as building a custom config below.
## Building and Flashing
After configuring coreboot, it is time to build and flash it onto your unsuspecting T440p :D
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot",
"make crossgcc-i386 CPUS=$(nproc)",
"make"
]}
description="Build coreboot"
client:load
/>
Once the coreboot build has completed, split the built ROM for the 8MB(bottom) chip & 4MB(top) chip.
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot/build",
"dd if=coreboot.rom of=bottom.rom bs=1M count=8",
"dd if=coreboot.rom of=top.rom bs=1M skin=8"
]}
description="Split the built ROM for both EEPROM chips"
client:load
/>
Now flash the new bios onto your thinkpad!
<Command
description="Flash the 4MB chip"
command="sudo flashrom --programmer ch341a_spi -w top.rom"
/>
<Command
description="Flash the 8MB chip"
command="sudo flashrom --programmer ch341a_spi -w bottom.rom"
/>
Thats it! If done properly, your thinkpad should now boot!
## Reverting to Original
If for some reason you feel the need to revert back, or your T440p can't boot,
here are the steps needed to flash the original image back.
### Can't Boot
<CommandSequence
commands={[
"cd ~/t440p-coreboot/",
"dd if=t440p-original.rom of=bottom.rom bs=1M count=8",
"dd if=t440p-original.rom of=top.rom bs=1M skip=8"
]}
description="Split original bios image for both EEPROM chips"
client:load
/>
<Command
description="Flash the 4MB chip"
command="sudo flashrom --programmer ch341a_spi -w top.rom"
/>
<Command
description="Flash the 8MB chip"
command="sudo flashrom --programmer ch341a_spi -w bottom.rom"
/>
### Can Boot
<CommandSequence
commands={[
"sudo sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s/\"/ iomem=relaxed\"/2' /etc/default/grub",
"sudo grub-mkconfig -o /boot/grub/grub.cfg",
]}
description="Set kernel flag iomem=relaxed and update grub config"
client:load
/>
Reboot to apply `iomem=relaxed`
<Command
description="Flash the original bios"
command="sudo flashrom -p internal:laptop=force_I_want_a_brick -r ~/t440p-coreboot/t440p-original.rom"
/>
And that about wraps it up! If you liked the guide, leave a reaction or comment any changes or fixes
I should make below. Your feedback is greatly appreciated!

View File

@@ -12,6 +12,7 @@ export const collections = {
}), }),
image: z.string().optional(), image: z.string().optional(),
imagePosition: z.string().optional(), imagePosition: z.string().optional(),
isDraft: z.boolean().optional()
}), }),
}), }),
projects: defineCollection({ projects: defineCollection({
@@ -22,7 +23,7 @@ export const collections = {
demoUrl: z.string().url().optional(), demoUrl: z.string().url().optional(),
techStack: z.array(z.string()), techStack: z.array(z.string()),
date: z.string(), date: z.string(),
image: z.string().optional(), image: z.string().optional()
}), }),
}), })
}; };

View File

@@ -1,38 +0,0 @@
---
title: Thinkpad T440p Coreboot Guide
description: The definitive guide on corebooting a Thinkpad T440p
author: Timothy Pidashev
tags: [t440p, coreboot, thinkpad]
date: 2025-01-15
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
---
> **Interactive Script Available!**
> Want to skip the manual steps in this guide?
> I've created an interactive script that can automate the entire process step by step as you follow along.
> Simply run the following command in your terminal to get started:
>
> ```
> curl -fsSL https://timmypidashev.dev/scripts/run.sh | sh -s -- -t coreboot-t440p
> ```
> NOTE: This script supports Arch, Debian, Fedora, Gentoo, and Nix linux distributions!
## Getting Started
The Thinkpad T440p is a powerful and versatile laptop that can be further enhanced by installing coreboot,
an open-source BIOS replacement. This guide will walk you through the process of corebooting your T440p,
including flashing the BIOS chip and installing the necessary software.
## What You'll Need
Before getting started corebooting your T440p, make sure you have the following:
- **Thinkpad T440p**: This guide is specifically for the T440p model.
- **CH341A Programmer**: This is a USB device used to flash the BIOS chip.
- **Screwdriver**: A torx screwdriver is needed to open the laptop.
## Disassembling the Laptop
1. **Power off your laptop**: Make sure your T440p is completely powered off and unplugged from any power source.
2. **Remove the battery**: Flip the laptop over and remove the battery by sliding the latch to the unlock position and lifting it out.
3. **Unscrew the back panel**: Use a torx screwdriver to remove the screws securing the back panel.
## Locating the EEPROM Chips

View File

@@ -1,86 +0,0 @@
---
title: Thinkpad T440p Modification Guide
description: You purchased a T440p, now what?
author: Timothy Pidashev
tags: [t440p, mods, coreboot, thinkpad]
date: 2025-01-15
image: "/blog/thinkpad-t440p-modification-guide/thumbnail.png"
---
## The T440p
Whether for privacy related reasons, coreboot, or someones advice on the internet,
you are now the proud owner of a T440p. Now what? Well, I have been daily driving
this laptop for over two years now, and would like to share my knowledge on this
lovely machine. If followed properly, this guide should help any privacy seeking
individual or programmer how to setup the "reasonably" perfect T440p.
## Buying the Right Model
Although the T440p comes in various configurations and specs, when searching for
one online there are two things to consider.
1. Online Marketplace
* Purchasing from the right marketplace is important to consider, and while trusted
vendors like Amazon might be preferred, consider Ebay or AliExpress.
* I personally have only purchased my thinkpad's on Ebay, as there are generally more listings
available from companies reselling retired units, usually at a steep discount.
2. Dedicated GPU
* The T440p motherboard comes in two different varieties, one with
a dGPU and the other without. There is only one dGPU model, which is the NVIDIA GT 730M.
Featuring 2GB of VRAM, it will work, however if your looking for longer battery life and
an easier coreboot config should you choose to coreboot, I would recommend sticking to
a non dGPU variant.
* Finding a dGPU variant is quite difficult, as many online
sellers don't always list the motherboard spec, making things quite the guessing game.
When I was shopping for one, my strategy was to purchase the dGPU motherboard on its own,
and then a T440p laptop listed with a dead motherboard, as I was going to swap it out anyways.
3. Quality
* Finding the perfect T440p is hard, and you will likely end up purchasing one that looks ok
in pictures, but comes with a cracked palmrest or front panel. Consider purchasing one which
looks good, and then replacing any cracked or aged parts should you choose to do so in the future.
* T440p plastics are aged. Although this machine is an absolute brick, which can probably be thrown
at the ground without any major damage, it will definitely chip and crack. I myself have replaced my
palm rest/keyboard cover thrice, as every half a year or so I will open the laptop in the morning to
find that my careless "throw it in the backpack" has finally cracked the palmrest yet again.
## Screen
When it comes to the screen, you really don't want to get one of poor quality, especially since the
lousy 1366x768 panel is not great nowadays. Generally, I would recommend going for an ips 1080p panel,
as this is generally most the most supported. I purchased this panel from amazon for ~$60USD and have
never looked back.
## Keyboard
## Trackpad
## Battery
## CPU
The T440p has a trick up its sleeve. The processor can be swapped out and replaced, allowing for an upgrade!
There are many models out there, however some aren't recommended due to thermal constraints, so finding the
right balance can be tough.
## RAM
## Storage
## WLAN
## WAN
## MISC
1. Fingerprint Reader
2. Disc Reader
3. Webcam & Microphone

View File

@@ -1,16 +1,20 @@
--- ---
import "@/style/globals.css"; import "@/style/globals.css";
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
import Header from "@/components/header"; import Header from "@/components/header";
import Footer from "@/components/footer"; import Footer from "@/components/footer";
import Background from "@/components/background"; import Background from "@/components/background";
export interface Props { export interface Props {
title: string; title: string;
description: string; description: string;
} }
const { title, description } = Astro.props; const { title, description } = Astro.props;
const ogImage = "https://timmypidashev.dev/og-image.jpg"; const ogImage = "https://timmypidashev.dev/og-image.jpg";
--- ---
<html lang="en"> <html lang="en">
<head> <head>
<title>{title}</title> <title>{title}</title>
@@ -52,7 +56,9 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
<main class="flex-1 flex flex-col"> <main class="flex-1 flex flex-col">
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1"> <div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
<Background layout="content" position="right" client:only="react" transition:persist /> <Background layout="content" position="right" client:only="react" transition:persist />
<slot /> <div>
<slot />
</div>
<Background layout="content" position="left" client:only="react" transition:persist /> <Background layout="content" position="left" client:only="react" transition:persist />
</div> </div>
</main> </main>

View File

@@ -0,0 +1,70 @@
---
import "@/style/globals.css";
import { ClientRouter } from "astro:transitions";
import Header from "@/components/header";
import Footer from "@/components/footer";
import Background from "@/components/background";
export interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
const ogImage = "https://timmypidashev.dev/og-image.jpg";
---
<html lang="en">
<head>
<title>{title}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<!-- OpenGraph -->
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImage} />
<meta name="twitter:description" content={description} />
<!-- Basic meta description for search engines -->
<meta name="description" content={description} />
<!-- Also used in OpenGraph for social media sharing -->
<meta property="og:description" content={description} />
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
<ClientRouter
defaultTransition={false}
handleFocus={false}
/>
<style>
::view-transition-new(:root) {
animation: none;
}
::view-transition-old(:root) {
animation: 90ms ease-out both fade-out;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
</head>
<body class="bg-background text-foreground min-h-screen flex flex-col">
<main class="flex-1 flex flex-col">
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
<Background layout="content" position="right" client:only="react" transition:persist />
<div>
<slot />
</div>
<Background layout="content" position="left" client:only="react" transition:persist />
</div>
</main>
<script>
document.addEventListener("astro:after-navigation", () => {
window.scrollTo(0, 0);
});
</script>
</body>
</html>

View File

@@ -13,10 +13,10 @@ const { slug } = Astro.params;
const posts = await getCollection("blog"); const posts = await getCollection("blog");
const post = posts.find(post => post.slug === slug); const post = posts.find(post => post.slug === slug);
if (!post) { if (!post || post.data.isDraft === true) {
return new Response(null, { return new Response(null, {
status: 404, status: 404,
statusText: 'Not found' statusText: "Not found"
}); });
} }
@@ -63,7 +63,7 @@ const jsonLd = {
> >
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} /> <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<div class="relative max-w-8xl mx-auto"> <div class="relative max-w-8xl mx-auto">
<article class="prose prose-lg mx-auto max-w-4xl"> <article class="prose prose-invert prose-lg mx-auto max-w-4xl">
{post.data.image && ( {post.data.image && (
<div class="-mx-4 sm:mx-0 mb-8"> <div class="-mx-4 sm:mx-0 mb-8">
<Image <Image
@@ -97,7 +97,7 @@ const jsonLd = {
</span> </span>
))} ))}
</div> </div>
<div class="prose prose-invert max-w-none"> <div class="prose prose-invert prose-lg max-w-none">
<Content /> <Content />
</div> </div>
</article> </article>

View File

@@ -2,6 +2,7 @@
export const prerender = true; export const prerender = true;
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro"; import ContentLayout from "@/layouts/content.astro";
import { Comments } from "@/components/blog/comments"; import { Comments } from "@/components/blog/comments";
@@ -59,7 +60,7 @@ const { Content } = await project.render();
</div> </div>
</header> </header>
<div class="prose prose-invert max-w-none"> <div class="prose prose-invert prose-lg max-w-none">
<Content /> <Content />
</div> </div>
</article> </article>

View File

@@ -27,3 +27,38 @@
body { body {
font-family: "ComicRegular"; font-family: "ComicRegular";
} }
code[data-line-numbers] {
counter-reset: line;
}
code[data-line-numbers] > [data-line]::before {
counter-increment: line;
content: counter(line);
/* Other styling */
display: inline-block;
width: 0.75rem;
margin-right: 2rem;
text-align: right;
color: gray;
}
code[data-line-numbers-max-digits="2"] > [data-line]::before {
width: 1.25rem;
}
code[data-line-numbers-max-digits="3"] > [data-line]::before {
width: 1.75rem;
}
code[data-line-numbers-max-digits="4"] > [data-line]::before {
width: 2.25rem;
}
code {
overflow-x: scroll !important;
max-width: calc(82vw - 2rem) !important;
width: 100% !important;
}

View File

@@ -41,10 +41,15 @@ module.exports = {
"draw-line": { "draw-line": {
"0%": { "stroke-dashoffset": "100" }, "0%": { "stroke-dashoffset": "100" },
"100%": { "stroke-dashoffset": "0" } "100%": { "stroke-dashoffset": "0" }
},
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" }
} }
}, },
animation: { animation: {
"draw-line": "draw-line 0.6s ease-out forwards" "draw-line": "draw-line 0.6s ease-out forwards",
"fade-in": "fade-in 0.3s ease-in-out forwards"
}, },
typography: (theme) => ({ typography: (theme) => ({
DEFAULT: { DEFAULT: {
@@ -91,6 +96,56 @@ module.exports = {
transition: "all 0.2s ease-in-out", transition: "all 0.2s ease-in-out",
}, },
// Code
'code:not([data-language])': {
color: theme('colors.purple.bright'),
backgroundColor: '#282828',
padding: '0',
borderRadius: '0.25rem',
fontFamily: 'Comic Code, monospace',
fontWeight: '400',
fontSize: 'inherit', // Match the parent text size
'&::before': { content: 'none' },
'&::after': { content: 'none' },
},
'pre': {
backgroundColor: '#282828',
color: theme("colors.foreground"),
borderRadius: '0.5rem',
overflow: 'visible', // This allows the copy button to be positioned outside
position: 'relative', // For the copy button positioning
marginTop: '1.5rem', // Space for the copy button and language label
fontSize: 'inherit', // Match the parent font size
},
'pre code': {
display: 'block',
fontFamily: 'Comic Code, monospace',
fontSize: '1em', // This will inherit from the prose-lg setting
padding: '0',
overflow: 'auto', // Enable horizontal scrolling
whiteSpace: 'pre',
'&::before': { content: 'none' },
'&::after': { content: 'none' },
},
'[data-rehype-pretty-code-fragment]:nth-of-type(2) pre': {
'[data-line]::before': {
content: 'counter(line)',
counterIncrement: 'line',
display: 'inline-block',
width: '1rem',
marginRight: '1rem',
textAlign: 'right',
color: '#86e1fc',
},
'[data-highlighted-line]::before': {
color: '#86e1fc',
},
},
// Bold // Bold
strong: { strong: {
color: theme("colors.orange.bright"), color: theme("colors.orange.bright"),
@@ -118,81 +173,6 @@ module.exports = {
}, },
}, },
// Code
'code:not([data-language])': {
color: theme('colors.purple.bright'),
backgroundColor: '#282828',
padding: '0.2em 0.4em',
borderRadius: '0.25rem',
fontFamily: 'Comic Code, monospace',
fontWeight: '400',
'&::before': { content: 'none' },
'&::after': { content: 'none' },
},
'pre code': {
display: 'grid', // This ensures line backgrounds stretch full width
minWidth: '100%',
fontFamily: 'Comic Code, monospace',
fontSize: '0.875rem', // text-sm
lineHeight: '1.7142857', // leading-6
padding: '1rem', // p-4
'&::before': { content: 'none' },
'&::after': { content: 'none' },
},
'.highlighted': {
backgroundColor: theme('colors.foreground/5'),
paddingLeft: '1rem',
paddingRight: '1rem',
marginLeft: '-1rem',
marginRight: '-1rem',
},
'.word': {
backgroundColor: theme('colors.foreground/20'),
padding: '0.2em',
borderRadius: '0.25rem',
},
code: {
color: theme("colors.purple.bright"),
backgroundColor: "#282828", // A dark gray that works with black
padding: "0.2em 0.4em",
borderRadius: "0.25rem",
fontWeight: "400",
"&::before": {
content: "\"\"",
},
"&::after": {
content: "\"\"",
},
},
// Inline code
"code::before": {
content: "\"\"",
},
"code::after": {
content: "\"\"",
},
// Pre
pre: {
backgroundColor: "#282828",
color: theme("colors.foreground"),
code: {
backgroundColor: "transparent",
padding: "0",
color: "inherit",
fontSize: "inherit",
fontWeight: "inherit",
"&::before": { content: "none" },
"&::after": { content: "none" },
},
},
// Horizontal rules // Horizontal rules
hr: { hr: {
borderColor: theme("colors.foreground"), borderColor: theme("colors.foreground"),
@@ -226,6 +206,13 @@ module.exports = {
}, },
}, },
}, },
lg: {
css: {
"pre code": {
fontSize: "1rem",
},
},
},
}), }),
}, },
}, },