diff --git a/.gitignore b/.gitignore index 0452851..eed56dc 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ next-env.d.ts .env .vercel + +# Rust/WASM build artifacts +wasm-renderer/target +wasm-renderer/pkg diff --git a/components/AsciiCanvas/AsciiCanvas.module.css b/components/AsciiCanvas/AsciiCanvas.module.css new file mode 100644 index 0000000..b053dd7 --- /dev/null +++ b/components/AsciiCanvas/AsciiCanvas.module.css @@ -0,0 +1,43 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + overflow: hidden; + background: #000; +} + +.canvas { + display: block; + width: 100%; + height: 100%; + image-rendering: pixelated; + image-rendering: crisp-edges; +} + +.loading, +.error { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-family: "Courier New", Consolas, Monaco, monospace; + font-size: 16px; + color: #00ff00; +} + +.error { + color: #ff4444; +} + +/* Scrollbar hiding */ +.container::-webkit-scrollbar { + display: none; +} + +.container { + -ms-overflow-style: none; + scrollbar-width: none; +} diff --git a/components/AsciiCanvas/AsciiCanvas.tsx b/components/AsciiCanvas/AsciiCanvas.tsx new file mode 100644 index 0000000..1d8edce --- /dev/null +++ b/components/AsciiCanvas/AsciiCanvas.tsx @@ -0,0 +1,298 @@ +import { useEffect, useRef, useCallback, useState } from 'react'; +import { useRouter } from 'next/router'; +import { useWasm } from './useWasm'; +import { useImages } from './useImages'; +import type { SiteContent, HitAction } from './types'; +import type { Renderer } from 'lib/wasm/ascii_renderer'; + +import styles from './AsciiCanvas.module.css'; + +interface AsciiCanvasProps { + content: SiteContent; + imageUrls: Map; +} + +// Character dimensions for the monospace font (50% size for higher density) +const CHAR_WIDTH = 4.8; +const CHAR_HEIGHT = 9; +const FONT_FAMILY = '"Courier New", Consolas, Monaco, monospace'; +const FONT_SIZE = 7.5; + +export function AsciiCanvas({ content, imageUrls }: AsciiCanvasProps) { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ cols: 80, rows: 40 }); + const animationFrameRef = useRef(undefined); + const scrollRef = useRef(0); + const router = useRouter(); + + // Initialize WASM + const { renderer, loading, error } = useWasm(dimensions.cols, dimensions.rows); + + // Image loading + const { loadImage, imagesLoaded } = useImages(renderer); + + // Calculate dimensions based on window size + const updateDimensions = useCallback(() => { + if (!containerRef.current) return; + + const width = containerRef.current.clientWidth; + const height = containerRef.current.clientHeight; + + const cols = Math.floor(width / CHAR_WIDTH); + const rows = Math.floor(height / CHAR_HEIGHT); + + setDimensions({ cols: Math.max(40, cols), rows: Math.max(20, rows) }); + }, []); + + // Set up resize observer + useEffect(() => { + updateDimensions(); + + const resizeObserver = new ResizeObserver(updateDimensions); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => resizeObserver.disconnect(); + }, [updateDimensions]); + + // Load images + useEffect(() => { + imageUrls.forEach((url, id) => { + loadImage(id, url); + }); + }, [imageUrls, loadImage]); + + // Update content when it changes + useEffect(() => { + if (!renderer) return; + + try { + renderer.set_content(JSON.stringify(content)); + } catch (err) { + console.error('Failed to set content:', err); + } + }, [renderer, content, imagesLoaded]); + + // Render loop + const renderFrame = useCallback(() => { + if (!renderer || !canvasRef.current) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Get render data from WASM + const data = renderer.render(); + const cols = renderer.get_width(); + const rows = renderer.get_height(); + + // Set canvas size + const pixelWidth = cols * CHAR_WIDTH; + const pixelHeight = rows * CHAR_HEIGHT; + + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + } + + // Clear canvas with theme background + const isDark = content.page === 'resume'; + ctx.fillStyle = isDark ? '#181a1b' : '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Set font + ctx.font = `${FONT_SIZE}px ${FONT_FAMILY}`; + ctx.textBaseline = 'top'; + + // Render each character + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const idx = (row * cols + col) * 4; + const charCode = data[idx]; + const fgColor = data[idx + 1]; + const bgColor = data[idx + 2]; + const flags = data[idx + 3]; + + const x = col * CHAR_WIDTH; + const y = row * CHAR_HEIGHT; + + // Draw background if not transparent + if (bgColor !== 0) { + ctx.fillStyle = colorToRgba(bgColor); + ctx.fillRect(x, y, CHAR_WIDTH, CHAR_HEIGHT); + } + + // Draw character if not space + if (charCode !== 32) { + const char = String.fromCharCode(charCode); + ctx.fillStyle = colorToRgba(fgColor); + + // Handle bold + if (flags & 0x01) { + ctx.font = `bold ${FONT_SIZE}px ${FONT_FAMILY}`; + } else { + ctx.font = `${FONT_SIZE}px ${FONT_FAMILY}`; + } + + ctx.fillText(char, x, y + 2); + + // Handle underline + if (flags & 0x02) { + ctx.beginPath(); + ctx.moveTo(x, y + CHAR_HEIGHT - 2); + ctx.lineTo(x + CHAR_WIDTH, y + CHAR_HEIGHT - 2); + ctx.strokeStyle = colorToRgba(fgColor); + ctx.stroke(); + } + } + } + } + }, [renderer, content.page]); + + // Animation loop + useEffect(() => { + if (!renderer) return; + + let running = true; + + function loop() { + if (!running) return; + renderFrame(); + animationFrameRef.current = requestAnimationFrame(loop); + } + + loop(); + + return () => { + running = false; + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [renderer, renderFrame]); + + // Handle scroll + const handleWheel = useCallback((e: WheelEvent) => { + if (!renderer) return; + e.preventDefault(); + + const delta = Math.sign(e.deltaY) * 3; + const currentScroll = renderer.get_scroll(); + const contentHeight = renderer.get_content_height(); + const maxScroll = Math.max(0, contentHeight - dimensions.rows); + const newScroll = Math.max(0, Math.min(maxScroll, currentScroll + delta)); + + renderer.set_scroll(newScroll); + scrollRef.current = newScroll; + }, [renderer, dimensions.rows]); + + // Handle click + const handleClick = useCallback((e: React.MouseEvent) => { + if (!renderer || !canvasRef.current) return; + + const canvas = canvasRef.current; + const rect = canvas.getBoundingClientRect(); + + // Account for CSS scaling - map display coordinates to canvas coordinates + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const canvasX = (e.clientX - rect.left) * scaleX; + const canvasY = (e.clientY - rect.top) * scaleY; + + const x = Math.floor(canvasX / CHAR_WIDTH); + const y = Math.floor(canvasY / CHAR_HEIGHT); + + const actionJson = renderer.hit_test(x, y); + if (!actionJson) return; + + try { + const action: HitAction = JSON.parse(actionJson); + + if (action.Navigate) { + router.push(action.Navigate); + } else if (action.OpenUrl) { + window.open(action.OpenUrl, '_blank', 'noopener,noreferrer'); + } + } catch (err) { + console.error('Failed to parse hit action:', err); + } + }, [renderer, router]); + + // Handle mouse move for hover effects + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!renderer || !canvasRef.current) return; + + const canvas = canvasRef.current; + const rect = canvas.getBoundingClientRect(); + + // Account for CSS scaling + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const canvasX = (e.clientX - rect.left) * scaleX; + const canvasY = (e.clientY - rect.top) * scaleY; + + const x = Math.floor(canvasX / CHAR_WIDTH); + const y = Math.floor(canvasY / CHAR_HEIGHT); + + renderer.set_hover(x, y); + + // Update cursor + const isHoverable = renderer.is_hoverable(x, y); + canvas.style.cursor = isHoverable ? 'pointer' : 'default'; + }, [renderer]); + + // Set up wheel event listener + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + canvas.addEventListener('wheel', handleWheel, { passive: false }); + return () => canvas.removeEventListener('wheel', handleWheel); + }, [handleWheel]); + + if (loading) { + return ( +
+
+ Loading ASCII renderer... +
+
+ ); + } + + if (error) { + return ( +
+
+ Failed to load renderer: {error.message} +
+
+ ); + } + + return ( +
+ +
+ ); +} + +// Convert packed RGBA to CSS color string +function colorToRgba(packed: number): string { + const r = packed & 0xff; + const g = (packed >> 8) & 0xff; + const b = (packed >> 16) & 0xff; + const a = ((packed >> 24) & 0xff) / 255; + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +export default AsciiCanvas; diff --git a/components/AsciiCanvas/index.ts b/components/AsciiCanvas/index.ts new file mode 100644 index 0000000..5058d1d --- /dev/null +++ b/components/AsciiCanvas/index.ts @@ -0,0 +1,3 @@ +export { AsciiCanvas } from './AsciiCanvas'; +export type { SiteContent, ProjectData, ExperienceData, ActivityPoint, HitAction } from './types'; +export type { Renderer as AsciiRenderer } from 'lib/wasm/ascii_renderer'; diff --git a/components/AsciiCanvas/types.ts b/components/AsciiCanvas/types.ts new file mode 100644 index 0000000..81c5973 --- /dev/null +++ b/components/AsciiCanvas/types.ts @@ -0,0 +1,72 @@ +// TypeScript types for the ASCII renderer + +export interface ActivityPoint { + x: number; + y: number; + name?: string; +} + +export interface ProjectData { + name: string; + link: string | null; + org: string; + date: string; + blurb: string; + imageId: string; +} + +export interface ExperienceData { + workplace: string; + location: string; + position: string; + timeframe: string; + description: string; +} + +export interface EducationData { + name: string; + location: string; + details: string; +} + +export interface HeaderData { + name: string; + title: string; + location: string; + profileImageId: string; + activity: ActivityPoint[]; +} + +export interface NavItem { + label: string; + path: string; +} + +export interface FooterData { + credits: string; + socialLinks: string[]; + sourceUrl: string; +} + +export type PageType = 'projects' | 'resume'; + +export interface SiteContent { + page: PageType; + header: HeaderData; + navigation: NavItem[]; + activePath: string; + projects?: ProjectData[]; + education?: EducationData[]; + experiences?: ExperienceData[]; + footer: FooterData; +} + +export interface HitAction { + Navigate?: string; + OpenUrl?: string; + ScrollTo?: string; + Custom?: string; +} + +// Re-export the Renderer type from the WASM module +export type { Renderer as AsciiRenderer } from 'lib/wasm/ascii_renderer'; diff --git a/components/AsciiCanvas/useImages.ts b/components/AsciiCanvas/useImages.ts new file mode 100644 index 0000000..913a6ce --- /dev/null +++ b/components/AsciiCanvas/useImages.ts @@ -0,0 +1,117 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import type { Renderer } from 'lib/wasm/ascii_renderer'; + +interface ImageInfo { + id: string; + url: string; +} + +interface UseImagesResult { + loadImage: (id: string, url: string) => void; + imagesLoaded: Set; + loading: boolean; +} + +export function useImages(renderer: Renderer | null): UseImagesResult { + const [imagesLoaded, setImagesLoaded] = useState>(new Set()); + const [loading, setLoading] = useState(false); + const pendingImages = useRef>(new Map()); + const loadingImages = useRef>(new Set()); + + const loadImage = useCallback((id: string, url: string) => { + if (imagesLoaded.has(id) || loadingImages.current.has(id)) { + return; + } + pendingImages.current.set(id, url); + }, [imagesLoaded]); + + useEffect(() => { + if (!renderer) return; + + async function loadPendingImages() { + const pending = Array.from(pendingImages.current.entries()); + if (pending.length === 0) return; + + setLoading(true); + pendingImages.current.clear(); + + for (const [id, url] of pending) { + if (loadingImages.current.has(id)) continue; + loadingImages.current.add(id); + + try { + const imageData = await loadImageData(url); + if (imageData) { + renderer.load_image( + id, + imageData.data, + imageData.width, + imageData.height + ); + setImagesLoaded(prev => new Set([...prev, id])); + } + } catch (err) { + console.error(`Failed to load image ${id}:`, err); + } finally { + loadingImages.current.delete(id); + } + } + + setLoading(false); + } + + const interval = setInterval(loadPendingImages, 100); + return () => clearInterval(interval); + }, [renderer]); + + return { loadImage, imagesLoaded, loading }; +} + +async function loadImageData(url: string): Promise<{ data: Uint8Array; width: number; height: number } | null> { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + // Create a canvas to extract pixel data + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + + // Scale down large images for better ASCII conversion + const maxDim = 200; + let width = img.width; + let height = img.height; + + if (width > maxDim || height > maxDim) { + const ratio = Math.min(maxDim / width, maxDim / height); + width = Math.floor(width * ratio); + height = Math.floor(height * ratio); + } + + canvas.width = width; + canvas.height = height; + + ctx.drawImage(img, 0, 0, width, height); + + const imageData = ctx.getImageData(0, 0, width, height); + resolve({ + data: new Uint8Array(imageData.data.buffer), + width, + height, + }); + }; + + img.onerror = () => { + reject(new Error(`Failed to load image: ${url}`)); + }; + + img.src = url; + }); +} + +export default useImages; diff --git a/components/AsciiCanvas/useWasm.ts b/components/AsciiCanvas/useWasm.ts new file mode 100644 index 0000000..7996d74 --- /dev/null +++ b/components/AsciiCanvas/useWasm.ts @@ -0,0 +1,50 @@ +import { useState, useEffect, useRef } from 'react'; +import type { Renderer } from 'lib/wasm/ascii_renderer'; + +interface UseWasmResult { + renderer: Renderer | null; + loading: boolean; + error: Error | null; +} + +export function useWasm(cols: number, rows: number): UseWasmResult { + const [renderer, setRenderer] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const initRef = useRef(false); + + useEffect(() => { + if (initRef.current) return; + initRef.current = true; + + async function initWasm() { + try { + // Dynamic import the WASM loader + const { create_renderer } = await import('lib/wasm'); + + // Create renderer instance + const instance = create_renderer(cols, rows); + setRenderer(instance); + setLoading(false); + } catch (err) { + console.error('Failed to initialize WASM:', err); + setError(err instanceof Error ? err : new Error(String(err))); + setLoading(false); + } + } + + initWasm(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Handle resize + useEffect(() => { + if (renderer && cols > 0 && rows > 0) { + renderer.resize(cols, rows); + } + }, [renderer, cols, rows]); + + return { renderer, loading, error }; +} + +export default useWasm; diff --git a/components/CustomLabel.tsx b/components/CustomLabel.tsx deleted file mode 100644 index 0a93101..0000000 --- a/components/CustomLabel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -type CustomLabelProps = { - viewBox?: { - width: number, - height: number, - x: number, - y: number, - }, - lines: string[], - mobile: boolean, -}; - -const CustomLabel = ({ - viewBox, - lines, - mobile -}: CustomLabelProps) => ( - - - {lines.map((line, index) => ( - {line} - ))} - - {mobile - ? null - : ( - - - - )} - -); - -export default CustomLabel; diff --git a/components/Footer.tsx b/components/Footer.tsx deleted file mode 100644 index 0ec47e8..0000000 --- a/components/Footer.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { SocialIcon } from 'react-social-icons'; -import { isMobile } from 'react-device-detect'; -import classes from 'utils/classes'; - -import styles from 'styles/Footer.module.scss'; - -const renderSocials = (socials: string[]) => ( -
- {socials.map((link) => ( - - ))} -
-); - -const renderSource = () => ( - -); - -const renderCredits = () => ( -
- Designed and Developed by Jai K. Smith (2020) -
-); - -type FooterProps = { - socialMedia: string[], -} - -const Footer = ({ - socialMedia, -}: FooterProps) => ( -
- {isMobile - ? ( - <> -
- {renderCredits()} - {renderSource()} -
-
- {renderSocials(socialMedia)} -
- - ) : ( - <> - {renderCredits()} - {renderSocials(socialMedia)} - {renderSource()} - - )} -
-); - -export default Footer; diff --git a/components/Header.tsx b/components/Header.tsx deleted file mode 100644 index 3850412..0000000 --- a/components/Header.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { GetStaticProps } from 'next'; -import { - ResponsiveContainer, AreaChart, Area, ReferenceDot, XAxis, Label -} from 'recharts'; -import { isMobile } from 'react-device-detect'; -import OnVisible from 'react-on-visible'; -import { Datapoint } from 'utils/activity'; -import classes from 'utils/classes'; - -import CustomLabel from 'components/CustomLabel'; - -import styles from 'styles/Header.module.scss'; - -export type HeaderProps = { - activity: Datapoint[], -}; - -const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - -const Header = ({ - activity, -}: HeaderProps) => { - const labeledActivity = activity.map((datapoint, idx) => { - const displayDate = new Date(datapoint.x); - // Label every 10th datapoint for cleaner spacing (approximately every 2 weeks for 50 bins over 6 months) - return { - ...datapoint, - name: idx % 10 === 0 - ? `${MONTHS[displayDate.getMonth()]} ${displayDate.getDate()}` - : '' - }; - }); - - // calc total activity over period - let totalActivity = labeledActivity.reduce((sum, val) => sum + val.y, 0); - - // calc highest isolated datapoint (used to scale const base value) - const highestIsolated = labeledActivity.reduce((max, val) => Math.max(max, val.y), 0); - - return ( -
- - - ({...d, y: d.y + (.25 * highestIsolated)}))} - margin={{ - top: 20, - right: isMobile ? 140 : 170, - bottom: 10, - left: 0, - }} - > - - - - - - - - - - - - - -
- - - - Self Portrait - -
-
- Jai K. Smith -
-
- Software Engineer, Dartmouth Alum
- New York, NY -
-
-
-
- ); -}; - -export default Header; diff --git a/components/NavBar.tsx b/components/NavBar.tsx deleted file mode 100644 index 5b66478..0000000 --- a/components/NavBar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import classes from 'utils/classes'; - -import styles from 'styles/NavBar.module.scss'; - -const NavBar = () => { - const { pathname } = useRouter(); - - return ( -
- - Projects - - - Resume - -
- ); -} - -export default NavBar; diff --git a/components/Project.tsx b/components/Project.tsx deleted file mode 100644 index 607739a..0000000 --- a/components/Project.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { isMobile } from 'react-device-detect'; -import OnVisible from 'react-on-visible'; -import classes from 'utils/classes'; - -import styles from 'styles/Project.module.scss' - -type MultiFormatImage = { - png: string; - webp: string; -} - -type ProjectImage = { - src: MultiFormatImage; - alt: string; -} - -export type ProjectType = { - name: string; - org: string; - date: string; - blurb: string; - img: ProjectImage; - link?: string; -}; - -type ProjectProps = { - project: ProjectType; - flipped: boolean; -}; - -const Project = ({ - project, - flipped -}: ProjectProps) => ( - -
-
- {project.link - ? ( - - {project.name} - - ) : project.name} -
-
- {project.org}
- {project.date} -
-
-
- - - - - {project.img.alt} - - -
- {project.blurb} -
-
-
-); - -export default Project; diff --git a/components/Projects.tsx b/components/Projects.tsx deleted file mode 100644 index 7a871b9..0000000 --- a/components/Projects.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import generateKey from 'utils/keygen'; - -import Project, { ProjectType } from 'components/Project'; - -type ProjectsProps = { - projects: ProjectType[]; -}; - -const Projects = ({ projects }: ProjectsProps) => ( -
- {projects.map((project, index) => ( - - ))} -
-); - -export default Projects; diff --git a/components/Resume.tsx b/components/Resume.tsx deleted file mode 100644 index 3290d6c..0000000 --- a/components/Resume.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import ReactMarkdown from 'react-markdown'; -import { isMobile } from 'react-device-detect' -import OnVisible from 'react-on-visible'; -import generateKey from 'utils/keygen'; -import classes from 'utils/classes'; - -import styles from 'styles/Resume.module.scss'; -import { ResumeProps } from 'pages/resume'; - -const Resume = ({ - education, - organizations, - experiences -}: ResumeProps) => ( -
-
-
-
- Education -
- {education.map((institution) => ( -
-
- {institution.name}, {institution.location} -
- {institution.details} -
- ))} - Request full resume -
- {organizations.length > 0 && ( -
-
- Organizations -
- {organizations.map((organization) => ( -
-
- {organization.name}, {organization.location} -
- {organization.details} -
- ))} -
- )} -
-
-
- Experience -
- {experiences.map((experience) => ( - -
- {experience.workplace}, {experience.location} -
-
- {experience.position}, {experience.timeframe} -
-
- {experience.description} -
-
- ))} -
-
-); - -export default Resume; diff --git a/lib/wasm/ascii_renderer.d.ts b/lib/wasm/ascii_renderer.d.ts new file mode 100644 index 0000000..86b2fa7 --- /dev/null +++ b/lib/wasm/ascii_renderer.d.ts @@ -0,0 +1,117 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The main character buffer + */ +export class CharBuffer { + free(): void; + [Symbol.dispose](): void; + /** + * Clear the entire buffer + */ + clear(): void; + /** + * Clear dirty region tracking + */ + clear_dirty(): void; + /** + * Get packed buffer data for JavaScript + * Returns array of [char_code, fg_color, bg_color, flags] for each cell + */ + get_data(): Uint32Array; + /** + * Get buffer height + */ + height(): number; + /** + * Check if buffer has dirty regions + */ + is_dirty(): boolean; + /** + * Create a new buffer with given dimensions + */ + constructor(width: number, height: number); + /** + * Resize the buffer + */ + resize(width: number, height: number): void; + /** + * Get buffer width + */ + width(): number; +} + +/** + * The main renderer + */ +export class Renderer { + free(): void; + [Symbol.dispose](): void; + /** + * Get total content height + */ + get_content_height(): number; + /** + * Get buffer height + */ + get_height(): number; + /** + * Get number of registered hit regions (for debugging) + */ + get_hit_count(): number; + /** + * Get current scroll position + */ + get_scroll(): number; + /** + * Get buffer width + */ + get_width(): number; + /** + * Hit test at a position (returns JSON action or null) + */ + hit_test(x: number, y: number): string | undefined; + /** + * Check if a position is hoverable + */ + is_hoverable(x: number, y: number): boolean; + /** + * Load an image for ASCII conversion + */ + load_image(id: string, data: Uint8Array, width: number, height: number): void; + /** + * Create a new renderer with given dimensions + */ + constructor(cols: number, rows: number); + /** + * Render and return the buffer data + */ + render(): Uint32Array; + /** + * Resize the viewport + */ + resize(cols: number, rows: number): void; + /** + * Set the page content from JSON + */ + set_content(json: string): void; + /** + * Set hover position + */ + set_hover(x: number, y: number): void; + /** + * Set the current scroll position + */ + set_scroll(scroll_y: number): void; +} + +/** + * Create a new renderer instance + */ +export function create_renderer(cols: number, rows: number): Renderer; + +/** + * Initialize panic hook for better error messages in console + */ +export function init_panic_hook(): void; diff --git a/lib/wasm/ascii_renderer.js b/lib/wasm/ascii_renderer.js new file mode 100644 index 0000000..8279512 --- /dev/null +++ b/lib/wasm/ascii_renderer.js @@ -0,0 +1,9 @@ +/* @ts-self-types="./ascii_renderer.d.ts" */ + +import * as wasm from "./ascii_renderer_bg.wasm"; +import { __wbg_set_wasm } from "./ascii_renderer_bg.js"; +__wbg_set_wasm(wasm); +wasm.__wbindgen_start(); +export { + CharBuffer, Renderer, create_renderer, init_panic_hook +} from "./ascii_renderer_bg.js"; diff --git a/lib/wasm/ascii_renderer_bg.js b/lib/wasm/ascii_renderer_bg.js new file mode 100644 index 0000000..2eb5fbf --- /dev/null +++ b/lib/wasm/ascii_renderer_bg.js @@ -0,0 +1,423 @@ +/** + * The main character buffer + */ +export class CharBuffer { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + CharBufferFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_charbuffer_free(ptr, 0); + } + /** + * Clear the entire buffer + */ + clear() { + wasm.charbuffer_clear(this.__wbg_ptr); + } + /** + * Clear dirty region tracking + */ + clear_dirty() { + wasm.charbuffer_clear_dirty(this.__wbg_ptr); + } + /** + * Get packed buffer data for JavaScript + * Returns array of [char_code, fg_color, bg_color, flags] for each cell + * @returns {Uint32Array} + */ + get_data() { + const ret = wasm.charbuffer_get_data(this.__wbg_ptr); + var v1 = getArrayU32FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v1; + } + /** + * Get buffer height + * @returns {number} + */ + height() { + const ret = wasm.charbuffer_height(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Check if buffer has dirty regions + * @returns {boolean} + */ + is_dirty() { + const ret = wasm.charbuffer_is_dirty(this.__wbg_ptr); + return ret !== 0; + } + /** + * Create a new buffer with given dimensions + * @param {number} width + * @param {number} height + */ + constructor(width, height) { + const ret = wasm.charbuffer_new(width, height); + this.__wbg_ptr = ret >>> 0; + CharBufferFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Resize the buffer + * @param {number} width + * @param {number} height + */ + resize(width, height) { + wasm.charbuffer_resize(this.__wbg_ptr, width, height); + } + /** + * Get buffer width + * @returns {number} + */ + width() { + const ret = wasm.charbuffer_width(this.__wbg_ptr); + return ret >>> 0; + } +} +if (Symbol.dispose) CharBuffer.prototype[Symbol.dispose] = CharBuffer.prototype.free; + +/** + * The main renderer + */ +export class Renderer { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Renderer.prototype); + obj.__wbg_ptr = ptr; + RendererFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + RendererFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_renderer_free(ptr, 0); + } + /** + * Get total content height + * @returns {number} + */ + get_content_height() { + const ret = wasm.renderer_get_content_height(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get buffer height + * @returns {number} + */ + get_height() { + const ret = wasm.renderer_get_height(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get number of registered hit regions (for debugging) + * @returns {number} + */ + get_hit_count() { + const ret = wasm.renderer_get_hit_count(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get current scroll position + * @returns {number} + */ + get_scroll() { + const ret = wasm.renderer_get_scroll(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get buffer width + * @returns {number} + */ + get_width() { + const ret = wasm.renderer_get_width(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Hit test at a position (returns JSON action or null) + * @param {number} x + * @param {number} y + * @returns {string | undefined} + */ + hit_test(x, y) { + const ret = wasm.renderer_hit_test(this.__wbg_ptr, x, y); + let v1; + if (ret[0] !== 0) { + v1 = getStringFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + } + return v1; + } + /** + * Check if a position is hoverable + * @param {number} x + * @param {number} y + * @returns {boolean} + */ + is_hoverable(x, y) { + const ret = wasm.renderer_is_hoverable(this.__wbg_ptr, x, y); + return ret !== 0; + } + /** + * Load an image for ASCII conversion + * @param {string} id + * @param {Uint8Array} data + * @param {number} width + * @param {number} height + */ + load_image(id, data, width, height) { + const ptr0 = passStringToWasm0(id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + wasm.renderer_load_image(this.__wbg_ptr, ptr0, len0, ptr1, len1, width, height); + } + /** + * Create a new renderer with given dimensions + * @param {number} cols + * @param {number} rows + */ + constructor(cols, rows) { + const ret = wasm.renderer_new(cols, rows); + this.__wbg_ptr = ret >>> 0; + RendererFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Render and return the buffer data + * @returns {Uint32Array} + */ + render() { + const ret = wasm.renderer_render(this.__wbg_ptr); + var v1 = getArrayU32FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v1; + } + /** + * Resize the viewport + * @param {number} cols + * @param {number} rows + */ + resize(cols, rows) { + wasm.renderer_resize(this.__wbg_ptr, cols, rows); + } + /** + * Set the page content from JSON + * @param {string} json + */ + set_content(json) { + const ptr0 = passStringToWasm0(json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.renderer_set_content(this.__wbg_ptr, ptr0, len0); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * Set hover position + * @param {number} x + * @param {number} y + */ + set_hover(x, y) { + wasm.renderer_set_hover(this.__wbg_ptr, x, y); + } + /** + * Set the current scroll position + * @param {number} scroll_y + */ + set_scroll(scroll_y) { + wasm.renderer_set_scroll(this.__wbg_ptr, scroll_y); + } +} +if (Symbol.dispose) Renderer.prototype[Symbol.dispose] = Renderer.prototype.free; + +/** + * Create a new renderer instance + * @param {number} cols + * @param {number} rows + * @returns {Renderer} + */ +export function create_renderer(cols, rows) { + const ret = wasm.create_renderer(cols, rows); + return Renderer.__wrap(ret); +} + +/** + * Initialize panic hook for better error messages in console + */ +export function init_panic_hook() { + wasm.init_panic_hook(); +} +export function __wbg___wbindgen_throw_be289d5034ed271b(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +} +export function __wbg_error_7534b8e9a36f1ab4(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } +} +export function __wbg_new_8a6f238a6ece86ea() { + const ret = new Error(); + return ret; +} +export function __wbg_stack_0ed75d68575b0f3c(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); +} +export function __wbindgen_cast_0000000000000001(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; +} +export function __wbindgen_init_externref_table() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); +} +const CharBufferFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_charbuffer_free(ptr >>> 0, 1)); +const RendererFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_renderer_free(ptr >>> 0, 1)); + +function getArrayU32FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint32ArrayMemory0 = null; +function getUint32ArrayMemory0() { + if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) { + cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32ArrayMemory0; +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + + +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} diff --git a/lib/wasm/ascii_renderer_bg.wasm b/lib/wasm/ascii_renderer_bg.wasm new file mode 100644 index 0000000..aef8ae7 Binary files /dev/null and b/lib/wasm/ascii_renderer_bg.wasm differ diff --git a/lib/wasm/ascii_renderer_bg.wasm.d.ts b/lib/wasm/ascii_renderer_bg.wasm.d.ts new file mode 100644 index 0000000..bbea712 --- /dev/null +++ b/lib/wasm/ascii_renderer_bg.wasm.d.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_renderer_free: (a: number, b: number) => void; +export const renderer_new: (a: number, b: number) => number; +export const renderer_resize: (a: number, b: number, c: number) => void; +export const renderer_set_scroll: (a: number, b: number) => void; +export const renderer_get_scroll: (a: number) => number; +export const renderer_get_content_height: (a: number) => number; +export const renderer_set_hover: (a: number, b: number, c: number) => void; +export const renderer_set_content: (a: number, b: number, c: number) => [number, number]; +export const renderer_load_image: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; +export const renderer_hit_test: (a: number, b: number, c: number) => [number, number]; +export const renderer_is_hoverable: (a: number, b: number, c: number) => number; +export const renderer_render: (a: number) => [number, number]; +export const renderer_get_width: (a: number) => number; +export const renderer_get_height: (a: number) => number; +export const renderer_get_hit_count: (a: number) => number; +export const __wbg_charbuffer_free: (a: number, b: number) => void; +export const charbuffer_new: (a: number, b: number) => number; +export const charbuffer_width: (a: number) => number; +export const charbuffer_height: (a: number) => number; +export const charbuffer_resize: (a: number, b: number, c: number) => void; +export const charbuffer_clear: (a: number) => void; +export const charbuffer_clear_dirty: (a: number) => void; +export const charbuffer_is_dirty: (a: number) => number; +export const charbuffer_get_data: (a: number) => [number, number]; +export const init_panic_hook: () => void; +export const create_renderer: (a: number, b: number) => number; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/lib/wasm/index.ts b/lib/wasm/index.ts new file mode 100644 index 0000000..55eda2a --- /dev/null +++ b/lib/wasm/index.ts @@ -0,0 +1,10 @@ +// WASM module loader for Next.js +// This file re-exports from the wasm-bindgen generated module + +export type { Renderer as AsciiRenderer } from './ascii_renderer_bg.js'; +export { create_renderer, Renderer } from './ascii_renderer'; + +export async function createRenderer(cols: number, rows: number) { + const { create_renderer } = await import('./ascii_renderer'); + return create_renderer(cols, rows); +} diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..2f4175a --- /dev/null +++ b/next.config.js @@ -0,0 +1,56 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + + // Use webpack for WASM support (Turbopack WASM support is experimental) + turbopack: {}, + + // Enable WebAssembly support + webpack: (config, { isServer }) => { + // Enable async WebAssembly + config.experiments = { + ...config.experiments, + asyncWebAssembly: true, + }; + + // Handle .wasm files + config.module.rules.push({ + test: /\.wasm$/, + type: 'webassembly/async', + }); + + return config; + }, + + // Optimize for production + compiler: { + removeConsole: process.env.NODE_ENV === 'production' ? { + exclude: ['error', 'warn'], + } : false, + }, + + // Headers for WASM files + async headers() { + return [ + { + source: '/wasm/:path*', + headers: [ + { + key: 'Content-Type', + value: 'application/wasm', + }, + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + { + key: 'Cross-Origin-Embedder-Policy', + value: 'require-corp', + }, + ], + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/package.json b/package.json index 524004c..1036d14 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "jaismith.dev", "author": "Jai Smith", - "version": "2.0", + "version": "3.0", "private": true, "engines": { "node": ">=18.17.0" }, "scripts": { "dev": "next dev", + "build:wasm": "bash scripts/build-wasm.sh", "build": "next build", "start": "next start", "lint": "next lint" @@ -16,16 +17,10 @@ "axios": "^1.10.0", "cheerio": "^1.1.0", "dotenv": "^17.2.0", - "next": "^15.4.1", + "next": "^16.1.5", "react": "^19.1.0", - "react-device-detect": "^2.2.3", "react-dom": "^19.1.0", - "react-ga": "^3.3.1", - "react-helmet-async": "^2.0.5", - "react-markdown": "^10.1.0", - "react-on-visible": "^1.6.0", - "react-social-icons": "^6.24.0", - "recharts": "^3.1.0" + "react-helmet-async": "^2.0.5" }, "devDependencies": { "@next/env": "^15.4.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index 4964368..1a7c487 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,65 +1,114 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { useRouter } from 'next/router'; import { Helmet, HelmetProvider } from 'react-helmet-async'; -import classes from 'utils/classes'; +import dynamic from 'next/dynamic'; -import NavBar from 'components/NavBar'; -import Footer from 'components/Footer'; +import type { SiteContent, ProjectData, ExperienceData, ActivityPoint } from 'components/AsciiCanvas'; import 'styles/globals.scss'; -function App({ Component, pageProps }) { +// Dynamically import AsciiCanvas to avoid SSR issues with WASM +const AsciiCanvas = dynamic( + () => import('components/AsciiCanvas').then(mod => mod.AsciiCanvas), + { ssr: false } +); + +interface PageProps { + activity?: ActivityPoint[]; + projects?: ProjectData[]; + education?: Array<{ name: string; location: string; details: string }>; + experiences?: ExperienceData[]; +} + +function App({ Component, pageProps }: { Component: React.ComponentType; pageProps: PageProps }) { + const router = useRouter(); const [systemDarkMode, setSystemDarkMode] = useState(false); - const darkMode = ['/resume'].includes(useRouter().pathname); + const pathname = router.pathname; + const isDarkMode = pathname === '/resume'; useEffect(() => { // Initialize based on current preference - const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - setSystemDarkMode(darkModeMediaQuery.matches); + if (typeof window !== 'undefined') { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + setSystemDarkMode(darkModeMediaQuery.matches); + + const handleChange = (e: MediaQueryListEvent) => { + setSystemDarkMode(e.matches); + }; + + darkModeMediaQuery.addEventListener('change', handleChange); + return () => darkModeMediaQuery.removeEventListener('change', handleChange); + } + }, []); - // Listen for changes in system theme - const handleChange = (e) => { - setSystemDarkMode(e.matches); + // Build the content object for AsciiCanvas + const content: SiteContent = useMemo(() => { + const isResume = pathname === '/resume'; + + return { + page: isResume ? 'resume' : 'projects', + header: { + name: 'Jai K. Smith', + title: 'Software Engineer, Dartmouth Alum', + location: 'New York, NY', + profileImageId: 'profile', + activity: pageProps.activity || [], + }, + navigation: [ + { label: 'Projects', path: '/' }, + { label: 'Resume', path: '/resume' }, + ], + activePath: pathname, + projects: pageProps.projects || [], + education: pageProps.education || [], + experiences: pageProps.experiences || [], + footer: { + credits: 'Jai K. Smith (2020)', + socialLinks: [ + 'https://github.com/jaismith', + 'https://linkedin.com/in/jaiksmith', + ], + sourceUrl: 'https://github.com/jaismith/jaismith.dev', + }, }; + }, [pathname, pageProps]); + + // Build image URLs map + const imageUrls = useMemo(() => { + const urls = new Map(); + urls.set('profile', '/media/profile-web.png'); - // Add listener for theme changes - darkModeMediaQuery.addEventListener('change', handleChange); + if (pageProps.projects) { + for (const project of pageProps.projects) { + urls.set(project.imageId, `/media/${project.imageId}.png`); + } + } - // Clean up listener on unmount - return () => { - darkModeMediaQuery.removeEventListener('change', handleChange); - }; - }, []); + return urls; + }, [pageProps.projects]); return ( -
- - - - - - - - Jai Smith - Software Engineer, Dartmouth Alum - - - -
-
+ + + + + + + + Jai Smith - Software Engineer, Dartmouth Alum + +
); -}; +} export default App; diff --git a/pages/index.tsx b/pages/index.tsx index 6f87406..a851af3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,132 +1,103 @@ -import { useEffect } from 'react'; -import { useRouter } from 'next/router'; -import ReactGA from 'react-ga'; - -import Header from 'components/Header'; -import Projects from 'components/Projects'; +import { GetStaticProps } from 'next'; import { getActivity, Datapoint } from 'utils/activity'; +import type { ProjectData, ActivityPoint } from 'components/AsciiCanvas'; + +// Map image names to IDs +const getImageId = (imgName: string) => { + const match = imgName.match(/\/media\/([^.]+)/); + return match ? match[1] : imgName; +}; -const genOptSrc = (imgName: string) => ({ - png: imgName + '.png', - webp: imgName + '.webp', -}); +interface ProjectsPageProps { + activity: ActivityPoint[]; + projects: ProjectData[]; +} -export async function getStaticProps() { +export const getStaticProps: GetStaticProps = async () => { let activity: Datapoint[] = [{ x: 0, y: 0 }]; try { activity = await getActivity(); } catch (e) { console.log('Failed to load activity:', e); } + + const projects: ProjectData[] = [ + { + name: 'Flowcast', + link: 'https://flowcast.jaismith.dev', + org: 'Personal', + date: 'January 2023 - Present', + blurb: 'Forecasting stream conditions throughout the United States using neural networks, and generating fishing reports with machine learning. A playground product to try new APIs, experiment with new serverless frameworks, and keep myself learning.', + imageId: 'flowcast', + }, + { + name: 'Live Event Advertising', + link: 'https://advertising.amazon.com', + org: 'Amazon', + date: 'October 2022 - Present', + blurb: 'Building novel solutions that extend cutting-edge ad interactivity and targeting capabilities typically found streaming TV to live sports. Scaling massive systems, and designing some of Amazon\'s first generative-AI powered adtech systems.', + imageId: 'prime-video-ads', + }, + { + name: 'Line at Dartmouth', + link: 'https://www.thedartmouth.com/article/2022/04/new-linedartmouth-app-displays-wait-times-at-campus-hotspots', + org: 'Dartmouth Capstone', + date: 'September 2021 - April 2022', + blurb: 'A mobile app that tracks wait times at popular campus dining locations and study space usage around Dartmouth. Built as a spiritual successor to "Line@KAF", Line at Dartmouth leverages the campus Wi-Fi network to anonymously monitor hotspots using device dwell time.', + imageId: 'linedartmouth', + }, + { + name: 'Skiff', + link: 'https://skiff.org', + org: 'Skiff', + date: 'December 2020 - June 2021', + blurb: 'The first fully end-to-end encrypted alternative to Google\'s collaboration suite. Complete with expiring links, password protection, and fine access controls, Skiff provides a privacy centric solution to collaborative document editing. The team raised a 3.7 million dollar round in 2021, led by Sequoia.', + imageId: 'skiff-web', + }, + { + name: 'Give Essential', + link: 'https://giveessential.org', + org: 'Give Essential', + date: 'Spring 2020 - Summer 2020', + blurb: 'An online peer-to-peer matching platform that connects essential workers to donors who have financial and household resources to share. Founded by a team of Dartmouth students during the COVID-19 pandemic, Give Essential has facilitated over $1 million in in-kind donations from all 50 states.', + imageId: 'giveessential-web', + }, + { + name: 'Dartmouth WiFi', + link: null, + org: 'DALI Lab', + date: 'Winter 2020', + blurb: 'Dartmouth College is currently undergoing a multi-million dollar campus-wide upgrade to WiFi infrastructure. In order to prioritize upcoming building upgrades in high-traffic areas, Dartmouth ITC hired the DALI Lab to build a WiFi reporting tool that taps into Dartmouth\'s networking data to track issues.', + imageId: 'wirelesstool-web', + }, + { + name: 'Fenceable', + link: null, + org: 'ENGS 021', + date: 'Fall 2019', + blurb: 'A wearable rack to facilitate easy deployment and collection of temporary electric fencing. Management-Intensive Rotational Grazing is a rapidly growing practice among organic farmers in the U.S., but no products currently exist on the market to facilitate its use. U.S. Patent Pending.', + imageId: 'fenceable-web', + }, + { + name: 'Vidya', + link: null, + org: 'Kathmandu Living Labs', + date: 'Summer 2019', + blurb: 'Produced in partnership with a local Nepali school in Kathmandu, Vidya aims to increase parent involvement in student learning. The app allows teachers to post positive feedback on student performance in a social media feed, alongside school announcements and homework assignments.', + imageId: 'kv-web', + }, + ]; + return { - props: { activity }, + props: { + activity: activity.map(d => ({ x: d.x, y: d.y })), + projects, + }, revalidate: 3600, }; -} - -export default function ProjectsPage({ activity }) { - const router = useRouter(); - - // initialize google analytics - ReactGA.initialize('UA-145221220-1'); - - useEffect(() => { - ReactGA.set({ page: router.pathname, }); - ReactGA.pageview(router.pathname); - }, [router.pathname]); +}; - return ( - <> -
- - - ); +// This page component doesn't render anything directly - _app.tsx handles everything +export default function ProjectsPage(props: ProjectsPageProps) { + return null; } diff --git a/pages/resume.tsx b/pages/resume.tsx index a9c6c7b..0d2ae76 100644 --- a/pages/resume.tsx +++ b/pages/resume.tsx @@ -1,38 +1,15 @@ -import Resume from 'components/Resume'; -import Header, { HeaderProps } from 'components/Header'; -import { GetStaticProps } from 'next/types'; -import { compareExperiencesByTimeframe, Experience } from 'utils/experience'; +import { GetStaticProps } from 'next'; import { getActivity, Datapoint } from 'utils/activity'; +import { compareExperiencesByTimeframe, Experience } from 'utils/experience'; +import type { ActivityPoint, ExperienceData } from 'components/AsciiCanvas'; -type Entity = { - name: string; - location: string; - details: string; -}; - -export type ResumeProps = { - education: Entity[], - organizations: Entity[], - experiences: Experience[] +interface ResumePageProps { + activity: ActivityPoint[]; + education: Array<{ name: string; location: string; details: string }>; + experiences: ExperienceData[]; } -const ResumePage = ({ - activity, - education, - organizations, - experiences -}: HeaderProps & ResumeProps) => ( - <> -
- - -); - -export const getStaticProps: GetStaticProps = async () => { +export const getStaticProps: GetStaticProps = async () => { let activity: Datapoint[] = [{ x: 0, y: 0 }]; try { activity = await getActivity(); @@ -48,25 +25,7 @@ export const getStaticProps: GetStaticProps = async ( } ]; - const organizations = [ - // { - // name: 'Give Essential', - // location: 'USA', - // details: 'Humanitarian Aid | Lead Engineer' - // }, - // { - // name: 'HackDartmouth', - // location: 'Hanover, NH', - // details: 'Education | Developer, Organizer' - // }, - // { - // name: 'Ledyard Canoe Club', - // location: 'Hanover, NH', - // details: 'Outdoor Rec | Flatwater Leader' - // } - ]; - - const experiences = [ + const experiences: Experience[] = [ { workplace: 'Anysphere', location: 'New York, NY', @@ -98,7 +57,7 @@ export const getStaticProps: GetStaticProps = async ( position: 'Software Engineer, Mentor, Core Staff', timeframe: 'January 2020 - July 2022', description: '- Served as lead engineer in cross-functional teams, working alongside designers and product managers with entrepreneurial partners.\n' + - '- Architected and implemented the technical foundation for multiple projects, including a web productivity app (now [bydesign](https://bydesign.io)), and Dartmouth\'s Wi-Fi reporting system used to guide a multi-million-dollar campus infrastructure upgrade.\n' + + '- Architected and implemented the technical foundation for multiple projects, including a web productivity app (now bydesign), and Dartmouth\'s Wi-Fi reporting system used to guide a multi-million-dollar campus infrastructure upgrade.\n' + '- Created an automation framework that eliminated several days of termly operational workload related to hiring and mentorship processes.\n' + '- Mentored beginner and intermediate engineers, teaching full stack frameworks and principles.', }, @@ -107,7 +66,7 @@ export const getStaticProps: GetStaticProps = async ( location: 'Kathmandu, Nepal', position: 'Software Engineer (iOS Developer)', timeframe: 'June 2019 - August 2019', - description: '- Lead iOS developer on a platform aiming to increase parent involvement in student learning at a local Nepali high school, now fully integrated with over 450 active users (responsible for the majority of the iOS user interface and frameworks).\n' + + description: '- Lead iOS developer on a platform aiming to increase parent involvement in student learning at a local Nepali high school, now fully integrated with over 450 active users.\n' + '- Learned about the roles of software development, humanitarian engineering, and open data in Nepal.', }, { @@ -115,7 +74,7 @@ export const getStaticProps: GetStaticProps = async ( location: 'USA', position: 'Lead Engineer (Full Stack)', timeframe: 'April 2020 - September 2020', - description: '- Rapidly developed an Express API integrated with Cloud Firestore that streamlined matching between essential workers and donors during COVID-19, helping the program reach thousands of people, facilitate over $100K of individual donations, and secure over $60K in funding.\n' + + description: '- Rapidly developed an Express API integrated with Cloud Firestore that streamlined matching between essential workers and donors during COVID-19.\n' + '- Built portal allowing 100+ volunteers to oversee thousands of ongoing essential worker/donor matches.', }, { @@ -124,7 +83,7 @@ export const getStaticProps: GetStaticProps = async ( position: 'Engineer', timeframe: 'September 2018 - June 2020', description: '- Helped to design and build the vehicle wiring harness for the 2019 competition vehicle.\n' + - '- Designed and manufactured module to convert raw sensor signals to CAN messages using Altium Designer, SolidWorks, and C, simplifying the wiring harness and making it easier to add new sensors to the vehicle in the future.' + '- Designed and manufactured module to convert raw sensor signals to CAN messages using Altium Designer, SolidWorks, and C.', }, { workplace: 'HackDartmouth', @@ -138,13 +97,21 @@ export const getStaticProps: GetStaticProps = async ( return { props: { - activity, + activity: activity.map(d => ({ x: d.x, y: d.y })), education, - organizations, - experiences: experiences.sort(compareExperiencesByTimeframe), + experiences: experiences.sort(compareExperiencesByTimeframe).map(exp => ({ + workplace: exp.workplace, + location: exp.location, + position: exp.position, + timeframe: exp.timeframe, + description: exp.description, + })), }, revalidate: 3600, }; }; -export default ResumePage; +// This page component doesn't render anything directly - _app.tsx handles everything +export default function ResumePage(props: ResumePageProps) { + return null; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2b67b4..93e8cf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,35 +18,17 @@ importers: specifier: ^17.2.0 version: 17.2.0 next: - specifier: ^15.4.1 - version: 15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: ^16.1.5 + version: 16.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: ^19.1.0 version: 19.1.0 - react-device-detect: - specifier: ^2.2.3 - version: 2.2.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) - react-ga: - specifier: ^3.3.1 - version: 3.3.1(prop-types@15.8.1)(react@19.1.0) react-helmet-async: specifier: ^2.0.5 version: 2.0.5(react@19.1.0) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.1.8)(react@19.1.0)(supports-color@10.0.0) - react-on-visible: - specifier: ^1.6.0 - version: 1.6.0(react@19.1.0) - react-social-icons: - specifier: ^6.24.0 - version: 6.24.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - recharts: - specifier: ^3.1.0 - version: 3.1.0(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1) devDependencies: '@next/env': specifier: ^15.4.1 @@ -87,16 +69,15 @@ importers: packages: - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} - engines: {node: '>=6.9.0'} - '@emnapi/core@1.4.4': resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==} '@emnapi/runtime@1.4.4': resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/wasi-threads@1.0.3': resolution: {integrity: sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==} @@ -158,124 +139,139 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.34.3': - resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.3': - resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.0': - resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': - resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.0': - resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.0': - resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.0': - resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.0': - resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.0': - resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.3': - resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.3': - resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.3': - resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.34.3': - resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.3': - resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': - resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': - resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.3': - resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.3': - resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.3': - resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.3': - resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -286,53 +282,56 @@ packages: '@next/env@15.4.1': resolution: {integrity: sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A==} + '@next/env@16.1.5': + resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==} + '@next/eslint-plugin-next@15.4.1': resolution: {integrity: sha512-lQnHUxN7mMksK7IxgKDIXNMWFOBmksVrjamMEURXiYfo7zgsc30lnU8u4y/MJktSh+nB80ktTQeQbWdQO6c8Ow==} - '@next/swc-darwin-arm64@15.4.1': - resolution: {integrity: sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig==} + '@next/swc-darwin-arm64@16.1.5': + resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.1': - resolution: {integrity: sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw==} + '@next/swc-darwin-x64@16.1.5': + resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.1': - resolution: {integrity: sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ==} + '@next/swc-linux-arm64-gnu@16.1.5': + resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.1': - resolution: {integrity: sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g==} + '@next/swc-linux-arm64-musl@16.1.5': + resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.1': - resolution: {integrity: sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ==} + '@next/swc-linux-x64-gnu@16.1.5': + resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.1': - resolution: {integrity: sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw==} + '@next/swc-linux-x64-musl@16.1.5': + resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.1': - resolution: {integrity: sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg==} + '@next/swc-win32-arm64-msvc@16.1.5': + resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.1': - resolution: {integrity: sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==} + '@next/swc-win32-x64-msvc@16.1.5': + resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -435,101 +434,33 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@reduxjs/toolkit@2.8.2': - resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} '@rushstack/eslint-patch@1.12.0': resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} - '@types/d3-array@3.2.1': - resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-shape@3.1.7': - resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/estree-jsx@1.0.5': - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.0.14': resolution: {integrity: sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==} '@types/react@19.1.8': resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@typescript-eslint/eslint-plugin@8.37.0': resolution: {integrity: sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -589,9 +520,6 @@ packages: resolution: {integrity: sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -768,12 +696,13 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.9.18: + resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==} + hasBin: true + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -806,25 +735,10 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -836,16 +750,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - classnames@2.5.1: - resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -853,20 +760,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -884,50 +781,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-format@3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -960,12 +813,6 @@ packages: supports-color: optional: true - decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - - decode-named-character-reference@1.2.0: - resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -981,22 +828,15 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1071,9 +911,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.39.7: - resolution: {integrity: sha512-ek/wWryKouBrZIjkwW2BFf91CWOIMvoy2AE5YYgUrfWsJQM2Su1LoLtrw8uusEpN9RfqLlV/0FVNjT0WMv8Bxw==} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1190,19 +1027,10 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-util-is-identifier-name@3.0.0: - resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1342,15 +1170,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-to-jsx-runtime@2.3.6: - resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - html-url-attributes@3.0.1: - resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} @@ -1366,9 +1185,6 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - immer@10.1.1: - resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} - immutable@5.1.3: resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} @@ -1380,33 +1196,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - - is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -1438,9 +1238,6 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} - is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1457,9 +1254,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -1476,10 +1270,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1571,9 +1361,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1582,97 +1369,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdast-util-from-markdown@2.0.2: - resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} - - mdast-util-mdx-expression@2.0.1: - resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} - - mdast-util-mdx-jsx@3.2.0: - resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} - - mdast-util-mdxjs-esm@2.0.1: - resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} - - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1711,9 +1411,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@15.4.1: - resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.1.5: + resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -1794,9 +1494,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-entities@4.0.2: - resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -1843,9 +1540,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -1856,12 +1550,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-device-detect@2.2.3: - resolution: {integrity: sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==} - peerDependencies: - react: '>= 0.14.0' - react-dom: '>= 0.14.0' - react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -1870,12 +1558,6 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-ga@3.3.1: - resolution: {integrity: sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ==} - peerDependencies: - prop-types: ^15.6.0 - react: ^15.6.2 || ^16.0 || ^17 || ^18 - react-helmet-async@2.0.5: resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==} peerDependencies: @@ -1884,35 +1566,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-markdown@10.1.0: - resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} - peerDependencies: - '@types/react': '>=18' - react: '>=18' - - react-on-visible@1.6.0: - resolution: {integrity: sha512-y1EhWlIre3xE6uFDlmoSyRkp6lkckWkCJla50GvYKMxrr0HQgItbfRdk3XYnVkobmpf1B4rHPe0Nj1iigYcpqQ==} - peerDependencies: - react: '>=15' - - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - - react-social-icons@6.24.0: - resolution: {integrity: sha512-1YlJe2TOf/UwPi2JAb8Ci7J207owP806Tpxu36o4EkB1/jLjGhi83xbCHOagoMyPozTZrPnZIGgvp1LiiWGuZw==} - peerDependencies: - react: 16.x.x || 17.x.x || 18.x.x || 19.x.x - react-dom: 16.x.x || 17.x.x || 18.x.x || 19.x.x - react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -1921,22 +1574,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - recharts@3.1.0: - resolution: {integrity: sha512-NqAqQcGBmLrfDs2mHX/bz8jJCQtG2FeXfE0GqpZmIuXIjkpIwj8sd9ad0WyvKiBKPd8ZgNG0hL85c8sFDwascw==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -1945,15 +1582,6 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.2: - resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2013,6 +1641,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2028,8 +1661,8 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - sharp@0.34.3: - resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -2056,16 +1689,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -2096,9 +1723,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -2107,12 +1731,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - style-to-js@1.1.17: - resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} - - style-to-object@1.0.9: - resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -2138,9 +1756,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2149,12 +1764,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2192,10 +1801,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@1.0.40: - resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} - hasBin: true - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -2207,44 +1812,12 @@ packages: resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} engines: {node: '>=20.18.1'} - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - - unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2282,13 +1855,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - snapshots: - '@babel/runtime@7.27.6': {} - '@emnapi/core@1.4.4': dependencies: '@emnapi/wasi-threads': 1.0.3 @@ -2300,6 +1868,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.0.3': dependencies: tslib: 2.8.1 @@ -2362,90 +1935,101 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/sharp-darwin-arm64@0.34.3': + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.3': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm64@0.34.3': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.3': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.3': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.3': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.3': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.3': + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.3': + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.4.4 + '@emnapi/runtime': 1.8.1 optional: true - '@img/sharp-win32-arm64@0.34.3': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.3': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.3': + '@img/sharp-win32-x64@0.34.5': optional: true '@napi-rs/wasm-runtime@0.2.12': @@ -2457,32 +2041,34 @@ snapshots: '@next/env@15.4.1': {} + '@next/env@16.1.5': {} + '@next/eslint-plugin-next@15.4.1': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.4.1': + '@next/swc-darwin-arm64@16.1.5': optional: true - '@next/swc-darwin-x64@15.4.1': + '@next/swc-darwin-x64@16.1.5': optional: true - '@next/swc-linux-arm64-gnu@15.4.1': + '@next/swc-linux-arm64-gnu@16.1.5': optional: true - '@next/swc-linux-arm64-musl@15.4.1': + '@next/swc-linux-arm64-musl@16.1.5': optional: true - '@next/swc-linux-x64-gnu@15.4.1': + '@next/swc-linux-x64-gnu@16.1.5': optional: true - '@next/swc-linux-x64-musl@15.4.1': + '@next/swc-linux-x64-musl@16.1.5': optional: true - '@next/swc-win32-arm64-msvc@15.4.1': + '@next/swc-win32-arm64-msvc@16.1.5': optional: true - '@next/swc-win32-x64-msvc@15.4.1': + '@next/swc-win32-x64-msvc@16.1.5': optional: true '@nodelib/fs.scandir@2.1.5': @@ -2560,26 +2146,10 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': - dependencies: - '@standard-schema/spec': 1.0.0 - '@standard-schema/utils': 0.3.0 - immer: 10.1.1 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 19.1.0 - react-redux: 9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1) - '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.12.0': {} - '@standard-schema/spec@1.0.0': {} - - '@standard-schema/utils@0.3.0': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2589,54 +2159,12 @@ snapshots: tslib: 2.8.1 optional: true - '@types/d3-array@3.2.1': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-shape@3.1.7': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - - '@types/debug@4.1.12': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree-jsx@1.0.5': - dependencies: - '@types/estree': 1.0.8 - '@types/estree@1.0.8': {} - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/ms@2.1.0': {} - '@types/node@24.0.14': dependencies: undici-types: 7.8.0 @@ -2645,12 +2173,6 @@ snapshots: dependencies: csstype: 3.1.3 - '@types/unist@2.0.11': {} - - '@types/unist@3.0.3': {} - - '@types/use-sync-external-store@0.0.6': {} - '@typescript-eslint/eslint-plugin@8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3))(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -2744,8 +2266,6 @@ snapshots: '@typescript-eslint/types': 8.37.0 eslint-visitor-keys: 4.2.1 - '@ungap/structured-clone@1.3.0': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -2915,10 +2435,10 @@ snapshots: axobject-query@4.1.0: {} - bail@2.0.2: {} - balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.18: {} + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -2955,21 +2475,11 @@ snapshots: caniuse-lite@1.0.30001727: {} - ccount@2.0.1: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - character-reference-invalid@2.0.1: {} - cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -2997,36 +2507,18 @@ snapshots: dependencies: readdirp: 4.1.2 - classnames@2.5.1: {} - client-only@0.0.1: {} - clsx@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - comma-separated-tokens@2.0.3: {} - concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -3047,44 +2539,6 @@ snapshots: csstype@3.1.3: {} - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-color@3.1.0: {} - - d3-ease@3.0.1: {} - - d3-format@3.1.0: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@3.1.0: {} - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.0 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -3117,12 +2571,6 @@ snapshots: optionalDependencies: supports-color: 10.0.0 - decimal.js-light@2.5.1: {} - - decode-named-character-reference@1.2.0: - dependencies: - character-entities: 2.0.2 - deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3139,18 +2587,12 @@ snapshots: delayed-stream@1.0.0: {} - dequal@2.0.3: {} - detect-libc@1.0.3: optional: true - detect-libc@2.0.4: + detect-libc@2.1.2: optional: true - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -3297,8 +2739,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.39.7: {} - escape-string-regexp@4.0.0: {} eslint-config-next@15.4.1(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3): @@ -3344,7 +2784,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.37.0(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9(supports-color@10.0.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.37.0(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9(supports-color@10.0.0))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0))(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0): dependencies: debug: 3.2.7(supports-color@10.0.0) optionalDependencies: @@ -3366,7 +2806,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.31.0(supports-color@10.0.0) eslint-import-resolver-node: 0.3.9(supports-color@10.0.0) - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.37.0(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9(supports-color@10.0.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.37.0(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9(supports-color@10.0.0))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0))(eslint@9.31.0(supports-color@10.0.0))(supports-color@10.0.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -3494,14 +2934,8 @@ snapshots: estraverse@5.3.0: {} - estree-util-is-identifier-name@3.0.0: {} - esutils@2.0.3: {} - eventemitter3@5.0.1: {} - - extend@3.0.2: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3650,32 +3084,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-jsx-runtime@2.3.6(supports-color@10.0.0): - dependencies: - '@types/estree': 1.0.8 - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1(supports-color@10.0.0) - mdast-util-mdx-jsx: 3.2.0(supports-color@10.0.0) - mdast-util-mdxjs-esm: 2.0.1(supports-color@10.0.0) - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.17 - unist-util-position: 5.0.0 - vfile-message: 4.0.2 - transitivePeerDependencies: - - supports-color - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - html-url-attributes@3.0.1: {} - htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -3691,8 +3099,6 @@ snapshots: ignore@7.0.5: {} - immer@10.1.1: {} - immutable@5.1.3: {} import-fresh@3.3.1: @@ -3702,36 +3108,22 @@ snapshots: imurmurhash@0.1.4: {} - inline-style-parser@0.2.4: {} - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 - internmap@2.0.3: {} - invariant@2.2.4: dependencies: loose-envify: 1.4.0 - is-alphabetical@2.0.1: {} - - is-alphanumerical@2.0.1: - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-arrayish@0.3.2: - optional: true - is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -3770,8 +3162,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-decimal@2.0.1: {} - is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -3789,8 +3179,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-hexadecimal@2.0.1: {} - is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -3802,8 +3190,6 @@ snapshots: is-number@7.0.0: {} - is-plain-obj@4.1.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -3900,238 +3286,14 @@ snapshots: lodash.merge@4.6.2: {} - longest-streak@3.1.0: {} - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 math-intrinsics@1.1.0: {} - mdast-util-from-markdown@2.0.2(supports-color@10.0.0): - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.2.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2(supports-color@10.0.0) - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-expression@2.0.1(supports-color@10.0.0): - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2(supports-color@10.0.0) - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-jsx@3.2.0(supports-color@10.0.0): - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2(supports-color@10.0.0) - mdast-util-to-markdown: 2.1.2 - parse-entities: 4.0.2 - stringify-entities: 4.0.4 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdxjs-esm@2.0.1(supports-color@10.0.0): - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2(supports-color@10.0.0) - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.0 - - mdast-util-to-hast@13.2.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.0.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - merge2@1.4.1: {} - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.2.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - - micromark-util-encode@2.0.1: {} - - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromark@4.0.2(supports-color@10.0.0): - dependencies: - '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@10.0.0) - decode-named-character-reference: 1.2.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4161,26 +3323,27 @@ snapshots: natural-compare@1.4.0: {} - next@15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): + next@16.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): dependencies: - '@next/env': 15.4.1 + '@next/env': 16.1.5 '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.18 caniuse-lite: 1.0.30001727 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.1 - '@next/swc-darwin-x64': 15.4.1 - '@next/swc-linux-arm64-gnu': 15.4.1 - '@next/swc-linux-arm64-musl': 15.4.1 - '@next/swc-linux-x64-gnu': 15.4.1 - '@next/swc-linux-x64-musl': 15.4.1 - '@next/swc-win32-arm64-msvc': 15.4.1 - '@next/swc-win32-x64-msvc': 15.4.1 + '@next/swc-darwin-arm64': 16.1.5 + '@next/swc-darwin-x64': 16.1.5 + '@next/swc-linux-arm64-gnu': 16.1.5 + '@next/swc-linux-arm64-musl': 16.1.5 + '@next/swc-linux-x64-gnu': 16.1.5 + '@next/swc-linux-x64-musl': 16.1.5 + '@next/swc-win32-arm64-msvc': 16.1.5 + '@next/swc-win32-x64-msvc': 16.1.5 sass: 1.89.2 - sharp: 0.34.3 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -4263,16 +3426,6 @@ snapshots: dependencies: callsites: 3.1.0 - parse-entities@4.0.2: - dependencies: - '@types/unist': 2.0.11 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.2.0 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -4314,20 +3467,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - property-information@7.1.0: {} - proxy-from-env@1.1.0: {} punycode@2.3.1: {} queue-microtask@1.2.3: {} - react-device-detect@2.2.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - ua-parser-js: 1.0.40 - react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -4335,11 +3480,6 @@ snapshots: react-fast-compare@3.2.2: {} - react-ga@3.3.1(prop-types@15.8.1)(react@19.1.0): - dependencies: - prop-types: 15.8.1 - react: 19.1.0 - react-helmet-async@2.0.5(react@19.1.0): dependencies: invariant: 2.2.4 @@ -4349,75 +3489,10 @@ snapshots: react-is@16.13.1: {} - react-markdown@10.1.0(@types/react@19.1.8)(react@19.1.0)(supports-color@10.0.0): - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/react': 19.1.8 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6(supports-color@10.0.0) - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.0 - react: 19.1.0 - remark-parse: 11.0.0(supports-color@10.0.0) - remark-rehype: 11.1.2 - unified: 11.0.5 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - react-on-visible@1.6.0(react@19.1.0): - dependencies: - classnames: 2.5.1 - prop-types: 15.8.1 - react: 19.1.0 - - react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - redux: 5.0.1 - - react-social-icons@6.24.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@babel/runtime': 7.27.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react@19.1.0: {} readdirp@4.1.2: {} - recharts@3.1.0(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1))(react@19.1.0) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.39.7 - eventemitter3: 5.0.1 - immer: 10.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-is: 16.13.1 - react-redux: 9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1) - reselect: 5.1.1 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.5.0(react@19.1.0) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -4438,25 +3513,6 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - remark-parse@11.0.0(supports-color@10.0.0): - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2(supports-color@10.0.0) - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.2: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 - unified: 11.0.5 - vfile: 6.0.3 - - reselect@5.1.1: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4518,6 +3574,9 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: + optional: true + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4542,34 +3601,36 @@ snapshots: shallowequal@1.1.0: {} - sharp@0.34.3: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.3 - '@img/sharp-darwin-x64': 0.34.3 - '@img/sharp-libvips-darwin-arm64': 1.2.0 - '@img/sharp-libvips-darwin-x64': 1.2.0 - '@img/sharp-libvips-linux-arm': 1.2.0 - '@img/sharp-libvips-linux-arm64': 1.2.0 - '@img/sharp-libvips-linux-ppc64': 1.2.0 - '@img/sharp-libvips-linux-s390x': 1.2.0 - '@img/sharp-libvips-linux-x64': 1.2.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - '@img/sharp-linux-arm': 0.34.3 - '@img/sharp-linux-arm64': 0.34.3 - '@img/sharp-linux-ppc64': 0.34.3 - '@img/sharp-linux-s390x': 0.34.3 - '@img/sharp-linux-x64': 0.34.3 - '@img/sharp-linuxmusl-arm64': 0.34.3 - '@img/sharp-linuxmusl-x64': 0.34.3 - '@img/sharp-wasm32': 0.34.3 - '@img/sharp-win32-arm64': 0.34.3 - '@img/sharp-win32-ia32': 0.34.3 - '@img/sharp-win32-x64': 0.34.3 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true shebang-command@2.0.0: @@ -4606,15 +3667,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - source-map-js@1.2.1: {} - space-separated-tokens@2.0.2: {} - stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -4672,23 +3726,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} - style-to-js@1.1.17: - dependencies: - style-to-object: 1.0.9 - - style-to-object@1.0.9: - dependencies: - inline-style-parser: 0.2.4 - styled-jsx@5.1.6(react@19.1.0): dependencies: client-only: 0.0.1 @@ -4702,8 +3743,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tiny-invariant@1.3.3: {} - tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) @@ -4713,10 +3752,6 @@ snapshots: dependencies: is-number: 7.0.0 - trim-lines@3.0.1: {} - - trough@2.2.0: {} - ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -4769,8 +3804,6 @@ snapshots: typescript@5.8.3: {} - ua-parser-js@1.0.40: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -4782,39 +3815,6 @@ snapshots: undici@7.11.0: {} - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - - unist-util-is@6.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.1: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - - unist-util-visit@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.0 @@ -4843,37 +3843,6 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.5.0(react@19.1.0): - dependencies: - react: 19.1.0 - - vfile-message@4.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.2 - - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.1 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.7 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -4928,5 +3897,3 @@ snapshots: word-wrap@1.2.5: {} yocto-queue@0.1.0: {} - - zwitch@2.0.4: {} diff --git a/public/wasm/ascii_renderer.d.ts b/public/wasm/ascii_renderer.d.ts new file mode 100644 index 0000000..6551cd1 --- /dev/null +++ b/public/wasm/ascii_renderer.d.ts @@ -0,0 +1,113 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The main character buffer + */ +export class CharBuffer { + free(): void; + [Symbol.dispose](): void; + /** + * Clear the entire buffer + */ + clear(): void; + /** + * Clear dirty region tracking + */ + clear_dirty(): void; + /** + * Get packed buffer data for JavaScript + * Returns array of [char_code, fg_color, bg_color, flags] for each cell + */ + get_data(): Uint32Array; + /** + * Get buffer height + */ + height(): number; + /** + * Check if buffer has dirty regions + */ + is_dirty(): boolean; + /** + * Create a new buffer with given dimensions + */ + constructor(width: number, height: number); + /** + * Resize the buffer + */ + resize(width: number, height: number): void; + /** + * Get buffer width + */ + width(): number; +} + +/** + * The main renderer + */ +export class Renderer { + free(): void; + [Symbol.dispose](): void; + /** + * Get total content height + */ + get_content_height(): number; + /** + * Get buffer height + */ + get_height(): number; + /** + * Get current scroll position + */ + get_scroll(): number; + /** + * Get buffer width + */ + get_width(): number; + /** + * Hit test at a position (returns JSON action or null) + */ + hit_test(x: number, y: number): string | undefined; + /** + * Check if a position is hoverable + */ + is_hoverable(x: number, y: number): boolean; + /** + * Load an image for ASCII conversion + */ + load_image(id: string, data: Uint8Array, width: number, height: number): void; + /** + * Create a new renderer with given dimensions + */ + constructor(cols: number, rows: number); + /** + * Render and return the buffer data + */ + render(): Uint32Array; + /** + * Resize the viewport + */ + resize(cols: number, rows: number): void; + /** + * Set the page content from JSON + */ + set_content(json: string): void; + /** + * Set hover position + */ + set_hover(x: number, y: number): void; + /** + * Set the current scroll position + */ + set_scroll(scroll_y: number): void; +} + +/** + * Create a new renderer instance + */ +export function create_renderer(cols: number, rows: number): Renderer; + +/** + * Initialize panic hook for better error messages in console + */ +export function init_panic_hook(): void; diff --git a/public/wasm/ascii_renderer.js b/public/wasm/ascii_renderer.js new file mode 100644 index 0000000..8279512 --- /dev/null +++ b/public/wasm/ascii_renderer.js @@ -0,0 +1,9 @@ +/* @ts-self-types="./ascii_renderer.d.ts" */ + +import * as wasm from "./ascii_renderer_bg.wasm"; +import { __wbg_set_wasm } from "./ascii_renderer_bg.js"; +__wbg_set_wasm(wasm); +wasm.__wbindgen_start(); +export { + CharBuffer, Renderer, create_renderer, init_panic_hook +} from "./ascii_renderer_bg.js"; diff --git a/public/wasm/ascii_renderer_bg.js b/public/wasm/ascii_renderer_bg.js new file mode 100644 index 0000000..2eb5fbf --- /dev/null +++ b/public/wasm/ascii_renderer_bg.js @@ -0,0 +1,423 @@ +/** + * The main character buffer + */ +export class CharBuffer { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + CharBufferFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_charbuffer_free(ptr, 0); + } + /** + * Clear the entire buffer + */ + clear() { + wasm.charbuffer_clear(this.__wbg_ptr); + } + /** + * Clear dirty region tracking + */ + clear_dirty() { + wasm.charbuffer_clear_dirty(this.__wbg_ptr); + } + /** + * Get packed buffer data for JavaScript + * Returns array of [char_code, fg_color, bg_color, flags] for each cell + * @returns {Uint32Array} + */ + get_data() { + const ret = wasm.charbuffer_get_data(this.__wbg_ptr); + var v1 = getArrayU32FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v1; + } + /** + * Get buffer height + * @returns {number} + */ + height() { + const ret = wasm.charbuffer_height(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Check if buffer has dirty regions + * @returns {boolean} + */ + is_dirty() { + const ret = wasm.charbuffer_is_dirty(this.__wbg_ptr); + return ret !== 0; + } + /** + * Create a new buffer with given dimensions + * @param {number} width + * @param {number} height + */ + constructor(width, height) { + const ret = wasm.charbuffer_new(width, height); + this.__wbg_ptr = ret >>> 0; + CharBufferFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Resize the buffer + * @param {number} width + * @param {number} height + */ + resize(width, height) { + wasm.charbuffer_resize(this.__wbg_ptr, width, height); + } + /** + * Get buffer width + * @returns {number} + */ + width() { + const ret = wasm.charbuffer_width(this.__wbg_ptr); + return ret >>> 0; + } +} +if (Symbol.dispose) CharBuffer.prototype[Symbol.dispose] = CharBuffer.prototype.free; + +/** + * The main renderer + */ +export class Renderer { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Renderer.prototype); + obj.__wbg_ptr = ptr; + RendererFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + RendererFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_renderer_free(ptr, 0); + } + /** + * Get total content height + * @returns {number} + */ + get_content_height() { + const ret = wasm.renderer_get_content_height(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get buffer height + * @returns {number} + */ + get_height() { + const ret = wasm.renderer_get_height(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get number of registered hit regions (for debugging) + * @returns {number} + */ + get_hit_count() { + const ret = wasm.renderer_get_hit_count(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get current scroll position + * @returns {number} + */ + get_scroll() { + const ret = wasm.renderer_get_scroll(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Get buffer width + * @returns {number} + */ + get_width() { + const ret = wasm.renderer_get_width(this.__wbg_ptr); + return ret >>> 0; + } + /** + * Hit test at a position (returns JSON action or null) + * @param {number} x + * @param {number} y + * @returns {string | undefined} + */ + hit_test(x, y) { + const ret = wasm.renderer_hit_test(this.__wbg_ptr, x, y); + let v1; + if (ret[0] !== 0) { + v1 = getStringFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + } + return v1; + } + /** + * Check if a position is hoverable + * @param {number} x + * @param {number} y + * @returns {boolean} + */ + is_hoverable(x, y) { + const ret = wasm.renderer_is_hoverable(this.__wbg_ptr, x, y); + return ret !== 0; + } + /** + * Load an image for ASCII conversion + * @param {string} id + * @param {Uint8Array} data + * @param {number} width + * @param {number} height + */ + load_image(id, data, width, height) { + const ptr0 = passStringToWasm0(id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + wasm.renderer_load_image(this.__wbg_ptr, ptr0, len0, ptr1, len1, width, height); + } + /** + * Create a new renderer with given dimensions + * @param {number} cols + * @param {number} rows + */ + constructor(cols, rows) { + const ret = wasm.renderer_new(cols, rows); + this.__wbg_ptr = ret >>> 0; + RendererFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Render and return the buffer data + * @returns {Uint32Array} + */ + render() { + const ret = wasm.renderer_render(this.__wbg_ptr); + var v1 = getArrayU32FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v1; + } + /** + * Resize the viewport + * @param {number} cols + * @param {number} rows + */ + resize(cols, rows) { + wasm.renderer_resize(this.__wbg_ptr, cols, rows); + } + /** + * Set the page content from JSON + * @param {string} json + */ + set_content(json) { + const ptr0 = passStringToWasm0(json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.renderer_set_content(this.__wbg_ptr, ptr0, len0); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * Set hover position + * @param {number} x + * @param {number} y + */ + set_hover(x, y) { + wasm.renderer_set_hover(this.__wbg_ptr, x, y); + } + /** + * Set the current scroll position + * @param {number} scroll_y + */ + set_scroll(scroll_y) { + wasm.renderer_set_scroll(this.__wbg_ptr, scroll_y); + } +} +if (Symbol.dispose) Renderer.prototype[Symbol.dispose] = Renderer.prototype.free; + +/** + * Create a new renderer instance + * @param {number} cols + * @param {number} rows + * @returns {Renderer} + */ +export function create_renderer(cols, rows) { + const ret = wasm.create_renderer(cols, rows); + return Renderer.__wrap(ret); +} + +/** + * Initialize panic hook for better error messages in console + */ +export function init_panic_hook() { + wasm.init_panic_hook(); +} +export function __wbg___wbindgen_throw_be289d5034ed271b(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +} +export function __wbg_error_7534b8e9a36f1ab4(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } +} +export function __wbg_new_8a6f238a6ece86ea() { + const ret = new Error(); + return ret; +} +export function __wbg_stack_0ed75d68575b0f3c(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); +} +export function __wbindgen_cast_0000000000000001(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; +} +export function __wbindgen_init_externref_table() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); +} +const CharBufferFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_charbuffer_free(ptr >>> 0, 1)); +const RendererFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_renderer_free(ptr >>> 0, 1)); + +function getArrayU32FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint32ArrayMemory0 = null; +function getUint32ArrayMemory0() { + if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) { + cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32ArrayMemory0; +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + + +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} diff --git a/public/wasm/ascii_renderer_bg.wasm b/public/wasm/ascii_renderer_bg.wasm new file mode 100644 index 0000000..aef8ae7 Binary files /dev/null and b/public/wasm/ascii_renderer_bg.wasm differ diff --git a/public/wasm/ascii_renderer_bg.wasm.d.ts b/public/wasm/ascii_renderer_bg.wasm.d.ts new file mode 100644 index 0000000..fa69651 --- /dev/null +++ b/public/wasm/ascii_renderer_bg.wasm.d.ts @@ -0,0 +1,34 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_renderer_free: (a: number, b: number) => void; +export const renderer_new: (a: number, b: number) => number; +export const renderer_resize: (a: number, b: number, c: number) => void; +export const renderer_set_scroll: (a: number, b: number) => void; +export const renderer_get_scroll: (a: number) => number; +export const renderer_get_content_height: (a: number) => number; +export const renderer_set_hover: (a: number, b: number, c: number) => void; +export const renderer_set_content: (a: number, b: number, c: number) => [number, number]; +export const renderer_load_image: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; +export const renderer_hit_test: (a: number, b: number, c: number) => [number, number]; +export const renderer_is_hoverable: (a: number, b: number, c: number) => number; +export const renderer_render: (a: number) => [number, number]; +export const renderer_get_width: (a: number) => number; +export const renderer_get_height: (a: number) => number; +export const __wbg_charbuffer_free: (a: number, b: number) => void; +export const charbuffer_new: (a: number, b: number) => number; +export const charbuffer_width: (a: number) => number; +export const charbuffer_height: (a: number) => number; +export const charbuffer_resize: (a: number, b: number, c: number) => void; +export const charbuffer_clear: (a: number) => void; +export const charbuffer_clear_dirty: (a: number) => void; +export const charbuffer_is_dirty: (a: number) => number; +export const charbuffer_get_data: (a: number) => [number, number]; +export const init_panic_hook: () => void; +export const create_renderer: (a: number, b: number) => number; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh new file mode 100755 index 0000000..40d2873 --- /dev/null +++ b/scripts/build-wasm.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +echo "Building WASM module..." + +# Check if wasm-pack is installed +if ! command -v wasm-pack &> /dev/null; then + echo "Installing wasm-pack..." + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +fi + +# Navigate to wasm-renderer directory +cd "$(dirname "$0")/../wasm-renderer" + +# Build the WASM module +wasm-pack build --target web --release + +# Copy output to public directory +mkdir -p ../public/wasm +cp pkg/ascii_renderer_bg.wasm ../public/wasm/ +cp pkg/ascii_renderer.js ../public/wasm/ +cp pkg/ascii_renderer.d.ts ../public/wasm/ + +echo "WASM build complete!" diff --git a/styles/Footer.module.scss b/styles/Footer.module.scss deleted file mode 100644 index 27060ea..0000000 --- a/styles/Footer.module.scss +++ /dev/null @@ -1,69 +0,0 @@ -.footer { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - height: calc(5vh + .75vw); - background-color: var(--background-color); - color: var(--text-color); - - a { - color: var(--text-color); - text-decoration: none; - } - - a:hover { - text-decoration: underline; - } - - &.mobile { - flex-direction: column; - height: fit-content; - font-size: 1.25em; - white-space: nowrap; - padding: 1em 0 1em 0; - - .footer-socials { - margin-top: .5em; - flex: 0 0 100%; - } - } -} - -.footerSocials { - display: flex; - justify-content: center; - align-items: center; - flex: 0 0 30%; - height: 75%; - overflow: hidden; - - .icon { - margin-left: 5px; - margin-right: 5px; - } -} - -.footerCredits { - flex: 0 0 30%; - text-align: left; - padding-left: 20px; -} - -.footerSource { - flex: 0 0 30%; - text-align: right; - padding-right: 20px; -} - -.footerRow { - width: 100%; - display: flex; - justify-content: space-between; -} - -@media (max-width: 1050px) { - .footerCredits span { - display: none; - } -} diff --git a/styles/Header.module.scss b/styles/Header.module.scss deleted file mode 100644 index 7f2cbeb..0000000 --- a/styles/Header.module.scss +++ /dev/null @@ -1,116 +0,0 @@ -.header { - position: relative; - top: 0; - height: 75vh; - border-bottom-style: solid; - border-bottom-width: 1px; - border-bottom-color: lightgrey; - - &.mobile { - height: 90vh; - margin-bottom: 5vh; - - .headerChart { - width: 97.5%; - } - - .headerBox { - flex-direction: column; - -ms-transform: translate(-50%, -60%); - transform: translate(-50%, -60%); - } - - .headerTitle { - padding: 40px 0 0 0; - text-align: center; - } - } -} - -.headerChart { - position: absolute; - z-index: 0; - top: 65%; - width: 80%; - height: calc(35% + 50px); - display: flex; - transform: translateY(50px); - opacity: 0; - transition: transform 0.6s 0.2s, opacity 0.6s 0.2s; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - - * { - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - } - - &.visible { - transform: translateY(0); - opacity: 1; - } -} - -.headerChartRefdot { - transition: 1s; - color: var(--text-color); - - // circle { - // fill: var(--chart-color) !important; - // } - - &.hidden { - -ms-transform: translateY(15%); - transform: translateY(15%); - opacity: 0; - } -} - -.headerPic { - height: 20vh; - min-height: 150px; - - img { - object-fit: contain; - height: 100%; - border-radius: 50%; - } -} - -.headerBox { - position: absolute; - top: 50%; - left: 50%; - -ms-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - width: fit-content; - margin: auto; - display: flex; - align-items: center; - padding: 2.5em; -} - -.headerTitle { - display: flex; - flex-direction: column; - text-align: left; - padding-left: 40px; -} - -.headerTitleMain { - font-weight: bolder; - line-height: 36pt; - font-size: 36pt; - margin: 0 0 7.5px 0; -} - -.headerTitleSub { - font-weight: lighter; - line-height: 26pt; - font-size: 18pt; - margin: 0px; -} diff --git a/styles/NavBar.module.scss b/styles/NavBar.module.scss deleted file mode 100644 index c97431d..0000000 --- a/styles/NavBar.module.scss +++ /dev/null @@ -1,26 +0,0 @@ -.nav { - position: fixed; - width: calc(100% - 3rem); - top: 0; - overflow: hidden; - z-index: 1; - padding: 0.9rem 1.5rem 1.1rem 1.5rem; - display: flex; - justify-content: flex-end; - transition: background 0.3s; - background: var(--background-color); - - a { - color: var(--text-color-nav); - } -} - -.navItem { - text-decoration: none; - font-size: 15pt; - padding-left: 1em; - - &.active { - text-decoration: underline; - } -} diff --git a/styles/Project.module.scss b/styles/Project.module.scss deleted file mode 100644 index 092d289..0000000 --- a/styles/Project.module.scss +++ /dev/null @@ -1,159 +0,0 @@ -.project, -.projectTitle, -.projectTitleMain, -.projectTitleSub, -.projectContent, -.projectContentBlurb, -.projectContentShowcase { - transition-property: transform, opacity; -} - -.project { - transition-duration: 0.6s; -} - -.projectTitle, -.projectTitleMain, -.projectTitleSub { - transition-duration: 0.5s; -} - -.projectContent, -.projectContentBlurb, -.projectContentShowcase { - transition-duration: 0.4s; - transition-delay: 0.1s; -} - -.project { - padding: 5% 7.5% 5% 7.5%; - - &:not(:first-child):not(.visible) { - opacity: 0; - transform: translateY(100px); - - .projectTitle, - .projectContent { - opacity: 0; - } - - .projectTitleMain { - transform: translateX(-50px); - } - - &:not(.flipped) .projectContentShowcase, - &.flipped .projectContentBlurb { - transform: translateX(-40px); - } - - .projectTitleSub { - transform: translateX(50px); - } - - &:not(.flipped) .projectContentBlurb, - &.flipped .projectContentShowcase { - transform: translateX(40px); - } - } - - &.flipped { - background-color: #f8f8f8; - } - - &.mobile { - padding: 5%; - - .projectTitle { - flex-direction: column; - text-align: left; - } - - .projectTitleSub { - border-top: 1px solid lightgrey; - padding-top: 10px; - text-align: left; - } - } -} - -.projectTitle { - display: flex; - justify-content: space-between; - - a { - text-decoration: none; - color: black; - border-bottom: 0px solid transparent; - display: inline-block; - line-height: 0.95; - transition: border-bottom-color 0.25s, border-bottom-width 0.15s; - - &:hover { - border-bottom-color: black; - border-bottom-width: 3px; - } - } -} - -.projectTitleMain { - font-size: 28pt; -} - -.projectTitleSub { - font-size: 18pt; - text-align: right; -} - -.projectContent { - margin-top: 5%; - display: flex; - justify-content: space-between; - align-items: center; - text-align: right; -} - -.projectContentBlurb { - width: 35%; - font-size: 18pt; - user-select: none; - cursor: default; -} - -.projectContentShowcase { - width: 60%; - - img { - object-fit: contain; - width: 100%; - } -} - -@media (min-aspect-ratio: 4/3) { - .projectContent.flipped { - flex-direction: row-reverse; - text-align: left; - } -} - -@media (max-aspect-ratio: 4/3) { - .projectContent { - flex-direction: column; - text-align: left; - align-items: stretch; - } - - .projectContentShowcase { - align-self: center; - width: 75%; - } - - .projectContentBlurb { - padding-top: 30px; - width: 100%; - } -} - -.unclickable { - pointer-events: none; - cursor: default; -} diff --git a/styles/Resume.module.scss b/styles/Resume.module.scss deleted file mode 100644 index 6f83500..0000000 --- a/styles/Resume.module.scss +++ /dev/null @@ -1,135 +0,0 @@ -.resume { - display: flex; - width: 60%; - max-width: 1300px; - margin: auto; - text-align: left; - padding: calc(3vw + 5vh) 0 calc(3vw + 5vh) 0; - - &.mobile { - width: 90%; - flex-direction: column; - - .resumeSidebar { - width: 100%; - text-align: center; - } - - .resumeSidebarHeader { - font-size: 18pt; - } - - .resumeOrganizations { - min-width: 0; - } - - .resumeExperiences { - min-width: 0; - padding-left: 0; - } - - .resumeExperiencesHeader { - font-size: 18pt; - padding-bottom: 0; - } - } - - a { - color: #80e5ff; - } -} - -.resumeSidebar { - flex: 0 0 1; - display: flex; - flex-direction: column; - color: rgba(255, 255, 255, 0.4); -} - -.resumeSidebarHeader { - font-weight: bold; - font-size: 14pt; - line-height: 14pt; - padding-bottom: 15px; - color: rgba(249, 249, 249, 0.8); -} - -.resumeEducation { - padding-bottom: 20px; - min-width: 225px; - - :nth-child(n + 2) { - padding-bottom: 2px; - } - - :nth-child(n + 3) { - padding-top: 2.5px; - margin-top: 2.5px; - } -} - -.resumeOrganizations { - padding-bottom: 20px; - min-width: 225px; - - :nth-child(n + 2) { - padding-bottom: 2px; - } - - :nth-child(n + 3) { - padding-top: 2.5px; - margin-top: 2.5px; - } -} - -.resumeExperiences { - min-width: 400px; - flex: 1 0 4; - display: flex; - flex-direction: column; - padding-left: 5vw; -} - -.resumeExperiencesHeader { - font-size: 24pt; - line-height: 24px; - font-weight: bold; - padding-bottom: 20px; -} - -.resumeExperience { - border-bottom: 1px solid lightgrey; - transition: transform 0.6s, opacity 0.6s; - - &:last-child { - border-bottom: none; - } - - &:nth-child(n + 3) { - transform: translateY(25px); - opacity: 0; - } - - &.visible { - transform: translateY(0); - opacity: 1; - } -} - -.resumeExperienceLogo { - height: 75px; - padding: 20px 0 10px 0; -} - -.resumeExperienceHeader { - font-size: 14pt; - padding-top: 20px; -} - -.resumeExperienceSummary { - color: rgba(255, 255, 255, 0.4); -} - -.resumeRequestFull { - line-height: 4; -} diff --git a/styles/globals.scss b/styles/globals.scss index 0eef8e8..e38a8d9 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -1,46 +1,33 @@ -@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap'); +/* Global styles for ASCII renderer site */ -body { +* { + box-sizing: border-box; + padding: 0; margin: 0; - font-family: "Titillium Web", -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - &.dark { - background: #181a1b; - }; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; +html, +body { + max-width: 100vw; + overflow-x: hidden; + overflow-y: hidden; + font-family: "Courier New", Consolas, Monaco, monospace; + background: #000; } -.app { - text-align: center; - min-width: 750px; - transition: background 0.3s; - - &.mobile { - min-width: 0; - } - - &.light { - --background-color: white; - --text-color: black; - --text-color-nav: black; - --chart-color: #0054B4; - } +/* Hide scrollbars globally */ +::-webkit-scrollbar { + display: none; +} - &.dark { - --background-color: #181a1b; - --text-color: rgba(249, 249, 249, 0.8); - --text-color-nav: rgb(186, 181, 171); - --chart-color: rgb(96, 182, 255); - } +html { + -ms-overflow-style: none; + scrollbar-width: none; +} - background: var(--background-color); - color: var(--text-color); +/* Ensure canvas fills the viewport */ +#__next { + width: 100vw; + height: 100vh; + overflow: hidden; } diff --git a/tsconfig.json b/tsconfig.json index c74112a..efdfccf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,13 @@ { "compilerOptions": { "baseUrl": "./", - "target": "es5", + "target": "es2015", "lib": [ "dom", "dom.iterable", "esnext" ], + "downlevelIteration": true, "allowJs": true, "skipLibCheck": true, "strict": false, @@ -17,7 +18,7 @@ "esModuleInterop": true, "module": "esnext", "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true }, "include": [ diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..7cf30b0 --- /dev/null +++ b/vercel.json @@ -0,0 +1,19 @@ +{ + "buildCommand": "pnpm build", + "framework": "nextjs", + "headers": [ + { + "source": "/wasm/(.*)", + "headers": [ + { + "key": "Content-Type", + "value": "application/wasm" + }, + { + "key": "Cache-Control", + "value": "public, max-age=31536000, immutable" + } + ] + } + ] +} diff --git a/wasm-renderer/Cargo.lock b/wasm-renderer/Cargo.lock new file mode 100644 index 0000000..3c501ce --- /dev/null +++ b/wasm-renderer/Cargo.lock @@ -0,0 +1,436 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ascii-renderer" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea" + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/wasm-renderer/Cargo.toml b/wasm-renderer/Cargo.toml new file mode 100644 index 0000000..dcfa0d4 --- /dev/null +++ b/wasm-renderer/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "ascii-renderer" +version = "0.1.0" +edition = "2021" +authors = ["Jai Smith"] +description = "High-performance ASCII renderer for web" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.92" +js-sys = "0.3.69" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# The `console_error_panic_hook` crate provides better debugging of panics +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dependencies.web-sys] +version = "0.3.69" +features = [ + "console", +] + +[dev-dependencies] +wasm-bindgen-test = "0.3.42" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" +lto = true diff --git a/wasm-renderer/src/buffer.rs b/wasm-renderer/src/buffer.rs new file mode 100644 index 0000000..dd90589 --- /dev/null +++ b/wasm-renderer/src/buffer.rs @@ -0,0 +1,248 @@ +use wasm_bindgen::prelude::*; + +/// A single character cell with its properties +#[derive(Clone, Copy, Debug)] +pub struct CharCell { + /// The character to display + pub ch: char, + /// Foreground color as RGBA packed into u32 + pub fg_color: u32, + /// Background color as RGBA packed into u32 (0 = transparent) + pub bg_color: u32, + /// Flags: bit 0 = bold, bit 1 = underline, bit 2 = clickable + pub flags: u8, +} + +impl Default for CharCell { + fn default() -> Self { + Self { + ch: ' ', + fg_color: 0xFF000000, // Black, fully opaque + bg_color: 0, // Transparent + flags: 0, + } + } +} + +impl CharCell { + pub fn new(ch: char, fg_color: u32) -> Self { + Self { + ch, + fg_color, + bg_color: 0, + flags: 0, + } + } + + pub fn with_bg(mut self, bg_color: u32) -> Self { + self.bg_color = bg_color; + self + } + + pub fn bold(mut self) -> Self { + self.flags |= 0x01; + self + } + + pub fn underline(mut self) -> Self { + self.flags |= 0x02; + self + } + + pub fn clickable(mut self) -> Self { + self.flags |= 0x04; + self + } + + pub fn is_bold(&self) -> bool { + self.flags & 0x01 != 0 + } + + pub fn is_underline(&self) -> bool { + self.flags & 0x02 != 0 + } + + pub fn is_clickable(&self) -> bool { + self.flags & 0x04 != 0 + } +} + +/// Color utilities +pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> u32 { + ((a as u32) << 24) | ((b as u32) << 16) | ((g as u32) << 8) | (r as u32) +} + +pub fn rgb(r: u8, g: u8, b: u8) -> u32 { + rgba(r, g, b, 255) +} + +pub fn unpack_rgba(color: u32) -> (u8, u8, u8, u8) { + let r = (color & 0xFF) as u8; + let g = ((color >> 8) & 0xFF) as u8; + let b = ((color >> 16) & 0xFF) as u8; + let a = ((color >> 24) & 0xFF) as u8; + (r, g, b, a) +} + +/// The main character buffer +#[wasm_bindgen] +pub struct CharBuffer { + width: u32, + height: u32, + cells: Vec, + /// Dirty region tracking (min_x, min_y, max_x, max_y) + dirty_region: Option<(u32, u32, u32, u32)>, +} + +#[wasm_bindgen] +impl CharBuffer { + /// Create a new buffer with given dimensions + #[wasm_bindgen(constructor)] + pub fn new(width: u32, height: u32) -> Self { + let size = (width * height) as usize; + Self { + width, + height, + cells: vec![CharCell::default(); size], + dirty_region: None, + } + } + + /// Get buffer width + pub fn width(&self) -> u32 { + self.width + } + + /// Get buffer height + pub fn height(&self) -> u32 { + self.height + } + + /// Resize the buffer + pub fn resize(&mut self, width: u32, height: u32) { + if self.width == width && self.height == height { + return; + } + self.width = width; + self.height = height; + let size = (width * height) as usize; + self.cells = vec![CharCell::default(); size]; + self.dirty_region = Some((0, 0, width, height)); + } + + /// Clear the entire buffer + pub fn clear(&mut self) { + for cell in &mut self.cells { + *cell = CharCell::default(); + } + self.dirty_region = Some((0, 0, self.width, self.height)); + } + + /// Get the index for a position + #[inline] + fn index(&self, x: u32, y: u32) -> Option { + if x < self.width && y < self.height { + Some((y * self.width + x) as usize) + } else { + None + } + } + + /// Mark a region as dirty + fn mark_dirty(&mut self, x: u32, y: u32) { + if let Some((min_x, min_y, max_x, max_y)) = self.dirty_region { + self.dirty_region = Some(( + min_x.min(x), + min_y.min(y), + max_x.max(x + 1), + max_y.max(y + 1), + )); + } else { + self.dirty_region = Some((x, y, x + 1, y + 1)); + } + } + + /// Clear dirty region tracking + pub fn clear_dirty(&mut self) { + self.dirty_region = None; + } + + /// Check if buffer has dirty regions + pub fn is_dirty(&self) -> bool { + self.dirty_region.is_some() + } + + /// Get packed buffer data for JavaScript + /// Returns array of [char_code, fg_color, bg_color, flags] for each cell + pub fn get_data(&self) -> Vec { + let mut data = Vec::with_capacity(self.cells.len() * 4); + for cell in &self.cells { + data.push(cell.ch as u32); + data.push(cell.fg_color); + data.push(cell.bg_color); + data.push(cell.flags as u32); + } + data + } +} + +// Non-wasm methods +impl CharBuffer { + /// Set a character at position + pub fn set_cell(&mut self, x: u32, y: u32, cell: CharCell) { + if let Some(idx) = self.index(x, y) { + self.cells[idx] = cell; + self.mark_dirty(x, y); + } + } + + /// Get a character at position + pub fn get_cell(&self, x: u32, y: u32) -> Option<&CharCell> { + self.index(x, y).map(|idx| &self.cells[idx]) + } + + /// Get mutable reference to a cell + pub fn get_cell_mut(&mut self, x: u32, y: u32) -> Option<&mut CharCell> { + if let Some(idx) = self.index(x, y) { + self.mark_dirty(x, y); + Some(&mut self.cells[idx]) + } else { + None + } + } + + /// Set a character with default styling + pub fn set_char(&mut self, x: u32, y: u32, ch: char, fg_color: u32) { + self.set_cell(x, y, CharCell::new(ch, fg_color)); + } + + /// Fill a region with a character + pub fn fill_region(&mut self, x: u32, y: u32, w: u32, h: u32, cell: CharCell) { + for dy in 0..h { + for dx in 0..w { + self.set_cell(x + dx, y + dy, cell); + } + } + } + + /// Fill region with a single color background + pub fn fill_bg(&mut self, x: u32, y: u32, w: u32, h: u32, bg_color: u32) { + for dy in 0..h { + for dx in 0..w { + if let Some(cell) = self.get_cell_mut(x + dx, y + dy) { + cell.bg_color = bg_color; + } + } + } + } + + /// Clear a region + pub fn clear_region(&mut self, x: u32, y: u32, w: u32, h: u32) { + self.fill_region(x, y, w, h, CharCell::default()); + } + + /// Get dirty region bounds + pub fn dirty_bounds(&self) -> Option<(u32, u32, u32, u32)> { + self.dirty_region + } +} diff --git a/wasm-renderer/src/chart.rs b/wasm-renderer/src/chart.rs new file mode 100644 index 0000000..99a1abb --- /dev/null +++ b/wasm-renderer/src/chart.rs @@ -0,0 +1,251 @@ +//! ASCII chart rendering for data visualization + +use crate::buffer::{CharBuffer, CharCell}; +use crate::text::{render_text, TextStyle}; + +/// A data point for charts +#[derive(Clone, Debug)] +pub struct DataPoint { + pub x: f64, + pub y: f64, + pub label: Option, +} + +/// Chart configuration +#[derive(Clone, Debug)] +pub struct ChartConfig { + pub width: u32, + pub height: u32, + pub show_axes: bool, + pub show_labels: bool, + pub fill_area: bool, + pub title: Option, +} + +impl Default for ChartConfig { + fn default() -> Self { + Self { + width: 60, + height: 15, + show_axes: true, + show_labels: true, + fill_area: true, + title: None, + } + } +} + +/// Characters for chart rendering +const CHART_FILL: char = '█'; +const CHART_TOP: char = '▀'; +const CHART_LINE: char = '─'; +const CHART_DOT: char = '●'; +const CHART_AXIS_V: char = '│'; +const CHART_AXIS_H: char = '─'; +const CHART_ORIGIN: char = '└'; + +/// Render an area chart +pub fn render_area_chart( + buffer: &mut CharBuffer, + x: u32, + y: u32, + data: &[DataPoint], + config: &ChartConfig, + style: &TextStyle, +) -> (u32, u32) { + if data.is_empty() || config.width == 0 || config.height == 0 { + return (0, 0); + } + + let chart_x = if config.show_axes { x + 3 } else { x }; + let chart_y = y; + let chart_width = if config.show_axes { config.width - 3 } else { config.width }; + let chart_height = if config.show_labels { config.height - 2 } else { config.height }; + + // Find data bounds + let min_y = data.iter().map(|d| d.y).fold(f64::INFINITY, f64::min); + let max_y = data.iter().map(|d| d.y).fold(f64::NEG_INFINITY, f64::max); + let y_range = if max_y > min_y { max_y - min_y } else { 1.0 }; + + // Normalize data to chart height + let normalized: Vec = data.iter().map(|d| { + let norm = (d.y - min_y) / y_range; + (norm * (chart_height - 1) as f64).round() as u32 + }).collect(); + + // Calculate x positions for each data point + let x_step = chart_width as f64 / (data.len().max(1) - 1).max(1) as f64; + + // Render the area/line + for (i, &height) in normalized.iter().enumerate() { + let px = chart_x + (i as f64 * x_step).round() as u32; + + if px >= chart_x + chart_width { + continue; + } + + // Fill from bottom to height + if config.fill_area { + for h in 0..=height { + let py = chart_y + chart_height - 1 - h; + if py >= chart_y && py < chart_y + chart_height { + let ch = if h == height { CHART_TOP } else { CHART_FILL }; + buffer.set_cell(px, py, CharCell::new(ch, style.fg_color)); + } + } + } else { + // Just draw the point + let py = chart_y + chart_height - 1 - height; + if py >= chart_y && py < chart_y + chart_height { + buffer.set_cell(px, py, CharCell::new(CHART_DOT, style.fg_color)); + } + } + } + + // Draw axes if enabled + if config.show_axes { + // Y axis + for row in chart_y..(chart_y + chart_height) { + buffer.set_cell(chart_x - 1, row, CharCell::new(CHART_AXIS_V, style.fg_color)); + } + // X axis + for col in chart_x..(chart_x + chart_width) { + buffer.set_cell(col, chart_y + chart_height, CharCell::new(CHART_AXIS_H, style.fg_color)); + } + // Origin + buffer.set_cell(chart_x - 1, chart_y + chart_height, CharCell::new(CHART_ORIGIN, style.fg_color)); + } + + // Draw labels if enabled + if config.show_labels && !data.is_empty() { + // First label + if let Some(ref label) = data[0].label { + let label_y = chart_y + chart_height + 1; + render_text(buffer, chart_x, label_y, label, style); + } + // Last label + if data.len() > 1 { + if let Some(ref label) = data[data.len() - 1].label { + let label_y = chart_y + chart_height + 1; + let label_x = (chart_x + chart_width).saturating_sub(label.len() as u32); + render_text(buffer, label_x, label_y, label, style); + } + } + } + + (config.width, config.height) +} + +/// Render a reference dot with label (like for showing current value) +pub fn render_reference_dot( + buffer: &mut CharBuffer, + x: u32, + y: u32, + label_lines: &[&str], + style: &TextStyle, +) { + // Draw the dot + buffer.set_cell(x, y, CharCell::new(CHART_DOT, style.fg_color)); + + // Draw label lines to the right + for (i, line) in label_lines.iter().enumerate() { + render_text(buffer, x + 2, y + i as u32, line, style); + } +} + +/// Render a simple bar chart (horizontal) +pub fn render_bar_chart( + buffer: &mut CharBuffer, + x: u32, + y: u32, + data: &[(String, f64)], + max_width: u32, + style: &TextStyle, +) -> u32 { + if data.is_empty() { + return 0; + } + + let max_val = data.iter().map(|(_, v)| *v).fold(f64::NEG_INFINITY, f64::max); + let max_label_len = data.iter().map(|(l, _)| l.len()).max().unwrap_or(0); + + for (i, (label, value)) in data.iter().enumerate() { + let row = y + i as u32; + + // Render label + render_text(buffer, x, row, label, style); + + // Render bar + let bar_x = x + max_label_len as u32 + 2; + let bar_width = if max_val > 0.0 { + ((value / max_val) * (max_width - max_label_len as u32 - 2) as f64) as u32 + } else { + 0 + }; + + for bx in 0..bar_width { + buffer.set_cell(bar_x + bx, row, CharCell::new('█', style.fg_color)); + } + } + + data.len() as u32 +} + +/// Simple sparkline chart (single line, minimal) +pub fn render_sparkline( + buffer: &mut CharBuffer, + x: u32, + y: u32, + data: &[f64], + width: u32, + style: &TextStyle, +) { + if data.is_empty() || width == 0 { + return; + } + + let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + + let min_val = data.iter().fold(f64::INFINITY, |a, &b| a.min(b)); + let max_val = data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + let range = if max_val > min_val { max_val - min_val } else { 1.0 }; + + // Resample data to fit width + let step = data.len() as f64 / width as f64; + + for i in 0..width { + let data_idx = (i as f64 * step) as usize; + if data_idx >= data.len() { + break; + } + + let val = data[data_idx]; + let norm = (val - min_val) / range; + let char_idx = (norm * 7.0).round() as usize; + let ch = chars[char_idx.min(7)]; + + buffer.set_cell(x + i, y, CharCell::new(ch, style.fg_color)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_area_chart() { + let mut buffer = CharBuffer::new(80, 20); + let data = vec![ + DataPoint { x: 0.0, y: 10.0, label: Some("Start".to_string()) }, + DataPoint { x: 1.0, y: 20.0, label: None }, + DataPoint { x: 2.0, y: 15.0, label: None }, + DataPoint { x: 3.0, y: 25.0, label: Some("End".to_string()) }, + ]; + let config = ChartConfig::default(); + let style = TextStyle::default(); + + let (w, h) = render_area_chart(&mut buffer, 0, 0, &data, &config, &style); + assert!(w > 0); + assert!(h > 0); + } +} diff --git a/wasm-renderer/src/fonts/mod.rs b/wasm-renderer/src/fonts/mod.rs new file mode 100644 index 0000000..1b00972 --- /dev/null +++ b/wasm-renderer/src/fonts/mod.rs @@ -0,0 +1,480 @@ +//! Embedded FIGlet-style fonts for ASCII art text rendering + +/// A FIGlet character definition +#[derive(Clone, Debug)] +pub struct FigChar { + /// Lines of the character (each line is a string of characters) + pub lines: Vec, + /// Width of this character + pub width: usize, +} + +/// A FIGlet font +#[derive(Clone, Debug)] +pub struct FigFont { + /// Height of the font in lines + pub height: usize, + /// Baseline offset from top + pub baseline: usize, + /// Character definitions (ASCII 32-126) + pub chars: Vec>, + /// Hard blank character + pub hardblank: char, +} + +impl FigFont { + /// Get a character definition + pub fn get_char(&self, c: char) -> Option<&FigChar> { + let idx = c as usize; + if idx >= 32 && idx <= 126 { + self.chars.get(idx - 32).and_then(|o| o.as_ref()) + } else { + None + } + } + + /// Calculate the width of rendered text + pub fn text_width(&self, text: &str) -> usize { + text.chars() + .filter_map(|c| self.get_char(c)) + .map(|fc| fc.width) + .sum() + } +} + +/// Built-in "Small" font - a compact 5-line FIGlet-style font +pub fn small_font() -> FigFont { + let height = 5; + let hardblank = '$'; + + // Define characters for ASCII 32-126 + // Each character is defined as height lines + let char_defs: Vec<(&str, Vec<&str>)> = vec![ + // Space (32) + (" ", vec![" ", " ", " ", " ", " "]), + // ! (33) + ("!", vec![" _ ", "| |", "|_|", "(_)", " "]), + // " (34) + ("\"", vec![" _ _ ", "( | )", " V V ", " ", " "]), + // # (35) + ("#", vec![" _ _ ", "_| || |_", " _ _ |", "| || |_ ", " |_||_|"]), + // $ (36) + ("$", vec![" _|_ ", "/ __)", "\\__ \\", "(___/", " | "]), + // % (37) + ("%", vec!["_ _", "(_)/ ", " / ", " /(_)", " "]), + // & (38) + ("&", vec![" _ ", " / \\ ", "/ _ \\", "\\ (_/", " \\__/"]), + // ' (39) + ("'", vec![" _ ", "( )", " V ", " ", " "]), + // ( (40) + ("(", vec![" _", " / ", "| |", " \\_", " "]), + // ) (41) + (")", vec!["_ ", " \\ ", "| |", "_/ ", " "]), + // * (42) + ("*", vec![" ", "_/\\_", "\\ /", "/_/\\", " "]), + // + (43) + ("+", vec![" ", " _ ", "|+| ", " T ", " "]), + // , (44) + (",", vec![" ", " ", " ", " _ ", "( )"]), + // - (45) + ("-", vec![" ", " ", "____", " ", " "]), + // . (46) + (".", vec![" ", " ", " ", " _ ", "(_)"]), + // / (47) + ("/", vec![" _", " / ", " / ", " / ", "/ "]), + // 0 (48) + ("0", vec![" ___ ", "/ _ \\ ", "| |/ /", "|_/\\ \\", "\\___/ "]), + // 1 (49) + ("1", vec![" _ ", "/_ |", " | |", " | |", " |_|"]), + // 2 (50) + ("2", vec![" ___ ", "|__ \\ ", " / / ", " / /_ ", "|____|"]), + // 3 (51) + ("3", vec![" ____", "|___ \\", " __) ", " |__ <", " ___) ", "|____/"]), + // 4 (52) + ("4", vec!["__ __", "| || |", "| || |_", "|__ _|", " |_|"]), + // 5 (53) + ("5", vec![" _____ ", "| ___|", "|___ \\ ", " ___) |", "|____/ "]), + // 6 (54) + ("6", vec![" __ ", " / / ", "/ /_ ", "| '_ \\", "| (_) |", " \\___/"]), + // 7 (55) + ("7", vec![" _____ ", "|___ |", " / / ", " / / ", " /_/ "]), + // 8 (56) + ("8", vec![" ___ ", "( _ ) ", "/ _ \\ ", "| (_) |", " \\___/ "]), + // 9 (57) + ("9", vec![" ___ ", "/ _ \\ ", "| (_) |", " \\__, |", " /_/ "]), + // : (58) + (":", vec![" ", " _ ", "(_)", " _ ", "(_)"]), + // ; (59) + (";", vec![" ", " _ ", "(_)", " _ ", "( )"]), + // < (60) + ("<", vec![" _", " / ", "< ", " \\_", " "]), + // = (61) + ("=", vec![" ", "____", " ", "____", " "]), + // > (62) + (">", vec!["_ ", " \\ ", " >", "_/ ", " "]), + // ? (63) + ("?", vec![" __ ", "| \\", " / ", " (_)", " "]), + // @ (64) + ("@", vec![" __ ", " / _\\ ", "/ (_) \\", "\\ ___ /", " \\__/ "]), + // A (65) + ("A", vec![" _ ", " / \\ ", " / _ \\ ", " / ___ \\ ", "/_/ \\_\\"]), + // B (66) + ("B", vec!["____ ", "| __ ) ", "| _ \\ ", "| |_) |", "|____/ "]), + // C (67) + ("C", vec![" ____ ", " / ___|", "| | ", "| |___ ", " \\____|"]), + // D (68) + ("D", vec!["____ ", "| _ \\ ", "| | | |", "| |_| |", "|____/ "]), + // E (69) + ("E", vec!["_____ ", "| ____|", "| _| ", "| |___ ", "|_____|"]), + // F (70) + ("F", vec!["_____ ", "| ___|", "| |_ ", "| _| ", "|_| "]), + // G (71) + ("G", vec![" ____ ", " / ___|", "| | _ ", "| |_| |", " \\____|"]), + // H (72) + ("H", vec!["_ _ ", "| | | |", "| |_| |", "| _ |", "|_| |_|"]), + // I (73) + ("I", vec!["___", "|_ |", " | |", " | |", "|__|"]), + // J (74) + ("J", vec![" _ ", " | |", " _ | |", "| |_| |", " \\___/ "]), + // K (75) + ("K", vec!["_ __", "| |/ /", "| ' / ", "| . \\ ", "|_|\\_\\"]), + // L (76) + ("L", vec!["_ ", "| | ", "| | ", "| |___ ", "|_____|"]), + // M (77) + ("M", vec!["__ __ ", "| \\/ |", "| |\\/| |", "| | | |", "|_| |_|"]), + // N (78) + ("N", vec!["_ _ ", "| \\ | |", "| \\| |", "| |\\ |", "|_| \\_|"]), + // O (79) + ("O", vec![" ___ ", " / _ \\ ", "| | | |", "| |_| |", " \\___/ "]), + // P (80) + ("P", vec!["____ ", "| _ \\ ", "| |_) |", "| __/ ", "|_| "]), + // Q (81) + ("Q", vec![" ___ ", " / _ \\ ", "| | | |", "| |_| |", " \\__\\_\\"]), + // R (82) + ("R", vec!["____ ", "| _ \\ ", "| |_) |", "| _ < ", "|_| \\_\\"]), + // S (83) + ("S", vec!["____ ", "/ ___| ", "\\___ \\ ", " ___) |", "|____/ "]), + // T (84) + ("T", vec!["_____ ", "|_ _|", " | | ", " | | ", " |_| "]), + // U (85) + ("U", vec!["_ _ ", "| | | |", "| | | |", "| |_| |", " \\___/ "]), + // V (86) + ("V", vec!["__ __", "\\ \\ / /", " \\ \\ / / ", " \\ V / ", " \\_/ "]), + // W (87) + ("W", vec!["__ __", "\\ \\ / /", " \\ \\ /\\ / / ", " \\ V V / ", " \\_/\\_/ "]), + // X (88) + ("X", vec!["__ __", "\\ \\/ /", " \\ / ", " / \\ ", "/_/\\_\\"]), + // Y (89) + ("Y", vec!["__ __", "\\ \\ / /", " \\ V / ", " | | ", " |_| "]), + // Z (90) + ("Z", vec!["_____", "|__ /", " / / ", " / /_ ", "/____|"]), + // [ (91) + ("[", vec!["__", "| |", "| |", "| |", "|_|"]), + // \\ (92) + ("\\", vec!["_ ", " \\ ", " \\ ", " \\ ", " _"]), + // ] (93) + ("]", vec!["__", "| |", "| |", "| |", "|_|"]), + // ^ (94) + ("^", vec![" /\\ ", "/ \\", " ", " ", " "]), + // _ (95) + ("_", vec![" ", " ", " ", " ", "_____"]), + // ` (96) + ("`", vec![" _ ", "( )", " V ", " ", " "]), + // a (97) + ("a", vec![" ", " __ _", " / _` |", "| (_| |", " \\__,_|"]), + // b (98) + ("b", vec!["_ ", "| |__ ", "| '_ \\ ", "| |_) |", "|_.__/ "]), + // c (99) + ("c", vec![" ", " ___", " / __|", "| (__ ", " \\___|"]), + // d (100) + ("d", vec![" _ ", " __| |", "/ _` |", "\\__,_|", " "]), + // e (101) + ("e", vec![" ", " ___", " / _ \\", "| __/", " \\___|"]), + // f (102) + ("f", vec![" __ ", " / _|", "| |_ ", "| _|", "|_| "]), + // g (103) + ("g", vec![" ", " __ _", " / _` |", "| (_| |", " \\__, |", " |___/"]), + // h (104) + ("h", vec!["_ ", "| |__ ", "| '_ \\ ", "| | | |", "|_| |_|"]), + // i (105) + ("i", vec![" _ ", "(_)", "| |", "| |", "|_|"]), + // j (106) + ("j", vec![" _ ", " (_)", " | |", " | |", " _/ |", "|__/ "]), + // k (107) + ("k", vec!["_ ", "| | _", "| |/ /", "| < ", "|_|\\_\\"]), + // l (108) + ("l", vec!["_ ", "| |", "| |", "| |", "|_|"]), + // m (109) + ("m", vec![" ", " _ __ ___", "| '_ ` _ \\", "| | | | | |", "|_| |_| |_|"]), + // n (110) + ("n", vec![" ", " _ __ ", "| '_ \\ ", "| | | |", "|_| |_|"]), + // o (111) + ("o", vec![" ", " ___ ", " / _ \\ ", "| (_) |", " \\___/ "]), + // p (112) + ("p", vec![" ", " _ __ ", "| '_ \\ ", "| |_) |", "| .__/ ", "|_| "]), + // q (113) + ("q", vec![" ", " __ _ ", " / _` |", "| (_| |", " \\__, |", " |_|"]), + // r (114) + ("r", vec![" ", " _ __ ", "| '__|", "| | ", "|_| "]), + // s (115) + ("s", vec![" ", " ___ ", "/ __|", "\\__ \\", "|___/"]), + // t (116) + ("t", vec!["_ ", "| |_ ", "| __|", "| |_ ", " \\__|"]), + // u (117) + ("u", vec![" ", " _ _ ", "| | | |", "| |_| |", " \\__,_|"]), + // v (118) + ("v", vec![" ", "__ __", "\\ \\ / /", " \\ V / ", " \\_/ "]), + // w (119) + ("w", vec![" ", "__ __", "\\ \\ /\\ / /", " \\ V V / ", " \\_/\\_/ "]), + // x (120) + ("x", vec![" ", "__ __", "\\ \\/ /", " > < ", "/_/\\_\\"]), + // y (121) + ("y", vec![" ", " _ _ ", "| | | |", "| |_| |", " \\__, |", " |___/ "]), + // z (122) + ("z", vec![" ", " ____", "|_ /", " / / ", "/___|"]), + // { (123) + ("{", vec![" _", " / |", "| | ", " \\_|", " "]), + // | (124) + ("|", vec![" _ ", "| |", "| |", "| |", "|_|"]), + // } (125) + ("}", vec!["_ ", "| \\ ", " | |", "|_/ ", " "]), + // ~ (126) + ("~", vec![" ", " /\\/|", "|/\\/ ", " ", " "]), + ]; + + // Build the character array + let mut chars: Vec> = Vec::with_capacity(95); + + for (_, lines) in char_defs.iter() { + let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0); + let fig_char = FigChar { + lines: lines.iter().take(height).map(|s| s.to_string()).collect(), + width: max_width, + }; + chars.push(Some(fig_char)); + } + + FigFont { + height, + baseline: 4, + chars, + hardblank, + } +} + +/// Simple block font - clean, minimal 3-line font +pub fn block_font() -> FigFont { + let height = 3; + let hardblank = '$'; + + let char_defs: Vec> = vec![ + // Space (32) + vec![" ", " ", " "], + // ! (33) + vec!["█", "█", "▄"], + // " (34) + vec!["█ █", " ", " "], + // # (35) + vec![" █ █ ", "█████", " █ █ "], + // $ (36) + vec!["▄███", " █▀▀", "▀▀█▄"], + // % (37) + vec!["█ ▄", " █ ", "▄ █"], + // & (38) + vec!["▄█▄ ", "▀▄█▀", "▀▀▀█"], + // ' (39) + vec!["█", " ", " "], + // ( (40) + vec![" █", "█ ", " █"], + // ) (41) + vec!["█ ", " █", "█ "], + // * (42) + vec!["▄█▄", "███", "▀█▀"], + // + (43) + vec![" █ ", "███", " █ "], + // , (44) + vec![" ", " ", "▄█"], + // - (45) + vec![" ", "███", " "], + // . (46) + vec![" ", " ", "█"], + // / (47) + vec![" █", " █ ", "█ "], + // 0 (48) + vec!["███", "█ █", "███"], + // 1 (49) + vec!["▄█ ", " █ ", "███"], + // 2 (50) + vec!["██▄", " ▄▀", "███"], + // 3 (51) + vec!["██▄", " ██", "██▀"], + // 4 (52) + vec!["█ █", "███", " █"], + // 5 (53) + vec!["███", "██▄", "▄▄█"], + // 6 (54) + vec!["▄██", "███", "▀██"], + // 7 (55) + vec!["███", " █", " █"], + // 8 (56) + vec!["▄█▄", "▀█▀", "▄█▄"], + // 9 (57) + vec!["██▄", "▀██", "██▀"], + // : (58) + vec!["▄", " ", "▄"], + // ; (59) + vec!["▄", " ", "█"], + // < (60) + vec![" █", "█ ", " █"], + // = (61) + vec!["███", " ", "███"], + // > (62) + vec!["█ ", " █", "█ "], + // ? (63) + vec!["██▄", " ▄▀", " ▄ "], + // @ (64) + vec!["▄██▄", "█▄██", "▀▀▀ "], + // A (65) + vec!["▄█▄", "███", "█ █"], + // B (66) + vec!["██▄", "██▀", "███"], + // C (67) + vec!["▄██", "█ ", "▀██"], + // D (68) + vec!["██▄", "█ █", "██▀"], + // E (69) + vec!["███", "██ ", "███"], + // F (70) + vec!["███", "██ ", "█ "], + // G (71) + vec!["▄██", "█ █", "▀██"], + // H (72) + vec!["█ █", "███", "█ █"], + // I (73) + vec!["███", " █ ", "███"], + // J (74) + vec!["███", " █", "██▀"], + // K (75) + vec!["█ █", "██ ", "█ █"], + // L (76) + vec!["█ ", "█ ", "███"], + // M (77) + vec!["█▄▄█", "█▀▀█", "█ █"], + // N (78) + vec!["█▄ █", "█ ██", "█ █"], + // O (79) + vec!["▄█▄", "█ █", "▀█▀"], + // P (80) + vec!["██▄", "██▀", "█ "], + // Q (81) + vec!["▄█▄", "█ █", "▀█▄"], + // R (82) + vec!["██▄", "██▀", "█ █"], + // S (83) + vec!["▄██", "▀█▄", "██▀"], + // T (84) + vec!["███", " █ ", " █ "], + // U (85) + vec!["█ █", "█ █", "▀█▀"], + // V (86) + vec!["█ █", "█ █", " █ "], + // W (87) + vec!["█ █", "█▄▄█", "▀ ▀"], + // X (88) + vec!["█ █", " █ ", "█ █"], + // Y (89) + vec!["█ █", " █ ", " █ "], + // Z (90) + vec!["███", " █ ", "███"], + // [ (91) + vec!["██", "█ ", "██"], + // \\ (92) + vec!["█ ", " █ ", " █"], + // ] (93) + vec!["██", " █", "██"], + // ^ (94) + vec![" █ ", "█ █", " "], + // _ (95) + vec![" ", " ", "███"], + // ` (96) + vec!["█ ", " █", " "], + // a (97) + vec![" ", "▄█▄", "▀██"], + // b (98) + vec!["█ ", "██▄", "██▀"], + // c (99) + vec![" ", "▄█▄", "▀█▀"], + // d (100) + vec![" █", "▄██", "▀██"], + // e (101) + vec![" ", "▄█▄", "▀▀ "], + // f (102) + vec![" █▄", "██ ", "█ "], + // g (103) + vec![" ", "▄██", "▀█▀"], + // h (104) + vec!["█ ", "██▄", "█ █"], + // i (105) + vec!["▄", " ", "█"], + // j (106) + vec![" ▄", " ", "▄█"], + // k (107) + vec!["█ ", "█▄▀", "█ █"], + // l (108) + vec!["█", "█", "▀"], + // m (109) + vec![" ", "█▄▄█", "█ █"], + // n (110) + vec![" ", "██▄", "█ █"], + // o (111) + vec![" ", "▄█▄", "▀█▀"], + // p (112) + vec![" ", "██▄", "█▀ "], + // q (113) + vec![" ", "▄██", " ▀█"], + // r (114) + vec![" ", "█▄▀", "█ "], + // s (115) + vec![" ", "▄█▀", "▀█▄"], + // t (116) + vec![" █ ", "██▄", " ▀▀"], + // u (117) + vec![" ", "█ █", "▀█▀"], + // v (118) + vec![" ", "█ █", " █ "], + // w (119) + vec![" ", "█ █", "▀██▀"], + // x (120) + vec![" ", "▀▄▀", "▄▀▄"], + // y (121) + vec![" ", "█ █", "▀█▀"], + // z (122) + vec![" ", "█▀█", "▀▀▀"], + // { (123) + vec![" █", "█ ", " █"], + // | (124) + vec!["█", "█", "█"], + // } (125) + vec!["█ ", " █", "█ "], + // ~ (126) + vec!["▄▀▄", " ", " "], + ]; + + let mut chars: Vec> = Vec::with_capacity(95); + + for lines in char_defs.iter() { + let max_width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0); + let fig_char = FigChar { + lines: lines.iter().map(|s| s.to_string()).collect(), + width: max_width + 1, // Add 1 for spacing + }; + chars.push(Some(fig_char)); + } + + FigFont { + height, + baseline: 2, + chars, + hardblank, + } +} diff --git a/wasm-renderer/src/hit_test.rs b/wasm-renderer/src/hit_test.rs new file mode 100644 index 0000000..79b6214 --- /dev/null +++ b/wasm-renderer/src/hit_test.rs @@ -0,0 +1,170 @@ +//! Hit testing for interactive elements + +use crate::layout::Rect; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Types of actions that can be triggered +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum HitAction { + /// Navigate to an internal route + Navigate(String), + /// Open an external URL + OpenUrl(String), + /// Scroll to a section + ScrollTo(String), + /// Custom action with ID + Custom(String), +} + +/// A clickable region +#[derive(Clone, Debug)] +pub struct HitRegion { + pub rect: Rect, + pub action: HitAction, + pub hover_effect: bool, +} + +/// Manages hit testing for the entire document +#[derive(Default)] +pub struct HitTestMap { + regions: Vec, + /// Cached grid for fast lookups (optional optimization) + grid: Option, +} + +impl HitTestMap { + pub fn new() -> Self { + Self { + regions: Vec::new(), + grid: None, + } + } + + /// Clear all regions + pub fn clear(&mut self) { + self.regions.clear(); + self.grid = None; + } + + /// Register a clickable region + pub fn register(&mut self, rect: Rect, action: HitAction, hover_effect: bool) { + self.regions.push(HitRegion { + rect, + action, + hover_effect, + }); + // Invalidate grid cache + self.grid = None; + } + + /// Register a link (common case) + pub fn register_link(&mut self, rect: Rect, url: &str) { + let action = if url.starts_with('/') { + HitAction::Navigate(url.to_string()) + } else { + HitAction::OpenUrl(url.to_string()) + }; + self.register(rect, action, true); + } + + /// Test a point and return the action if any + pub fn test(&self, x: u32, y: u32) -> Option<&HitAction> { + // Simple linear search for now + // Could use grid for optimization if needed + for region in self.regions.iter().rev() { + if region.rect.contains(x, y) { + return Some(®ion.action); + } + } + None + } + + /// Check if a point is hovering over a clickable region + pub fn is_hovering(&self, x: u32, y: u32) -> bool { + self.regions.iter().any(|r| r.hover_effect && r.rect.contains(x, y)) + } + + /// Get all regions that overlap with a given rect (for hover highlighting) + pub fn get_hover_regions(&self, x: u32, y: u32) -> Vec<&Rect> { + self.regions + .iter() + .filter(|r| r.hover_effect && r.rect.contains(x, y)) + .map(|r| &r.rect) + .collect() + } + + /// Get number of registered regions + pub fn len(&self) -> usize { + self.regions.len() + } + + pub fn is_empty(&self) -> bool { + self.regions.is_empty() + } +} + +/// Grid-based optimization for hit testing (for many regions) +struct HitGrid { + cell_size: u32, + width: u32, + height: u32, + cells: HashMap<(u32, u32), Vec>, +} + +impl HitGrid { + fn new(width: u32, height: u32, cell_size: u32) -> Self { + Self { + cell_size, + width: (width / cell_size) + 1, + height: (height / cell_size) + 1, + cells: HashMap::new(), + } + } + + fn add_region(&mut self, idx: usize, rect: &Rect) { + let start_cx = rect.x / self.cell_size; + let end_cx = (rect.x + rect.width) / self.cell_size; + let start_cy = rect.y / self.cell_size; + let end_cy = (rect.y + rect.height) / self.cell_size; + + for cy in start_cy..=end_cy { + for cx in start_cx..=end_cx { + self.cells.entry((cx, cy)).or_default().push(idx); + } + } + } + + fn get_candidates(&self, x: u32, y: u32) -> &[usize] { + let cx = x / self.cell_size; + let cy = y / self.cell_size; + self.cells.get(&(cx, cy)).map(|v| v.as_slice()).unwrap_or(&[]) + } +} + +/// Serialize hit action to JSON string for JavaScript +pub fn action_to_json(action: &HitAction) -> String { + serde_json::to_string(action).unwrap_or_else(|_| "null".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hit_test() { + let mut map = HitTestMap::new(); + map.register_link(Rect::new(0, 0, 10, 2), "/projects"); + map.register_link(Rect::new(0, 5, 10, 2), "https://github.com"); + + assert!(matches!( + map.test(5, 1), + Some(HitAction::Navigate(ref s)) if s == "/projects" + )); + assert!(matches!( + map.test(5, 6), + Some(HitAction::OpenUrl(ref s)) if s == "https://github.com" + )); + assert!(map.test(5, 3).is_none()); + } +} diff --git a/wasm-renderer/src/image.rs b/wasm-renderer/src/image.rs new file mode 100644 index 0000000..d87345e --- /dev/null +++ b/wasm-renderer/src/image.rs @@ -0,0 +1,245 @@ +//! Image to ASCII conversion + +use crate::buffer::{CharBuffer, CharCell}; + +/// ASCII character ramps from dark to light +pub const RAMP_STANDARD: &str = " .:-=+*#%@"; +pub const RAMP_EXTENDED: &str = " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"; +pub const RAMP_BLOCKS: &str = " ░▒▓█"; + +/// Processed ASCII image ready for rendering +#[derive(Clone, Debug)] +pub struct AsciiImage { + /// Width in characters + pub width: u32, + /// Height in characters + pub height: u32, + /// Character data + pub chars: Vec, + /// Grayscale values (0-255) for potential color mapping + pub values: Vec, +} + +impl AsciiImage { + /// Get character at position + pub fn get(&self, x: u32, y: u32) -> Option { + if x < self.width && y < self.height { + Some(self.chars[(y * self.width + x) as usize]) + } else { + None + } + } + + /// Get grayscale value at position + pub fn get_value(&self, x: u32, y: u32) -> Option { + if x < self.width && y < self.height { + Some(self.values[(y * self.width + x) as usize]) + } else { + None + } + } +} + +/// Convert RGBA pixel data to ASCII art +/// +/// # Arguments +/// * `data` - Raw RGBA pixel data (4 bytes per pixel) +/// * `img_width` - Image width in pixels +/// * `img_height` - Image height in pixels +/// * `target_width` - Target width in characters +/// * `ramp` - Character ramp to use (dark to light) +/// * `invert` - Invert the brightness mapping +pub fn image_to_ascii( + data: &[u8], + img_width: u32, + img_height: u32, + target_width: u32, + ramp: &str, + invert: bool, +) -> AsciiImage { + let ramp_chars: Vec = ramp.chars().collect(); + let ramp_len = ramp_chars.len(); + + if ramp_len == 0 || target_width == 0 || img_width == 0 || img_height == 0 { + return AsciiImage { + width: 0, + height: 0, + chars: vec![], + values: vec![], + }; + } + + // Calculate character cell size + // Characters are typically ~2x taller than wide, so we sample more vertical pixels + let char_width = img_width as f32 / target_width as f32; + let char_height = char_width * 2.0; // Adjust for character aspect ratio + let target_height = (img_height as f32 / char_height).ceil() as u32; + + let mut chars = Vec::with_capacity((target_width * target_height) as usize); + let mut values = Vec::with_capacity((target_width * target_height) as usize); + + for cy in 0..target_height { + for cx in 0..target_width { + // Calculate the pixel region for this character + let px_start = (cx as f32 * char_width) as u32; + let py_start = (cy as f32 * char_height) as u32; + let px_end = ((cx + 1) as f32 * char_width) as u32; + let py_end = ((cy + 1) as f32 * char_height) as u32; + + // Average the pixel values in this region + let mut total_gray: u32 = 0; + let mut count: u32 = 0; + + for py in py_start..py_end.min(img_height) { + for px in px_start..px_end.min(img_width) { + let idx = ((py * img_width + px) * 4) as usize; + if idx + 3 < data.len() { + let r = data[idx] as u32; + let g = data[idx + 1] as u32; + let b = data[idx + 2] as u32; + let a = data[idx + 3] as u32; + + // Skip fully transparent pixels + if a < 10 { + continue; + } + + // Perceptual grayscale conversion + // Using BT.709 coefficients + let gray = (r * 2126 + g * 7152 + b * 722) / 10000; + total_gray += gray; + count += 1; + } + } + } + + // Calculate average grayscale + let avg_gray = if count > 0 { + (total_gray / count) as u8 + } else { + 255 // Transparent = white/light + }; + + // Map to character + let brightness = if invert { 255 - avg_gray } else { avg_gray }; + let char_idx = (brightness as usize * (ramp_len - 1)) / 255; + let ch = ramp_chars[char_idx.min(ramp_len - 1)]; + + chars.push(ch); + values.push(brightness); + } + } + + AsciiImage { + width: target_width, + height: target_height, + chars, + values, + } +} + +/// Render an ASCII image to the buffer +pub fn render_ascii_image( + buffer: &mut CharBuffer, + x: u32, + y: u32, + image: &AsciiImage, + fg_color: u32, +) { + for iy in 0..image.height { + for ix in 0..image.width { + if let Some(ch) = image.get(ix, iy) { + let bx = x + ix; + let by = y + iy; + if bx < buffer.width() && by < buffer.height() { + buffer.set_cell(bx, by, CharCell::new(ch, fg_color)); + } + } + } + } +} + +/// Render an ASCII image with brightness-based coloring +pub fn render_ascii_image_colored( + buffer: &mut CharBuffer, + x: u32, + y: u32, + image: &AsciiImage, + base_color: u32, +) { + use crate::buffer::unpack_rgba; + + let (r, g, b, _) = unpack_rgba(base_color); + + for iy in 0..image.height { + for ix in 0..image.width { + if let (Some(ch), Some(val)) = (image.get(ix, iy), image.get_value(ix, iy)) { + let bx = x + ix; + let by = y + iy; + if bx < buffer.width() && by < buffer.height() { + // Modulate color by brightness + let factor = val as f32 / 255.0; + let nr = (r as f32 * factor) as u8; + let ng = (g as f32 * factor) as u8; + let nb = (b as f32 * factor) as u8; + let color = crate::buffer::rgba(nr, ng, nb, 255); + buffer.set_cell(bx, by, CharCell::new(ch, color)); + } + } + } + } +} + +/// Simple resize of image data using nearest neighbor +pub fn resize_image_data( + data: &[u8], + src_width: u32, + src_height: u32, + dst_width: u32, + dst_height: u32, +) -> Vec { + let mut result = vec![0u8; (dst_width * dst_height * 4) as usize]; + + let x_ratio = src_width as f32 / dst_width as f32; + let y_ratio = src_height as f32 / dst_height as f32; + + for y in 0..dst_height { + for x in 0..dst_width { + let src_x = (x as f32 * x_ratio) as u32; + let src_y = (y as f32 * y_ratio) as u32; + + let src_idx = ((src_y * src_width + src_x) * 4) as usize; + let dst_idx = ((y * dst_width + x) * 4) as usize; + + if src_idx + 3 < data.len() && dst_idx + 3 < result.len() { + result[dst_idx] = data[src_idx]; + result[dst_idx + 1] = data[src_idx + 1]; + result[dst_idx + 2] = data[src_idx + 2]; + result[dst_idx + 3] = data[src_idx + 3]; + } + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_image_to_ascii_basic() { + // Create a simple 4x4 gradient image + let mut data = Vec::new(); + for y in 0..4 { + for x in 0..4 { + let gray = ((x + y) * 255 / 6) as u8; + data.extend_from_slice(&[gray, gray, gray, 255]); + } + } + + let result = image_to_ascii(&data, 4, 4, 4, RAMP_STANDARD, false); + assert!(result.width == 4); + assert!(result.height > 0); + } +} diff --git a/wasm-renderer/src/layout.rs b/wasm-renderer/src/layout.rs new file mode 100644 index 0000000..39dc879 --- /dev/null +++ b/wasm-renderer/src/layout.rs @@ -0,0 +1,291 @@ +//! Layout engine for positioning content + +use serde::{Deserialize, Serialize}; + +/// A rectangle in character coordinates +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct Rect { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +impl Rect { + pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { + Self { x, y, width, height } + } + + pub fn contains(&self, px: u32, py: u32) -> bool { + px >= self.x && px < self.x + self.width && py >= self.y && py < self.y + self.height + } + + pub fn right(&self) -> u32 { + self.x + self.width + } + + pub fn bottom(&self) -> u32 { + self.y + self.height + } +} + +/// Spacing values +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct Spacing { + pub top: u32, + pub right: u32, + pub bottom: u32, + pub left: u32, +} + +impl Spacing { + pub fn all(value: u32) -> Self { + Self { + top: value, + right: value, + bottom: value, + left: value, + } + } + + pub fn vertical(v: u32) -> Self { + Self { + top: v, + right: 0, + bottom: v, + left: 0, + } + } + + pub fn horizontal(h: u32) -> Self { + Self { + top: 0, + right: h, + bottom: 0, + left: h, + } + } + + pub fn symmetric(v: u32, h: u32) -> Self { + Self { + top: v, + right: h, + bottom: v, + left: h, + } + } +} + +/// Responsive breakpoints +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Breakpoint { + Mobile, // < 60 cols + Tablet, // 60-100 cols + Desktop, // >= 100 cols +} + +impl Breakpoint { + pub fn from_width(width: u32) -> Self { + if width < 60 { + Breakpoint::Mobile + } else if width < 100 { + Breakpoint::Tablet + } else { + Breakpoint::Desktop + } + } +} + +/// Layout context for rendering +#[derive(Clone, Debug)] +pub struct LayoutContext { + /// Total viewport width in characters + pub viewport_width: u32, + /// Total viewport height in characters + pub viewport_height: u32, + /// Current scroll offset (in characters) + pub scroll_y: u32, + /// Current breakpoint + pub breakpoint: Breakpoint, + /// Content area (excluding fixed elements) + pub content_area: Rect, +} + +impl LayoutContext { + pub fn new(viewport_width: u32, viewport_height: u32) -> Self { + let breakpoint = Breakpoint::from_width(viewport_width); + Self { + viewport_width, + viewport_height, + scroll_y: 0, + breakpoint, + content_area: Rect::new(0, 0, viewport_width, viewport_height), + } + } + + pub fn with_scroll(mut self, scroll_y: u32) -> Self { + self.scroll_y = scroll_y; + self + } + + /// Check if a y position is visible in the viewport + pub fn is_visible(&self, y: u32, height: u32) -> bool { + let view_start = self.scroll_y; + let view_end = self.scroll_y + self.viewport_height; + let item_end = y + height; + + // Item is visible if it overlaps with viewport + y < view_end && item_end > view_start + } + + /// Convert document y to viewport y + pub fn to_viewport_y(&self, doc_y: u32) -> i32 { + doc_y as i32 - self.scroll_y as i32 + } + + /// Get padding based on breakpoint + pub fn get_padding(&self) -> Spacing { + match self.breakpoint { + Breakpoint::Mobile => Spacing::symmetric(1, 1), + Breakpoint::Tablet => Spacing::symmetric(2, 3), + Breakpoint::Desktop => Spacing::symmetric(2, 5), + } + } + + /// Get content width based on breakpoint (with max-width for desktop) + pub fn get_content_width(&self) -> u32 { + let padding = self.get_padding(); + let available = self.viewport_width.saturating_sub(padding.left + padding.right); + + // Higher limits for smaller font / higher density rendering + match self.breakpoint { + Breakpoint::Mobile => available, + Breakpoint::Tablet => available.min(160), + Breakpoint::Desktop => available.min(200), + } + } + + /// Get centered x position for content + pub fn get_content_x(&self) -> u32 { + let content_width = self.get_content_width(); + let padding = self.get_padding(); + let available = self.viewport_width.saturating_sub(padding.left + padding.right); + + padding.left + (available.saturating_sub(content_width)) / 2 + } +} + +/// Layout builder for stacking content vertically +#[derive(Clone, Debug)] +pub struct VerticalLayout { + pub x: u32, + pub y: u32, + pub width: u32, + pub cursor_y: u32, + pub spacing: u32, +} + +impl VerticalLayout { + pub fn new(x: u32, y: u32, width: u32) -> Self { + Self { + x, + y, + width, + cursor_y: y, + spacing: 1, + } + } + + pub fn with_spacing(mut self, spacing: u32) -> Self { + self.spacing = spacing; + self + } + + /// Reserve space and return the rectangle + pub fn push(&mut self, height: u32) -> Rect { + let rect = Rect::new(self.x, self.cursor_y, self.width, height); + self.cursor_y += height + self.spacing; + rect + } + + /// Add vertical space + pub fn add_space(&mut self, space: u32) { + self.cursor_y += space; + } + + /// Get total height consumed + pub fn total_height(&self) -> u32 { + self.cursor_y.saturating_sub(self.y) + } + + /// Get current y position + pub fn current_y(&self) -> u32 { + self.cursor_y + } +} + +/// Two-column layout helper +#[derive(Clone, Debug)] +pub struct TwoColumnLayout { + pub x: u32, + pub y: u32, + pub total_width: u32, + pub left_width: u32, + pub right_width: u32, + pub gap: u32, +} + +impl TwoColumnLayout { + pub fn new(x: u32, y: u32, total_width: u32, left_ratio: f32, gap: u32) -> Self { + let left_width = ((total_width - gap) as f32 * left_ratio) as u32; + let right_width = total_width - left_width - gap; + Self { + x, + y, + total_width, + left_width, + right_width, + gap, + } + } + + pub fn left_rect(&self, height: u32) -> Rect { + Rect::new(self.x, self.y, self.left_width, height) + } + + pub fn right_rect(&self, height: u32) -> Rect { + Rect::new(self.x + self.left_width + self.gap, self.y, self.right_width, height) + } + + pub fn left_x(&self) -> u32 { + self.x + } + + pub fn right_x(&self) -> u32 { + self.x + self.left_width + self.gap + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rect_contains() { + let rect = Rect::new(10, 10, 20, 20); + assert!(rect.contains(15, 15)); + assert!(!rect.contains(5, 15)); + assert!(!rect.contains(35, 15)); + } + + #[test] + fn test_vertical_layout() { + let mut layout = VerticalLayout::new(0, 0, 100); + let r1 = layout.push(10); + let r2 = layout.push(5); + + assert_eq!(r1.y, 0); + assert_eq!(r1.height, 10); + assert_eq!(r2.y, 11); // 10 + 1 spacing + } +} diff --git a/wasm-renderer/src/lib.rs b/wasm-renderer/src/lib.rs new file mode 100644 index 0000000..a444ed0 --- /dev/null +++ b/wasm-renderer/src/lib.rs @@ -0,0 +1,32 @@ +mod buffer; +mod chart; +mod fonts; +mod hit_test; +mod image; +mod layout; +mod renderer; +mod text; + +use wasm_bindgen::prelude::*; + +pub use buffer::{CharBuffer, CharCell, rgb, rgba}; +pub use chart::{ChartConfig, DataPoint}; +pub use hit_test::{HitAction, HitTestMap}; +pub use image::{AsciiImage, image_to_ascii, RAMP_STANDARD, RAMP_EXTENDED, RAMP_BLOCKS}; +pub use layout::{LayoutContext, Rect, Breakpoint}; +pub use renderer::{Renderer, SiteContent, PageType, Theme}; +pub use text::{TextStyle, TextAlign}; +pub use fonts::{FigFont, block_font}; + +/// Initialize panic hook for better error messages in console +#[wasm_bindgen(start)] +pub fn init_panic_hook() { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +/// Create a new renderer instance +#[wasm_bindgen] +pub fn create_renderer(cols: u32, rows: u32) -> Renderer { + Renderer::new(cols, rows) +} diff --git a/wasm-renderer/src/renderer.rs b/wasm-renderer/src/renderer.rs new file mode 100644 index 0000000..0a41d92 --- /dev/null +++ b/wasm-renderer/src/renderer.rs @@ -0,0 +1,832 @@ +//! Main renderer that orchestrates all components + +use wasm_bindgen::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::buffer::{CharBuffer, CharCell, rgb, rgba}; +use crate::chart::{render_area_chart, render_reference_dot, ChartConfig, DataPoint}; +use crate::fonts::block_font; +use crate::hit_test::{HitTestMap, HitAction, action_to_json}; +use crate::image::{image_to_ascii, AsciiImage, RAMP_STANDARD}; +use crate::layout::{LayoutContext, VerticalLayout, TwoColumnLayout, Rect, Breakpoint}; +use crate::text::{render_text, render_text_wrapped, render_figlet, TextStyle, render_hline}; + +/// Page types +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PageType { + Projects, + Resume, +} + +/// Activity data point +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ActivityPoint { + pub x: f64, + pub y: f64, + #[serde(default)] + pub name: Option, +} + +/// Project data +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ProjectData { + pub name: String, + pub link: Option, + pub org: String, + pub date: String, + pub blurb: String, + #[serde(rename = "imageId")] + pub image_id: String, +} + +/// Experience data +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ExperienceData { + pub workplace: String, + pub location: String, + pub position: String, + pub timeframe: String, + pub description: String, +} + +/// Education data +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EducationData { + pub name: String, + pub location: String, + pub details: String, +} + +/// Header data +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HeaderData { + pub name: String, + pub title: String, + pub location: String, + #[serde(rename = "profileImageId")] + pub profile_image_id: String, + pub activity: Vec, +} + +/// Navigation item +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NavItem { + pub label: String, + pub path: String, +} + +/// Site content +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SiteContent { + pub page: PageType, + pub header: HeaderData, + pub navigation: Vec, + #[serde(rename = "activePath")] + pub active_path: String, + #[serde(default)] + pub projects: Vec, + #[serde(default)] + pub education: Vec, + #[serde(default)] + pub experiences: Vec, + #[serde(default)] + pub footer: FooterData, +} + +/// Footer data +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct FooterData { + #[serde(default)] + pub credits: String, + #[serde(default, rename = "socialLinks")] + pub social_links: Vec, + #[serde(default, rename = "sourceUrl")] + pub source_url: String, +} + +/// Color themes +#[derive(Clone, Copy)] +pub struct Theme { + pub bg_color: u32, + pub text_color: u32, + pub text_secondary: u32, + pub link_color: u32, + pub accent_color: u32, + pub border_color: u32, +} + +impl Theme { + pub fn light() -> Self { + Self { + bg_color: rgb(255, 255, 255), + text_color: rgb(0, 0, 0), + text_secondary: rgb(100, 100, 100), + link_color: rgb(0, 84, 180), + accent_color: rgb(136, 132, 216), + border_color: rgb(211, 211, 211), + } + } + + pub fn dark() -> Self { + Self { + bg_color: rgb(24, 26, 27), + text_color: rgba(249, 249, 249, 204), + text_secondary: rgba(255, 255, 255, 102), + link_color: rgb(128, 229, 255), + accent_color: rgb(96, 182, 255), + border_color: rgb(100, 100, 100), + } + } +} + +/// The main renderer +#[wasm_bindgen] +pub struct Renderer { + buffer: CharBuffer, + layout: LayoutContext, + hit_map: HitTestMap, + content: Option, + images: HashMap, + theme: Theme, + hover_x: i32, + hover_y: i32, + total_content_height: u32, + font: crate::fonts::FigFont, +} + +#[wasm_bindgen] +impl Renderer { + /// Create a new renderer with given dimensions + #[wasm_bindgen(constructor)] + pub fn new(cols: u32, rows: u32) -> Renderer { + Renderer { + buffer: CharBuffer::new(cols, rows), + layout: LayoutContext::new(cols, rows), + hit_map: HitTestMap::new(), + content: None, + images: HashMap::new(), + theme: Theme::light(), + hover_x: -1, + hover_y: -1, + total_content_height: 0, + font: block_font(), + } + } + + /// Resize the viewport + pub fn resize(&mut self, cols: u32, rows: u32) { + self.buffer.resize(cols, rows); + self.layout = LayoutContext::new(cols, rows).with_scroll(self.layout.scroll_y); + self.render_content(); + } + + /// Set the current scroll position + pub fn set_scroll(&mut self, scroll_y: u32) { + let max_scroll = self.total_content_height.saturating_sub(self.layout.viewport_height); + let clamped = scroll_y.min(max_scroll); + if self.layout.scroll_y != clamped { + self.layout.scroll_y = clamped; + self.render_content(); + } + } + + /// Get current scroll position + pub fn get_scroll(&self) -> u32 { + self.layout.scroll_y + } + + /// Get total content height + pub fn get_content_height(&self) -> u32 { + self.total_content_height + } + + /// Set hover position + pub fn set_hover(&mut self, x: i32, y: i32) { + self.hover_x = x; + self.hover_y = y; + } + + /// Set the page content from JSON + pub fn set_content(&mut self, json: &str) -> Result<(), JsValue> { + let content: SiteContent = serde_json::from_str(json) + .map_err(|e| JsValue::from_str(&format!("JSON parse error: {}", e)))?; + + // Set theme based on page + self.theme = match content.page { + PageType::Projects => Theme::light(), + PageType::Resume => Theme::dark(), + }; + + self.content = Some(content); + self.render_content(); + Ok(()) + } + + /// Load an image for ASCII conversion + pub fn load_image(&mut self, id: &str, data: &[u8], width: u32, height: u32) { + // Convert to ASCII with appropriate width based on breakpoint (1.75x for higher detail) + let target_width = match self.layout.breakpoint { + Breakpoint::Mobile => 52, + Breakpoint::Tablet => 70, + Breakpoint::Desktop => 88, + }; + + let ascii = image_to_ascii(data, width, height, target_width, RAMP_STANDARD, false); + self.images.insert(id.to_string(), ascii); + } + + /// Hit test at a position (returns JSON action or null) + pub fn hit_test(&self, x: u32, y: u32) -> Option { + // First check navbar (fixed at top, uses viewport coords directly) + if y < 3 { + // Navbar area - check without scroll adjustment + if let Some(action) = self.hit_map.test(x, y) { + return Some(action_to_json(action)); + } + } + + // Convert viewport coords to document coords for content + let doc_y = y + self.layout.scroll_y; + self.hit_map.test(x, doc_y).map(action_to_json) + } + + /// Check if a position is hoverable + pub fn is_hoverable(&self, x: u32, y: u32) -> bool { + // First check navbar + if y < 3 { + if self.hit_map.is_hovering(x, y) { + return true; + } + } + + let doc_y = y + self.layout.scroll_y; + self.hit_map.is_hovering(x, doc_y) + } + + /// Render and return the buffer data + pub fn render(&mut self) -> Vec { + self.buffer.get_data() + } + + /// Get buffer width + pub fn get_width(&self) -> u32 { + self.buffer.width() + } + + /// Get buffer height + pub fn get_height(&self) -> u32 { + self.buffer.height() + } + + /// Get number of registered hit regions (for debugging) + pub fn get_hit_count(&self) -> u32 { + self.hit_map.len() as u32 + } +} + +// Internal rendering methods +impl Renderer { + /// Main render function + fn render_content(&mut self) { + self.buffer.clear(); + self.hit_map.clear(); + + let Some(content) = &self.content.clone() else { + return; + }; + + // Calculate total content first + self.total_content_height = self.calculate_content_height(&content); + + // Render based on page type + match content.page { + PageType::Projects => self.render_projects_page(&content), + PageType::Resume => self.render_resume_page(&content), + } + } + + fn calculate_content_height(&self, content: &SiteContent) -> u32 { + // Estimate height based on content - be generous to allow scrolling + let nav_height = 3; + let header_height = match self.layout.breakpoint { + Breakpoint::Mobile => 45, + _ => 35, + }; + + let content_height = match content.page { + PageType::Projects => { + // Each project takes roughly 40 lines with 1.75x images + content.projects.len() as u32 * 40 + } + PageType::Resume => { + // Each experience takes roughly 15 lines + content.experiences.len() as u32 * 15 + 30 + } + }; + + let footer_height = 5; + + // Add extra padding to ensure we can scroll to see everything + nav_height + header_height + content_height + footer_height + 20 + } + + fn render_projects_page(&mut self, content: &SiteContent) { + let scroll_y = self.layout.scroll_y; + let _view_height = self.layout.viewport_height; + + let content_x = self.layout.get_content_x(); + let content_width = self.layout.get_content_width(); + + // Content starts at row 3 (after navbar) in document space + // When scrolled, we offset by scroll_y + let mut y: i32 = 3 - (scroll_y as i32); + + // Navbar (fixed at top - always render at y=0 in viewport) + self.render_navbar(content, 0); + + // Header + let header_height = self.render_header(&content.header, content_x, &mut y, content_width); + + // Projects + for (i, project) in content.projects.iter().enumerate() { + let flipped = i % 2 == 1; + let project_height = self.render_project(project, content_x, &mut y, content_width, flipped); + } + + // Footer + y += 3; + self.render_footer(&content.footer, content_x, &mut y, content_width); + + // Update total height + self.total_content_height = (y + scroll_y as i32) as u32; + } + + fn render_resume_page(&mut self, content: &SiteContent) { + let scroll_y = self.layout.scroll_y; + let content_x = self.layout.get_content_x(); + let content_width = self.layout.get_content_width(); + + // Content starts at row 3 (after navbar) in document space + let mut y: i32 = 3 - (scroll_y as i32); + + // Navbar + self.render_navbar(content, 0); + + // Header + let header_height = self.render_header(&content.header, content_x, &mut y, content_width); + + // Two column layout for resume + y += 2; + + if self.layout.breakpoint == Breakpoint::Mobile { + // Single column on mobile + self.render_education(&content.education, content_x, &mut y, content_width); + y += 2; + self.render_experiences(&content.experiences, content_x, &mut y, content_width); + } else { + // Two columns on larger screens + let sidebar_width = content_width / 4; + let main_width = content_width - sidebar_width - 3; + + let sidebar_x = content_x; + let main_x = content_x + sidebar_width + 3; + + let start_y = y; + + // Sidebar (education) + let mut sidebar_y = y; + self.render_education(&content.education, sidebar_x, &mut sidebar_y, sidebar_width); + + // Main content (experiences) + let mut main_y = y; + self.render_experiences(&content.experiences, main_x, &mut main_y, main_width); + + y = sidebar_y.max(main_y); + } + + // Footer + y += 3; + self.render_footer(&content.footer, content_x, &mut y, content_width); + + self.total_content_height = (y + scroll_y as i32) as u32; + } + + fn render_navbar(&mut self, content: &SiteContent, y: u32) { + let style = TextStyle::new(self.theme.text_color); + let link_style = TextStyle::new(self.theme.link_color).clickable(); + let active_style = TextStyle::new(self.theme.text_color).underline(); + + // Background for navbar - 3 rows for better click area + self.buffer.fill_bg(0, y, self.buffer.width(), 3, self.theme.bg_color); + + // Render nav items on the right, centered vertically in navbar + let text_y = y + 1; // Center text in navbar + let mut x = self.buffer.width().saturating_sub(5); + + for item in content.navigation.iter().rev() { + let is_active = item.path == content.active_path; + let item_style = if is_active { &active_style } else { &link_style }; + + let label_width = item.label.len() as u32; + x = x.saturating_sub(label_width + 3); + + if !is_active { + // Register hit region for non-active items - cover full navbar height + self.hit_map.register_link( + Rect::new(x, y, label_width + 2, 3), + &item.path, + ); + } + + render_text(&mut self.buffer, x, text_y, &item.label, item_style); + } + } + + fn render_header(&mut self, header: &HeaderData, x: u32, y: &mut i32, width: u32) -> u32 { + let start_y = *y; + + // Skip if completely above viewport + if *y + 30 < 0 { + *y += 30; + return 30; + } + + let style = TextStyle::new(self.theme.text_color); + let secondary_style = TextStyle::new(self.theme.text_secondary); + + *y += 3; // Top padding + + // Profile image (if loaded) + let img_width; + let img_height; + if let Some(img) = self.images.get(&header.profile_image_id) { + img_width = img.width; + img_height = img.height; + + if *y >= 0 { + let img_x = if self.layout.breakpoint == Breakpoint::Mobile { + x + (width - img_width) / 2 + } else { + x + (width - img_width) / 2 - 20 + }; + + for iy in 0..img_height { + for ix in 0..img_width { + if let Some(ch) = img.get(ix, iy) { + let buf_y = (*y + iy as i32) as u32; + if buf_y < self.buffer.height() { + self.buffer.set_cell(img_x + ix, buf_y, CharCell::new(ch, self.theme.text_color)); + } + } + } + } + } + } else { + img_width = 20; + img_height = 10; + } + + // Name (FIGlet) + let name_y = *y + img_height as i32 + 2; + if name_y >= 0 && name_y < self.buffer.height() as i32 { + let name_upper = header.name.to_uppercase(); + let figlet_width = crate::text::figlet_width(&name_upper, &self.font); + let name_x = x + (width.saturating_sub(figlet_width)) / 2; + render_figlet(&mut self.buffer, name_x, name_y as u32, &name_upper, &self.font, &style); + } + + *y = name_y + self.font.height as i32 + 1; + + // Title + if *y >= 0 && *y < self.buffer.height() as i32 { + let title_x = x + (width - header.title.len() as u32) / 2; + render_text(&mut self.buffer, title_x, *y as u32, &header.title, &style); + } + *y += 1; + + // Location + if *y >= 0 && *y < self.buffer.height() as i32 { + let loc_x = x + (width - header.location.len() as u32) / 2; + render_text(&mut self.buffer, loc_x, *y as u32, &header.location, &secondary_style); + } + *y += 3; + + // Activity chart + if !header.activity.is_empty() { + let chart_width = width.min(60); + let chart_height = 8; + let chart_x = x + (width - chart_width) / 2; + + if *y >= -(chart_height as i32) && *y < self.buffer.height() as i32 { + let data: Vec = header.activity.iter().enumerate().map(|(i, a)| { + DataPoint { + x: a.x, + y: a.y, + label: if i == 0 || i == header.activity.len() - 1 { + a.name.clone() + } else { + None + }, + } + }).collect(); + + let chart_style = TextStyle::new(self.theme.accent_color); + let config = ChartConfig { + width: chart_width, + height: chart_height, + show_axes: true, + show_labels: true, + fill_area: true, + title: None, + }; + + if *y >= 0 { + render_area_chart(&mut self.buffer, chart_x, *y as u32, &data, &config, &chart_style); + + // Contribution count label + let total: f64 = header.activity.iter().map(|a| a.y).sum(); + let label = format!("{} contributions", total as u32); + let label_x = chart_x + chart_width + 2; + if label_x + label.len() as u32 <= self.buffer.width() { + render_text(&mut self.buffer, label_x, *y as u32 + 2, &label, &TextStyle::new(self.theme.text_color)); + } + } + } + + *y += chart_height as i32 + 4; + } + + // Separator line + if *y >= 0 && *y < self.buffer.height() as i32 { + render_hline(&mut self.buffer, x, *y as u32, width, &TextStyle::new(self.theme.border_color)); + } + *y += 2; + + (*y - start_y) as u32 + } + + fn render_project(&mut self, project: &ProjectData, x: u32, y: &mut i32, width: u32, flipped: bool) -> u32 { + let start_y = *y; + + // Skip if completely outside viewport + if *y > self.buffer.height() as i32 + 5 || *y + 25 < 0 { + *y += 20; + return 20; + } + + let style = TextStyle::new(self.theme.text_color); + let secondary_style = TextStyle::new(self.theme.text_secondary); + let link_style = TextStyle::new(self.theme.link_color).clickable(); + + // Background for flipped projects + if flipped && *y >= 0 { + let bg_color = rgba(248, 248, 248, 255); + let start_row = (*y).max(0) as u32; + let height = 20.min(self.buffer.height().saturating_sub(start_row)); + self.buffer.fill_bg(0, start_row, self.buffer.width(), height, bg_color); + } + + *y += 2; + + // Project title + if *y >= 0 && *y < self.buffer.height() as i32 { + let title_style = if project.link.is_some() { &link_style } else { &style }; + let title_x = if flipped { x + width - project.name.len() as u32 } else { x }; + render_text(&mut self.buffer, title_x, *y as u32, &project.name, title_style); + + if let Some(ref link) = project.link { + // Register hit region in document coordinates (viewport_y + scroll_y) + let doc_y = (*y as u32) + self.layout.scroll_y; + self.hit_map.register_link( + Rect::new(title_x, doc_y, project.name.len() as u32, 1), + link, + ); + } + } + *y += 1; + + // Org and date + if *y >= 0 && *y < self.buffer.height() as i32 { + let org_date = format!("{} | {}", project.org, project.date); + let od_x = if flipped { x + width - org_date.len() as u32 } else { x }; + render_text(&mut self.buffer, od_x, *y as u32, &org_date, &secondary_style); + } + *y += 2; + + // Content: image and blurb + let img_width = match self.layout.breakpoint { + Breakpoint::Mobile => width, + _ => width * 55 / 100, + }; + let blurb_width = match self.layout.breakpoint { + Breakpoint::Mobile => width, + _ => width * 40 / 100, + }; + + let (img_x, blurb_x) = if self.layout.breakpoint == Breakpoint::Mobile { + (x, x) + } else if flipped { + (x + width - img_width, x) + } else { + (x, x + img_width + 5) + }; + + let img_start_y = *y; + + // Render image if loaded (1.75x height for higher detail) + let max_img_height = 26u32; + if let Some(img) = self.images.get(&project.image_id) { + if *y >= 0 { + for iy in 0..img.height.min(max_img_height) { + for ix in 0..img.width.min(img_width) { + if let Some(ch) = img.get(ix, iy) { + let buf_y = (*y + iy as i32) as u32; + if buf_y < self.buffer.height() { + self.buffer.set_cell(img_x + ix, buf_y, CharCell::new(ch, self.theme.text_color)); + } + } + } + } + } + + if self.layout.breakpoint == Breakpoint::Mobile { + *y += img.height.min(max_img_height) as i32 + 1; + } + } + + // Render blurb + let blurb_y = if self.layout.breakpoint == Breakpoint::Mobile { *y } else { img_start_y }; + if blurb_y >= 0 && blurb_y < self.buffer.height() as i32 { + let lines = render_text_wrapped(&mut self.buffer, blurb_x, blurb_y as u32, blurb_width, &project.blurb, &style); + + if self.layout.breakpoint == Breakpoint::Mobile { + *y += lines as i32; + } else { + *y = img_start_y + (lines as i32).max(max_img_height as i32); + } + } else { + *y += 10; + } + + *y += 3; + + (*y - start_y) as u32 + } + + fn render_education(&mut self, education: &[EducationData], x: u32, y: &mut i32, width: u32) { + let style = TextStyle::new(self.theme.text_color); + let secondary_style = TextStyle::new(self.theme.text_secondary); + let link_style = TextStyle::new(self.theme.link_color).clickable(); + + if *y >= 0 && *y < self.buffer.height() as i32 { + render_text(&mut self.buffer, x, *y as u32, "Education", &style.clone().bold()); + } + *y += 2; + + for edu in education { + if *y >= 0 && *y < self.buffer.height() as i32 { + render_text(&mut self.buffer, x, *y as u32, &edu.name, &style); + } + *y += 1; + + if *y >= 0 && *y < self.buffer.height() as i32 { + render_text(&mut self.buffer, x, *y as u32, &edu.location, &secondary_style); + } + *y += 1; + + if *y >= 0 && *y < self.buffer.height() as i32 { + let lines = render_text_wrapped(&mut self.buffer, x, *y as u32, width, &edu.details, &secondary_style); + *y += lines as i32; + } + *y += 1; + } + + // Request full resume link + *y += 1; + if *y >= 0 && *y < self.buffer.height() as i32 { + let link_text = "Request full resume"; + render_text(&mut self.buffer, x, *y as u32, link_text, &link_style); + // Register hit region in document coordinates + let doc_y = (*y as u32) + self.layout.scroll_y; + self.hit_map.register_link( + Rect::new(x, doc_y, link_text.len() as u32, 1), + "mailto:jksmithnyc@gmail.com", + ); + } + *y += 2; + } + + fn render_experiences(&mut self, experiences: &[ExperienceData], x: u32, y: &mut i32, width: u32) { + let style = TextStyle::new(self.theme.text_color); + let secondary_style = TextStyle::new(self.theme.text_secondary); + + if *y >= 0 && *y < self.buffer.height() as i32 { + render_text(&mut self.buffer, x, *y as u32, "Experience", &style.clone().bold()); + } + *y += 2; + + for exp in experiences { + // Skip if way above viewport + if *y > self.buffer.height() as i32 + 5 { + break; + } + + if *y >= 0 && *y < self.buffer.height() as i32 { + let header = format!("{}, {}", exp.workplace, exp.location); + render_text(&mut self.buffer, x, *y as u32, &header, &style.clone().bold()); + } + *y += 1; + + if *y >= 0 && *y < self.buffer.height() as i32 { + let summary = format!("{}, {}", exp.position, exp.timeframe); + render_text(&mut self.buffer, x, *y as u32, &summary, &secondary_style); + } + *y += 1; + + // Description (handle markdown bullet points) + for line in exp.description.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + let display = if trimmed.starts_with("- ") { + format!("• {}", &trimmed[2..]) + } else { + trimmed.to_string() + }; + + if *y >= 0 && *y < self.buffer.height() as i32 { + let lines = render_text_wrapped(&mut self.buffer, x + 2, *y as u32, width - 2, &display, &secondary_style); + *y += lines as i32; + } else { + *y += 1; + } + } + + *y += 2; + + // Separator + if *y >= 0 && *y < self.buffer.height() as i32 { + render_hline(&mut self.buffer, x, *y as u32, width, &TextStyle::new(self.theme.border_color)); + } + *y += 1; + } + } + + fn render_footer(&mut self, footer: &FooterData, x: u32, y: &mut i32, width: u32) { + if *y < 0 || *y >= self.buffer.height() as i32 { + return; + } + + let style = TextStyle::new(self.theme.text_color); + let link_style = TextStyle::new(self.theme.link_color).clickable(); + + // Credits + let credits = if footer.credits.is_empty() { + "Jai K. Smith (2020)".to_string() + } else { + footer.credits.clone() + }; + render_text(&mut self.buffer, x, *y as u32, &credits, &style); + + // Social links (simplified - just show domain) + let socials_text: Vec<&str> = footer.social_links.iter() + .filter_map(|url| { + if url.contains("github") { Some("GitHub") } + else if url.contains("linkedin") { Some("LinkedIn") } + else { None } + }) + .collect(); + + let social_x = x + width / 2 - socials_text.join(" | ").len() as u32 / 2; + let mut sx = social_x; + let doc_y = (*y as u32) + self.layout.scroll_y; + for (i, (name, url)) in socials_text.iter().zip(footer.social_links.iter()).enumerate() { + if i > 0 { + render_text(&mut self.buffer, sx, *y as u32, " | ", &style); + sx += 3; + } + render_text(&mut self.buffer, sx, *y as u32, name, &link_style); + // Register hit region in document coordinates + self.hit_map.register_link(Rect::new(sx, doc_y, name.len() as u32, 1), url); + sx += name.len() as u32; + } + + // Source code link + let source_text = "Source Code"; + let source_url = if footer.source_url.is_empty() { + "https://github.com/jaismith/jaismith.dev" + } else { + &footer.source_url + }; + let source_x = x + width - source_text.len() as u32; + render_text(&mut self.buffer, source_x, *y as u32, source_text, &link_style); + // Register hit region in document coordinates + self.hit_map.register_link(Rect::new(source_x, doc_y, source_text.len() as u32, 1), source_url); + + *y += 2; + } +} diff --git a/wasm-renderer/src/text.rs b/wasm-renderer/src/text.rs new file mode 100644 index 0000000..88e9064 --- /dev/null +++ b/wasm-renderer/src/text.rs @@ -0,0 +1,270 @@ +//! Text rendering utilities + +use crate::buffer::{CharBuffer, CharCell}; +use crate::fonts::{FigFont, block_font}; + +/// Text alignment options +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TextAlign { + Left, + Center, + Right, +} + +/// Text style for rendering +#[derive(Clone, Debug)] +pub struct TextStyle { + pub fg_color: u32, + pub bg_color: u32, + pub bold: bool, + pub underline: bool, + pub clickable: bool, +} + +impl Default for TextStyle { + fn default() -> Self { + Self { + fg_color: 0xFF000000, // Black + bg_color: 0, // Transparent + bold: false, + underline: false, + clickable: false, + } + } +} + +impl TextStyle { + pub fn new(fg_color: u32) -> Self { + Self { + fg_color, + ..Default::default() + } + } + + pub fn with_bg(mut self, bg_color: u32) -> Self { + self.bg_color = bg_color; + self + } + + pub fn bold(mut self) -> Self { + self.bold = true; + self + } + + pub fn underline(mut self) -> Self { + self.underline = true; + self + } + + pub fn clickable(mut self) -> Self { + self.clickable = true; + self + } +} + +/// Render plain text at a position +pub fn render_text( + buffer: &mut CharBuffer, + x: u32, + y: u32, + text: &str, + style: &TextStyle, +) -> u32 { + let mut col = x; + for ch in text.chars() { + if col >= buffer.width() { + break; + } + let mut cell = CharCell::new(ch, style.fg_color); + cell.bg_color = style.bg_color; + if style.bold { + cell = cell.bold(); + } + if style.underline { + cell = cell.underline(); + } + if style.clickable { + cell = cell.clickable(); + } + buffer.set_cell(col, y, cell); + col += 1; + } + col - x +} + +/// Render text with word wrapping +pub fn render_text_wrapped( + buffer: &mut CharBuffer, + x: u32, + y: u32, + width: u32, + text: &str, + style: &TextStyle, +) -> u32 { + let words: Vec<&str> = text.split_whitespace().collect(); + let mut row = y; + let mut col = x; + + for word in words { + let word_len = word.chars().count() as u32; + + // Check if word fits on current line + if col > x && col + word_len > x + width { + // Move to next line + row += 1; + col = x; + } + + // Check if we've exceeded buffer height + if row >= buffer.height() { + break; + } + + // Render the word + for ch in word.chars() { + if col >= x + width { + row += 1; + col = x; + } + if row >= buffer.height() { + break; + } + let mut cell = CharCell::new(ch, style.fg_color); + cell.bg_color = style.bg_color; + if style.bold { + cell = cell.bold(); + } + if style.underline { + cell = cell.underline(); + } + if style.clickable { + cell = cell.clickable(); + } + buffer.set_cell(col, row, cell); + col += 1; + } + + // Add space after word + if col < x + width { + col += 1; + } + } + + row - y + 1 +} + +/// Render FIGlet-style big text +pub fn render_figlet( + buffer: &mut CharBuffer, + x: u32, + y: u32, + text: &str, + font: &FigFont, + style: &TextStyle, +) -> (u32, u32) { + let mut col = x; + let height = font.height as u32; + + for ch in text.chars() { + if let Some(fig_char) = font.get_char(ch) { + for (line_idx, line) in fig_char.lines.iter().enumerate() { + let row = y + line_idx as u32; + if row >= buffer.height() { + continue; + } + let mut char_col = col; + for glyph_char in line.chars() { + if char_col >= buffer.width() { + break; + } + // Skip hard blank + if glyph_char != font.hardblank && glyph_char != ' ' { + let mut cell = CharCell::new(glyph_char, style.fg_color); + cell.bg_color = style.bg_color; + if style.bold { + cell = cell.bold(); + } + if style.clickable { + cell = cell.clickable(); + } + buffer.set_cell(char_col, row, cell); + } + char_col += 1; + } + } + col += fig_char.width as u32; + } else { + // Unknown character, use space + col += 1; + } + } + + (col - x, height) +} + +/// Calculate width of FIGlet text without rendering +pub fn figlet_width(text: &str, font: &FigFont) -> u32 { + text.chars() + .filter_map(|ch| font.get_char(ch)) + .map(|fc| fc.width as u32) + .sum() +} + +/// Render a horizontal line +pub fn render_hline( + buffer: &mut CharBuffer, + x: u32, + y: u32, + width: u32, + style: &TextStyle, +) { + for col in x..(x + width).min(buffer.width()) { + buffer.set_cell(col, y, CharCell::new('─', style.fg_color)); + } +} + +/// Render a box border +pub fn render_box( + buffer: &mut CharBuffer, + x: u32, + y: u32, + width: u32, + height: u32, + style: &TextStyle, +) { + // Corners + buffer.set_cell(x, y, CharCell::new('┌', style.fg_color)); + buffer.set_cell(x + width - 1, y, CharCell::new('┐', style.fg_color)); + buffer.set_cell(x, y + height - 1, CharCell::new('└', style.fg_color)); + buffer.set_cell(x + width - 1, y + height - 1, CharCell::new('┘', style.fg_color)); + + // Horizontal lines + for col in (x + 1)..(x + width - 1) { + buffer.set_cell(col, y, CharCell::new('─', style.fg_color)); + buffer.set_cell(col, y + height - 1, CharCell::new('─', style.fg_color)); + } + + // Vertical lines + for row in (y + 1)..(y + height - 1) { + buffer.set_cell(x, row, CharCell::new('│', style.fg_color)); + buffer.set_cell(x + width - 1, row, CharCell::new('│', style.fg_color)); + } +} + +/// Get the default block font +pub fn get_block_font() -> FigFont { + block_font() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_render_text() { + let mut buffer = CharBuffer::new(20, 5); + let style = TextStyle::default(); + let width = render_text(&mut buffer, 0, 0, "Hello", &style); + assert_eq!(width, 5); + } +}