From 9e5244870b9dcbc977845ae8060afb8c9b421658 Mon Sep 17 00:00:00 2001 From: hu901131 Date: Sun, 15 Feb 2026 16:23:58 +0100 Subject: [PATCH 01/17] feat(barcode): scaffold package structure --- packages/barcode/license.md | 7 ++++ packages/barcode/package.json | 62 ++++++++++++++++++++++++++++++++++ packages/barcode/src/index.ts | 2 ++ packages/barcode/tsconfig.json | 5 +++ 4 files changed, 76 insertions(+) create mode 100644 packages/barcode/license.md create mode 100644 packages/barcode/package.json create mode 100644 packages/barcode/src/index.ts create mode 100644 packages/barcode/tsconfig.json diff --git a/packages/barcode/license.md b/packages/barcode/license.md new file mode 100644 index 0000000000..5058f53ebb --- /dev/null +++ b/packages/barcode/license.md @@ -0,0 +1,7 @@ +Copyright 2024 Plus Five Five, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/barcode/package.json b/packages/barcode/package.json new file mode 100644 index 0000000000..c2fc44e573 --- /dev/null +++ b/packages/barcode/package.json @@ -0,0 +1,62 @@ +{ + "name": "@react-email/barcode", + "version": "0.0.1", + "description": "Generate barcodes as pure HTML tables for email — no images needed.", + "sideEffects": false, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "scripts": { + "build": "tsdown src/index.ts --format esm,cjs --dts --external react", + "build:watch": "tsdown src/index.ts --format esm,cjs --dts --external react --watch", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest" + }, + "keywords": [ + "react", + "email", + "barcode", + "qrcode", + "html-table" + ], + "repository": { + "type": "git", + "url": "https://github.com/resend/react-email.git", + "directory": "packages/barcode" + }, + "engines": { + "node": ">=20.0.0" + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "dependencies": { + "bwip-js": "^4.5.1", + "qrcode-generator": "^1.4.4" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + }, + "devDependencies": { + "@react-email/render": "workspace:*", + "tsconfig": "workspace:*", + "typescript": "5.8.3" + } +} diff --git a/packages/barcode/src/index.ts b/packages/barcode/src/index.ts new file mode 100644 index 0000000000..e91bee05e6 --- /dev/null +++ b/packages/barcode/src/index.ts @@ -0,0 +1,2 @@ +export { Barcode } from './barcode'; +export type { BarcodeProps, BarcodeType } from './types'; diff --git a/packages/barcode/tsconfig.json b/packages/barcode/tsconfig.json new file mode 100644 index 0000000000..cd6c94d6e8 --- /dev/null +++ b/packages/barcode/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/react-library.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} From 77eff75624ca08a23d3fd2bc1233385a727052fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:24:18 +0100 Subject: [PATCH 02/17] feat(barcode): add types and alignment position table --- packages/barcode/src/align-pos.ts | 44 +++++++++++++++++ packages/barcode/src/types.ts | 81 +++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 packages/barcode/src/align-pos.ts create mode 100644 packages/barcode/src/types.ts diff --git a/packages/barcode/src/align-pos.ts b/packages/barcode/src/align-pos.ts new file mode 100644 index 0000000000..467d7693a0 --- /dev/null +++ b/packages/barcode/src/align-pos.ts @@ -0,0 +1,44 @@ +/** QR alignment pattern center positions by version (1-40). Index 0 is unused. */ +export const ALIGN_POS: (number[] | null)[] = [ + null, + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170], +]; diff --git a/packages/barcode/src/types.ts b/packages/barcode/src/types.ts new file mode 100644 index 0000000000..3bf28b7b6b --- /dev/null +++ b/packages/barcode/src/types.ts @@ -0,0 +1,81 @@ +import type * as React from 'react'; + +export type BarcodeType = + | 'qrcode' + | 'azteccode' + | 'datamatrix' + | 'code128' + | 'code39' + | 'ean13' + | 'upca'; + +export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'; + +export type Grid = boolean[][]; + +export interface Span { + w: number; + h: number; + dark: boolean; +} + +export interface BarcodeTypeConfig { + lib: 'qr' | 'bwip'; + type: '1d' | '2d'; + hasEC: boolean; + hasLossy: boolean; + ecRate?: number; +} + +export const BARCODE_TYPES: Record = { + qrcode: { lib: 'qr', type: '2d', hasEC: true, hasLossy: true }, + azteccode: { lib: 'bwip', type: '2d', hasEC: true, hasLossy: true }, + datamatrix: { + lib: 'bwip', + type: '2d', + hasEC: false, + hasLossy: true, + ecRate: 0.15, + }, + code128: { + lib: 'bwip', + type: '1d', + hasEC: false, + hasLossy: true, + ecRate: 0.02, + }, + code39: { + lib: 'bwip', + type: '1d', + hasEC: false, + hasLossy: true, + ecRate: 0.02, + }, + ean13: { + lib: 'bwip', + type: '1d', + hasEC: false, + hasLossy: true, + ecRate: 0.02, + }, + upca: { + lib: 'bwip', + type: '1d', + hasEC: false, + hasLossy: true, + ecRate: 0.02, + }, +}; + +export interface BarcodeProps + extends Omit, 'children'> { + value: string; + type?: BarcodeType; + foregroundColor?: string; + backgroundColor?: string; + cellSize?: number; + quietZone?: boolean; + errorCorrection?: ErrorCorrectionLevel; + lossy?: boolean; + lossyBudget?: number; +} From d2a1b12d3aa663fc72c89e073e6104e493b60077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:24:28 +0100 Subject: [PATCH 03/17] feat(barcode): add QR code generation via qrcode-generator --- packages/barcode/src/generate-qr.ts | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/barcode/src/generate-qr.ts diff --git a/packages/barcode/src/generate-qr.ts b/packages/barcode/src/generate-qr.ts new file mode 100644 index 0000000000..bfefd136b9 --- /dev/null +++ b/packages/barcode/src/generate-qr.ts @@ -0,0 +1,33 @@ +import qrcode from 'qrcode-generator'; +import type { ErrorCorrectionLevel, Grid } from './types'; + +export function generateQR( + text: string, + ecLevel: ErrorCorrectionLevel, + pad: number, +): { grid: Grid; moduleRows: number; moduleCols: number } { + const qr = qrcode(0, ecLevel); + qr.addData(text); + qr.make(); + + const moduleCount = qr.getModuleCount(); + const moduleRows = moduleCount; + const moduleCols = moduleCount; + const totalR = moduleRows + pad * 2; + const totalC = moduleCols + pad * 2; + + const grid: Grid = []; + for (let r = 0; r < totalR; r++) { + grid[r] = []; + for (let c = 0; c < totalC; c++) { + const qrR = r - pad; + const qrC = c - pad; + grid[r][c] = + qrR >= 0 && qrR < moduleCount && qrC >= 0 && qrC < moduleCount + ? qr.isDark(qrR, qrC) + : false; + } + } + + return { grid, moduleRows, moduleCols }; +} From e11c58ab22858102a08254b93c933210495babc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:33:14 +0100 Subject: [PATCH 04/17] feat(barcode): add custom bwip-js grid drawing backend --- packages/barcode/src/grid-drawing.ts | 148 +++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 packages/barcode/src/grid-drawing.ts diff --git a/packages/barcode/src/grid-drawing.ts b/packages/barcode/src/grid-drawing.ts new file mode 100644 index 0000000000..9385bdaf0c --- /dev/null +++ b/packages/barcode/src/grid-drawing.ts @@ -0,0 +1,148 @@ +/** + * Custom bwip-js drawing backend that records filled pixels into a boolean[][] + * grid. This avoids any canvas/image dependency and is synchronous. + */ +export class GridDrawing { + private _grid: boolean[][] = []; + private _width = 0; + private _height = 0; + private _polys: number[][][] = []; + + setopts(_opts: Record) {} + + scale(sx: number, sy: number): [number, number] { + return [sx, sy]; + } + + measure( + _str: string, + _font: string, + _fwidth: number, + _fheight: number, + ): { width: number; ascent: number; descent: number } { + return { width: 0, ascent: 0, descent: 0 }; + } + + init(width: number, height: number) { + this._width = Math.ceil(width); + this._height = Math.ceil(height); + this._grid = Array.from({ length: this._height }, () => + new Array(this._width).fill(false), + ); + } + + /** + * Draw a thick orthogonal line (used by 1D barcodes). + * Coordinates represent the line center; linew is the full thickness. + */ + line( + x0: number, + y0: number, + x1: number, + y1: number, + linew: number, + _rgb: string, + ) { + const half = linew / 2; + const w = Math.round(linew); + let minX: number, maxX: number, minY: number, maxY: number; + + if (Math.abs(y0 - y1) < 0.1) { + // Horizontal line + minX = Math.round(Math.min(x0, x1)); + maxX = Math.round(Math.max(x0, x1)); + minY = Math.round(y0 - half); + maxY = minY + w - 1; + } else { + // Vertical line + minX = Math.round(x0 - half); + maxX = minX + w - 1; + minY = Math.round(Math.min(y0, y1)); + maxY = Math.round(Math.max(y0, y1)); + } + + minX = Math.max(0, minX); + minY = Math.max(0, minY); + maxX = Math.min(this._width - 1, maxX); + maxY = Math.min(this._height - 1, maxY); + + for (let r = minY; r <= maxY; r++) + for (let c = minX; c <= maxX; c++) this._grid[r][c] = true; + } + + polygon(pts: number[][]) { + this._polys.push(pts); + } + + hexagon(_pts: number[][], _rgb: string) {} + ellipse(_x: number, _y: number, _rx: number, _ry: number, _ccw: boolean) {} + + /** + * Fill all accumulated polygons using scanline with non-zero winding rule. + * Polygons from bwip-js are always orthogonal (axis-aligned edges). + */ + fill(_rgb: string) { + // Collect all vertical edges from all polygons + const edges: { x: number; yMin: number; yMax: number; dir: number }[] = []; + + for (const poly of this._polys) { + const n = poly.length; + for (let i = 0; i < n; i++) { + const [x0, y0] = poly[i]; + const [x1, y1] = poly[(i + 1) % n]; + // Only vertical edges contribute to horizontal scanline crossings + if (Math.abs(x0 - x1) < 0.001) { + const yMin = Math.min(y0, y1); + const yMax = Math.max(y0, y1); + if (yMax - yMin > 0.001) { + edges.push({ x: x0, yMin, yMax, dir: y1 > y0 ? 1 : -1 }); + } + } + } + } + + for (let row = 0; row < this._height; row++) { + const scanY = row + 0.5; + + // Find vertical edges that cross this scanline + const crossings: { x: number; dir: number }[] = []; + for (const edge of edges) { + if (edge.yMin < scanY && scanY < edge.yMax) { + crossings.push({ x: edge.x, dir: edge.dir }); + } + } + + if (crossings.length === 0) continue; + crossings.sort((a, b) => a.x - b.x); + + // Walk pixels left to right, tracking winding number + let winding = 0; + let ci = 0; + for (let col = 0; col < this._width; col++) { + while (ci < crossings.length && crossings[ci].x <= col) { + winding += crossings[ci].dir; + ci++; + } + if (winding !== 0) { + this._grid[row][col] = true; + } + } + } + + this._polys = []; + } + + clip(_polys: number[][][]) {} + unclip() {} + text( + _x: number, + _y: number, + _str: string, + _rgb: string, + _font: unknown, + ) {} + + end(): boolean[][] { + return this._grid; + } +} From 1618ebc28dda33c8621dc51e7e22ca652594ee89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:33:15 +0100 Subject: [PATCH 05/17] feat(barcode): add bwip-js barcode generation --- packages/barcode/src/generate-bwip.ts | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/barcode/src/generate-bwip.ts diff --git a/packages/barcode/src/generate-bwip.ts b/packages/barcode/src/generate-bwip.ts new file mode 100644 index 0000000000..8311c5da67 --- /dev/null +++ b/packages/barcode/src/generate-bwip.ts @@ -0,0 +1,58 @@ +import bwipjs from 'bwip-js'; +import { GridDrawing } from './grid-drawing'; +import type { BarcodeType, ErrorCorrectionLevel, Grid } from './types'; + +const EC_MAP: Record = { + L: 10, + M: 23, + Q: 36, + H: 50, +}; + +export function generateBwip( + bcid: BarcodeType, + text: string, + hasEC: boolean, + ecLevel: ErrorCorrectionLevel, + pad: number, +): { grid: Grid; moduleRows: number; moduleCols: number } { + const drawing = new GridDrawing(); + + const opts: Record = { + bcid, + text, + scale: 1, + includetext: false, + padding: 0, + }; + + if (hasEC) { + opts.eclevel = EC_MAP[ecLevel] || 23; + } + + bwipjs.render(opts as Parameters[0], drawing as never); + + const rawGrid = drawing.end(); + const moduleRows = rawGrid.length; + const moduleCols = moduleRows > 0 ? rawGrid[0].length : 0; + + if (pad > 0) { + const totalR = moduleRows + pad * 2; + const totalC = moduleCols + pad * 2; + const padded: Grid = []; + for (let r = 0; r < totalR; r++) { + padded[r] = []; + for (let c = 0; c < totalC; c++) { + const sr = r - pad; + const sc = c - pad; + padded[r][c] = + sr >= 0 && sr < moduleRows && sc >= 0 && sc < moduleCols + ? rawGrid[sr][sc] + : false; + } + } + return { grid: padded, moduleRows, moduleCols }; + } + + return { grid: rawGrid, moduleRows, moduleCols }; +} From 67d66651a9affc92d53d8d5bcc3f99782381f52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:33:16 +0100 Subject: [PATCH 06/17] feat(barcode): add lossy compression algorithms --- packages/barcode/src/compression.ts | 218 ++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 packages/barcode/src/compression.ts diff --git a/packages/barcode/src/compression.ts b/packages/barcode/src/compression.ts new file mode 100644 index 0000000000..a1c7d2a449 --- /dev/null +++ b/packages/barcode/src/compression.ts @@ -0,0 +1,218 @@ +import { ALIGN_POS } from './align-pos'; +import type { Grid } from './types'; + +/** + * Build protection mask for QR structural patterns. + * Returns a 2D Uint8Array grid in total (padded) coordinates. + * Protected cells (1) will not be flipped during lossy compression. + */ +export function buildProtectionMask( + moduleCount: number, + pad: number, + totalSize: number, +): Uint8Array[] { + const p = Array.from({ length: totalSize }, () => new Uint8Array(totalSize)); + const n = moduleCount; + + function protectQR(r0: number, c0: number, r1: number, c1: number) { + for ( + let r = Math.max(0, r0 + pad); + r <= Math.min(totalSize - 1, r1 + pad); + r++ + ) + for ( + let c = Math.max(0, c0 + pad); + c <= Math.min(totalSize - 1, c1 + pad); + c++ + ) + p[r][c] = 1; + } + + // 1. Quiet zone (all padding cells) + for (let r = 0; r < totalSize; r++) + for (let c = 0; c < totalSize; c++) + if ( + r < pad || + r >= totalSize - pad || + c < pad || + c >= totalSize - pad + ) + p[r][c] = 1; + + // 2. Finder patterns (7x7) + separator (1 module border) + protectQR(-1, -1, 7, 7); + protectQR(-1, n - 8, 7, n); + protectQR(n - 8, -1, n, 7); + + // 3. Timing patterns (row 6 and col 6) + for (let i = 0; i < n; i++) { + p[6 + pad][i + pad] = 1; + p[i + pad][6 + pad] = 1; + } + + // 4. Format information areas + for (let i = 0; i <= 8; i++) { + if (i + pad < totalSize) p[8 + pad][i + pad] = 1; + if (i + pad < totalSize) p[i + pad][8 + pad] = 1; + } + for (let i = 0; i < 8; i++) { + if (n - 1 - i >= 0) p[8 + pad][(n - 1 - i) + pad] = 1; + } + for (let i = 0; i < 7; i++) { + if (n - 1 - i >= 0) p[(n - 1 - i) + pad][8 + pad] = 1; + } + // Dark module + if (4 * 1 + 9 < n) p[(n - 8) + pad][8 + pad] = 1; + + // 5. Alignment patterns (5x5 each) + const version = Math.ceil((n - 17) / 4); + if (version >= 2 && version < ALIGN_POS.length) { + const positions = ALIGN_POS[version]; + if (positions) { + for (const ar of positions) { + for (const ac of positions) { + if (ar <= 8 && ac <= 8) continue; + if (ar <= 8 && ac >= n - 8) continue; + if (ar >= n - 8 && ac <= 8) continue; + protectQR(ar - 2, ac - 2, ar + 2, ac + 2); + } + } + } + } + + // 6. Version information (versions 7+) + if (version >= 7) { + protectQR(0, n - 11, 5, n - 8); + protectQR(n - 11, 0, n - 8, 5); + } + + return p; +} + +/** + * Build a protection mask that only protects quiet zone padding cells. + * Used for non-QR barcode types. + */ +export function buildQuietZoneMask( + pad: number, + totalRows: number, + totalCols: number, +): Uint8Array[] { + const p = Array.from({ length: totalRows }, () => new Uint8Array(totalCols)); + if (pad > 0) { + for (let r = 0; r < totalRows; r++) + for (let c = 0; c < totalCols; c++) + if ( + r < pad || + r >= totalRows - pad || + c < pad || + c >= totalCols - pad + ) + p[r][c] = 1; + } + return p; +} + +/** + * Lossy compression: iteratively flip isolated data modules to aid merging. + * Uses per-flip re-scoring so each flip updates the isolation landscape + * for its neighbors, producing better cascading improvements. + */ +export function applyLossyCompression( + grid: Grid, + pad: number, + totalRows: number, + totalCols: number, + protect: Uint8Array[], + ecPct: number, + budget: number, +): number { + let dataModules = 0; + for (let r = pad; r < totalRows - pad; r++) + for (let c = pad; c < totalCols - pad; c++) + if (!protect[r][c]) dataModules++; + + const maxFlips = Math.floor(dataModules * ecPct * 0.5 * budget); + if (maxFlips === 0) return 0; + + const wasFlipped = new Uint8Array(totalRows * totalCols); + + function scoreAt(r: number, c: number): number { + if (protect[r][c] || wasFlipped[r * totalCols + c]) return -1; + const val = grid[r][c]; + let cardDiff = 0; + if (r > 0 && grid[r - 1][c] !== val) cardDiff++; + if (r < totalRows - 1 && grid[r + 1][c] !== val) cardDiff++; + if (c > 0 && grid[r][c - 1] !== val) cardDiff++; + if (c < totalCols - 1 && grid[r][c + 1] !== val) cardDiff++; + if (cardDiff < 2) return -1; + let diagDiff = 0; + if (r > 0 && c > 0 && grid[r - 1][c - 1] !== val) diagDiff++; + if (r > 0 && c < totalCols - 1 && grid[r - 1][c + 1] !== val) diagDiff++; + if (r < totalRows - 1 && c > 0 && grid[r + 1][c - 1] !== val) diagDiff++; + if (r < totalRows - 1 && c < totalCols - 1 && grid[r + 1][c + 1] !== val) + diagDiff++; + return cardDiff * 10 + diagDiff * 3; + } + + // Build initial score grid + const scores = Array.from({ length: totalRows }, () => + new Array(totalCols).fill(-1), + ); + for (let r = pad; r < totalRows - pad; r++) + for (let c = pad; c < totalCols - pad; c++) scores[r][c] = scoreAt(r, c); + + let flippedCount = 0; + while (flippedCount < maxFlips) { + let bestR = -1; + let bestC = -1; + let bestScore = -1; + for (let r = pad; r < totalRows - pad; r++) + for (let c = pad; c < totalCols - pad; c++) + if (scores[r][c] > bestScore) { + bestScore = scores[r][c]; + bestR = r; + bestC = c; + } + if (bestScore < 0) break; + + grid[bestR][bestC] = !grid[bestR][bestC]; + wasFlipped[bestR * totalCols + bestC] = 1; + flippedCount++; + + // Re-score the 3x3 neighborhood + for (let dr = -1; dr <= 1; dr++) + for (let dc = -1; dc <= 1; dc++) { + const nr = bestR + dr; + const nc = bestC + dc; + if ( + nr >= pad && + nr < totalRows - pad && + nc >= pad && + nc < totalCols - pad + ) + scores[nr][nc] = scoreAt(nr, nc); + } + } + + return flippedCount; +} + +/** + * 1D barcodes: enforce column uniformity by replicating the middle data row. + * Edge rows near the quiet zone get artificially high isolation scores, + * causing scattered single-row flips that break the vertical-bar structure. + */ +export function enforceColumnUniformity( + grid: Grid, + pad: number, + totalRows: number, + totalCols: number, + moduleRows: number, +) { + const refRow = pad + Math.floor(moduleRows / 2); + for (let r = pad; r < totalRows - pad; r++) { + if (r === refRow) continue; + for (let c = pad; c < totalCols - pad; c++) grid[r][c] = grid[refRow][c]; + } +} From f3ed67b8f316a667f6e770f2ccbf5f35e6a5696a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:33:17 +0100 Subject: [PATCH 07/17] feat(barcode): add D4 rectangle packing --- packages/barcode/src/packing.ts | 151 ++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 packages/barcode/src/packing.ts diff --git a/packages/barcode/src/packing.ts b/packages/barcode/src/packing.ts new file mode 100644 index 0000000000..1e482f6809 --- /dev/null +++ b/packages/barcode/src/packing.ts @@ -0,0 +1,151 @@ +import type { Grid, Span } from './types'; + +/** + * Greedy rectangle packing: scan top-left to bottom-right, at each uncovered + * cell find the maximal-area same-color rectangle anchored there. + */ +export function packGreedy( + grid: Grid, + totalRows: number, + totalCols: number, +): { spans: (Span | null)[][]; cellCount: number } { + const used = Array.from({ length: totalRows }, () => + new Uint8Array(totalCols), + ); + + function findBestRect(r: number, c: number) { + const val = grid[r][c]; + let maxW = 0; + while ( + c + maxW < totalCols && + grid[r][c + maxW] === val && + !used[r][c + maxW] + ) + maxW++; + + let bestArea = 0; + let bestW = 0; + let bestH = 0; + let curW = maxW; + + for (let h = 1; r + h - 1 < totalRows; h++) { + if (h > 1) { + const row = r + h - 1; + let newW = 0; + while ( + newW < curW && + c + newW < totalCols && + grid[row][c + newW] === val && + !used[row][c + newW] + ) + newW++; + curW = newW; + if (curW === 0) break; + } + const area = curW * h; + if (area > bestArea) { + bestArea = area; + bestW = curW; + bestH = h; + } + } + return { w: bestW, h: bestH }; + } + + const spans: (Span | null)[][] = []; + for (let r = 0; r < totalRows; r++) + spans[r] = new Array(totalCols).fill(null); + + let cellCount = 0; + for (let r = 0; r < totalRows; r++) { + for (let c = 0; c < totalCols; c++) { + if (used[r][c]) continue; + const { w, h } = findBestRect(r, c); + spans[r][c] = { w, h, dark: grid[r][c] }; + for (let dr = 0; dr < h; dr++) + for (let dc = 0; dc < w; dc++) used[r + dr][c + dc] = 1; + cellCount++; + } + } + + return { spans, cellCount }; +} + +/** + * Try all 8 orientations of the dihedral group D4 (4 reflections x + * identity/transpose) and return the packing with the fewest cells. + */ +export function packBestOrientation( + grid: Grid, + totalRows: number, + totalCols: number, +): { spans: (Span | null)[][]; cellCount: number } { + const R = totalRows; + const C = totalCols; + + // Identity (original scan order) + let best = packGreedy(grid, R, C); + + const orientations: [boolean, boolean, boolean][] = [ + [false, true, false], + [false, false, true], + [false, true, true], + [true, false, false], + [true, true, false], + [true, false, true], + [true, true, true], + ]; + + for (const [trans, fH, fV] of orientations) { + const tR = trans ? C : R; + const tC = trans ? R : C; + const tGrid: Grid = Array.from({ length: tR }, (_, r) => { + const row = new Array(tC); + for (let c = 0; c < tC; c++) { + let srcR = trans ? c : r; + let srcC = trans ? r : c; + if (fV) srcR = R - 1 - srcR; + if (fH) srcC = C - 1 - srcC; + row[c] = grid[srcR][srcC]; + } + return row; + }); + + const result = packGreedy(tGrid, tR, tC); + if (result.cellCount >= best.cellCount) continue; + + // Map spans back to original coordinates + const newSpans: (Span | null)[][] = Array.from({ length: R }, () => + new Array(C).fill(null), + ); + for (let r = 0; r < tR; r++) + for (let c = 0; c < tC; c++) { + const s = result.spans[r][c]; + if (!s) continue; + let tlR = trans ? c : r; + let tlC = trans ? r : c; + let brR = trans ? c + s.w - 1 : r + s.h - 1; + let brC = trans ? r + s.h - 1 : c + s.w - 1; + if (fV) { + tlR = R - 1 - tlR; + brR = R - 1 - brR; + } + if (fH) { + tlC = C - 1 - tlC; + brC = C - 1 - brC; + } + const minR = Math.min(tlR, brR); + const minC = Math.min(tlC, brC); + const maxR = Math.max(tlR, brR); + const maxC = Math.max(tlC, brC); + newSpans[minR][minC] = { + w: maxC - minC + 1, + h: maxR - minR + 1, + dark: s.dark, + }; + } + best = { spans: newSpans, cellCount: result.cellCount }; + } + + return best; +} From 8fe37ff8cac9b499342fd3f94b685bdcffb7ec91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:33:17 +0100 Subject: [PATCH 08/17] feat(barcode): add HTML table renderer --- packages/barcode/src/table-renderer.ts | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/barcode/src/table-renderer.ts diff --git a/packages/barcode/src/table-renderer.ts b/packages/barcode/src/table-renderer.ts new file mode 100644 index 0000000000..a9ad59a95b --- /dev/null +++ b/packages/barcode/src/table-renderer.ts @@ -0,0 +1,51 @@ +import type { Span } from './types'; + +function shortHex(hex: string): string { + if ( + hex.length === 7 && + hex[1] === hex[2] && + hex[3] === hex[4] && + hex[5] === hex[6] + ) + return '#' + hex[1] + hex[3] + hex[5]; + return hex; +} + +/** + * Convert packed spans into an HTML table string with inline styles. + * Includes Safari zero-width sizing cell fix for rowspan height. + */ +export function renderTable( + spans: (Span | null)[][], + totalRows: number, + totalCols: number, + cellSize: number, + foregroundColor: string, + backgroundColor: string, +): string { + const fg = shortHex(foregroundColor); + const bg = shortHex(backgroundColor); + const cs = cellSize; + + let html = ``; + + for (let r = 0; r < totalRows; r++) { + html += ''; + // Zero-width sizing cell to lock row height in Safari + html += ``; + for (let c = 0; c < totalCols; c++) { + const s = spans[r][c]; + if (!s) continue; + let attrs = ''; + if (s.w > 1) attrs += ` colspan="${s.w}"`; + if (s.h > 1) attrs += ` rowspan="${s.h}"`; + let style = `width:${cs * s.w}px;height:${cs * s.h}px;padding:0`; + if (s.dark) style += `;background:${fg}`; + html += ``; + } + html += ''; + } + + html += '
'; + return html; +} From 7e0fb42d9f03c4a8b57e016cc8a7387d00e396f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:33:40 +0100 Subject: [PATCH 09/17] feat(barcode): add React component and wire barrel exports --- packages/barcode/src/barcode.tsx | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 packages/barcode/src/barcode.tsx diff --git a/packages/barcode/src/barcode.tsx b/packages/barcode/src/barcode.tsx new file mode 100644 index 0000000000..01d227f369 --- /dev/null +++ b/packages/barcode/src/barcode.tsx @@ -0,0 +1,128 @@ +import * as React from 'react'; +import { + applyLossyCompression, + buildProtectionMask, + buildQuietZoneMask, + enforceColumnUniformity, +} from './compression'; +import { generateBwip } from './generate-bwip'; +import { generateQR } from './generate-qr'; +import { packBestOrientation } from './packing'; +import { renderTable } from './table-renderer'; +import { + BARCODE_TYPES, + type BarcodeProps, + type ErrorCorrectionLevel, +} from './types'; + +const EC_RATES: Record = { + L: 0.07, + M: 0.15, + Q: 0.25, + H: 0.3, +}; + +const BWIP_EC_RATES: Record = { + L: 0.1, + M: 0.23, + Q: 0.36, + H: 0.5, +}; + +export const Barcode = React.forwardRef( + ( + { + value, + type = 'qrcode', + foregroundColor = '#000000', + backgroundColor = '#ffffff', + cellSize = 4, + quietZone = true, + errorCorrection = 'M', + lossy = false, + lossyBudget = 0.2, + style, + ...props + }, + ref, + ) => { + const cfg = BARCODE_TYPES[type]; + const pad = quietZone ? 4 : 0; + + let grid: boolean[][]; + let moduleRows: number; + let moduleCols: number; + + if (cfg.lib === 'qr') { + const result = generateQR(value, errorCorrection, pad); + grid = result.grid; + moduleRows = result.moduleRows; + moduleCols = result.moduleCols; + } else { + const result = generateBwip( + type, + value, + cfg.hasEC, + errorCorrection, + pad, + ); + grid = result.grid; + moduleRows = result.moduleRows; + moduleCols = result.moduleCols; + } + + const totalRows = moduleRows + pad * 2; + const totalCols = moduleCols + pad * 2; + + // Apply lossy compression + if (cfg.hasLossy && lossy) { + let protect: Uint8Array[]; + let ecPct: number; + if (type === 'qrcode') { + protect = buildProtectionMask(moduleRows, pad, totalRows); + ecPct = EC_RATES[errorCorrection]; + } else { + protect = buildQuietZoneMask(pad, totalRows, totalCols); + ecPct = cfg.hasEC + ? BWIP_EC_RATES[errorCorrection] + : (cfg.ecRate ?? 0.02); + } + const flipped = applyLossyCompression( + grid, + pad, + totalRows, + totalCols, + protect, + ecPct, + lossyBudget, + ); + + // 1D: enforce column uniformity + if (cfg.type === '1d' && flipped > 0) { + enforceColumnUniformity(grid, pad, totalRows, totalCols, moduleRows); + } + } + + const { spans } = packBestOrientation(grid, totalRows, totalCols); + const tableHTML = renderTable( + spans, + totalRows, + totalCols, + cellSize, + foregroundColor, + backgroundColor, + ); + + return ( +
+ ); + }, +); + +Barcode.displayName = 'Barcode'; From e7aec550d9c3ac45737ee8351ab2feb1c752490c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 16:36:11 +0100 Subject: [PATCH 10/17] test(barcode): add component tests --- packages/barcode/src/barcode.spec.tsx | 97 +++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/barcode/src/barcode.spec.tsx diff --git a/packages/barcode/src/barcode.spec.tsx b/packages/barcode/src/barcode.spec.tsx new file mode 100644 index 0000000000..9a4fce9eae --- /dev/null +++ b/packages/barcode/src/barcode.spec.tsx @@ -0,0 +1,97 @@ +import { render } from '@react-email/render'; +import { Barcode } from './index'; + +describe(' component', () => { + it('renders a QR code as an HTML table', async () => { + const html = await render( + , + ); + expect(html).toContain('data-id="react-email-barcode"'); + expect(html).toContain(' { + const html = await render( + , + ); + expect(html).toContain(' { + const html = await render( + , + ); + expect(html).toContain(' { + const html = await render( + , + ); + expect(html).toContain('background:#f00'); + expect(html).toContain('background:#0f0'); + }); + + it('respects custom cellSize', async () => { + const html = await render( + , + ); + // Cell size should appear in width/height styles + expect(html).toContain('height:8px'); + }); + + it('renders without quiet zone when disabled', async () => { + const withQuiet = await render( + , + ); + const withoutQuiet = await render( + , + ); + // Without quiet zone should have fewer rows (less HTML) + expect(withoutQuiet.length).toBeLessThan(withQuiet.length); + }); + + it('renders with lossy compression enabled', async () => { + const html = await render( + , + ); + expect(html).toContain(' { + const html = await render( + , + ); + // Each should have a zero-width sizing cell + expect(html).toContain('width:0;height:4px;padding:0'); + }); + + it('passes through extra props', async () => { + const html = await render( + , + ); + expect(html).toContain('data-testid="barcode-test"'); + expect(html).toContain('padding:16px'); + }); +}); From 23e74cc95048deaa42473474e44871613d58aebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 17:23:11 +0100 Subject: [PATCH 11/17] fix(barcode): encode QR input as UTF-8 byte mode --- packages/barcode/src/generate-qr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/barcode/src/generate-qr.ts b/packages/barcode/src/generate-qr.ts index bfefd136b9..f75745e98b 100644 --- a/packages/barcode/src/generate-qr.ts +++ b/packages/barcode/src/generate-qr.ts @@ -1,13 +1,13 @@ import qrcode from 'qrcode-generator'; import type { ErrorCorrectionLevel, Grid } from './types'; -export function generateQR( +export function generateQr( text: string, ecLevel: ErrorCorrectionLevel, pad: number, ): { grid: Grid; moduleRows: number; moduleCols: number } { const qr = qrcode(0, ecLevel); - qr.addData(text); + qr.addData(text, 'Byte'); qr.make(); const moduleCount = qr.getModuleCount(); From eaad25665574c73cc3637110447fd2bff70e0ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 17:23:16 +0100 Subject: [PATCH 12/17] refactor(barcode): rename hasEC/generateQR to hasEc/generateQr --- packages/barcode/src/barcode.tsx | 8 ++++---- packages/barcode/src/generate-bwip.ts | 4 ++-- packages/barcode/src/types.ts | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/barcode/src/barcode.tsx b/packages/barcode/src/barcode.tsx index 01d227f369..4f3eb1aec9 100644 --- a/packages/barcode/src/barcode.tsx +++ b/packages/barcode/src/barcode.tsx @@ -6,7 +6,7 @@ import { enforceColumnUniformity, } from './compression'; import { generateBwip } from './generate-bwip'; -import { generateQR } from './generate-qr'; +import { generateQr } from './generate-qr'; import { packBestOrientation } from './packing'; import { renderTable } from './table-renderer'; import { @@ -54,7 +54,7 @@ export const Barcode = React.forwardRef( let moduleCols: number; if (cfg.lib === 'qr') { - const result = generateQR(value, errorCorrection, pad); + const result = generateQr(value, errorCorrection, pad); grid = result.grid; moduleRows = result.moduleRows; moduleCols = result.moduleCols; @@ -62,7 +62,7 @@ export const Barcode = React.forwardRef( const result = generateBwip( type, value, - cfg.hasEC, + cfg.hasEc, errorCorrection, pad, ); @@ -83,7 +83,7 @@ export const Barcode = React.forwardRef( ecPct = EC_RATES[errorCorrection]; } else { protect = buildQuietZoneMask(pad, totalRows, totalCols); - ecPct = cfg.hasEC + ecPct = cfg.hasEc ? BWIP_EC_RATES[errorCorrection] : (cfg.ecRate ?? 0.02); } diff --git a/packages/barcode/src/generate-bwip.ts b/packages/barcode/src/generate-bwip.ts index 8311c5da67..b8efe38da1 100644 --- a/packages/barcode/src/generate-bwip.ts +++ b/packages/barcode/src/generate-bwip.ts @@ -12,7 +12,7 @@ const EC_MAP: Record = { export function generateBwip( bcid: BarcodeType, text: string, - hasEC: boolean, + hasEc: boolean, ecLevel: ErrorCorrectionLevel, pad: number, ): { grid: Grid; moduleRows: number; moduleCols: number } { @@ -26,7 +26,7 @@ export function generateBwip( padding: 0, }; - if (hasEC) { + if (hasEc) { opts.eclevel = EC_MAP[ecLevel] || 23; } diff --git a/packages/barcode/src/types.ts b/packages/barcode/src/types.ts index 3bf28b7b6b..4bea2ed50b 100644 --- a/packages/barcode/src/types.ts +++ b/packages/barcode/src/types.ts @@ -22,46 +22,46 @@ export interface Span { export interface BarcodeTypeConfig { lib: 'qr' | 'bwip'; type: '1d' | '2d'; - hasEC: boolean; + hasEc: boolean; hasLossy: boolean; ecRate?: number; } export const BARCODE_TYPES: Record = { - qrcode: { lib: 'qr', type: '2d', hasEC: true, hasLossy: true }, - azteccode: { lib: 'bwip', type: '2d', hasEC: true, hasLossy: true }, + qrcode: { lib: 'qr', type: '2d', hasEc: true, hasLossy: true }, + azteccode: { lib: 'bwip', type: '2d', hasEc: true, hasLossy: true }, datamatrix: { lib: 'bwip', type: '2d', - hasEC: false, + hasEc: false, hasLossy: true, ecRate: 0.15, }, code128: { lib: 'bwip', type: '1d', - hasEC: false, + hasEc: false, hasLossy: true, ecRate: 0.02, }, code39: { lib: 'bwip', type: '1d', - hasEC: false, + hasEc: false, hasLossy: true, ecRate: 0.02, }, ean13: { lib: 'bwip', type: '1d', - hasEC: false, + hasEc: false, hasLossy: true, ecRate: 0.02, }, upca: { lib: 'bwip', type: '1d', - hasEC: false, + hasEc: false, hasLossy: true, ecRate: 0.02, }, From 3b77f40ceea78988aa30d45b34b6c34730652cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 17:31:17 +0100 Subject: [PATCH 13/17] chore(barcode): update lockfile with barcode dependencies --- pnpm-lock.yaml | 173 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da95224c25..b9256dc20a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,7 +113,7 @@ importers: dependencies: mintlify: specifier: 4.2.280 - version: 4.2.280(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + version: 4.2.280(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) zod: specifier: 3.24.3 version: 3.24.3 @@ -310,6 +310,28 @@ importers: specifier: 5.8.3 version: 5.8.3 + packages/barcode: + dependencies: + bwip-js: + specifier: ^4.5.1 + version: 4.8.0 + qrcode-generator: + specifier: ^1.4.4 + version: 1.5.2 + react: + specifier: ^19.0.0 + version: 19.0.0 + devDependencies: + '@react-email/render': + specifier: workspace:* + version: link:../render + tsconfig: + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: 5.8.3 + version: 5.8.3 + packages/body: dependencies: react: @@ -5174,6 +5196,10 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + bwip-js@4.8.0: + resolution: {integrity: sha512-gUDkDHSTv8/DJhomSIbO0fX/Dx0MO/sgllLxJyJfu4WixCQe9nfGJzmHm64ZCbxo+gUYQEsQcRmqcwcwPRwUkg==} + hasBin: true + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -8057,6 +8083,9 @@ packages: deprecated: < 24.15.0 is no longer supported hasBin: true + qrcode-generator@1.5.2: + resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} + qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -11270,6 +11299,36 @@ snapshots: - acorn - supports-color + '@mdx-js/mdx@3.1.0(acorn@8.15.0)': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.3 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.15.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - acorn + - supports-color + '@mdx-js/react@3.1.0(@types/react@19.2.13)(react@19.0.0)': dependencies: '@types/mdx': 2.0.13 @@ -11278,15 +11337,15 @@ snapshots: '@mediapipe/tasks-vision@0.10.17': {} - '@mintlify/cli@4.0.884(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': + '@mintlify/cli@4.0.884(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': dependencies: '@inquirer/prompts': 7.9.0(@types/node@25.0.6) '@mintlify/common': 1.0.666(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/link-rot': 3.0.823(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/link-rot': 3.0.823(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) '@mintlify/models': 0.0.257 - '@mintlify/prebuild': 1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/previewing': 4.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) - '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/prebuild': 1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/previewing': 4.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) adm-zip: 0.5.16 chalk: 5.2.0 color: 4.2.3 @@ -11437,13 +11496,13 @@ snapshots: - ts-node - typescript - '@mintlify/link-rot@3.0.823(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': + '@mintlify/link-rot@3.0.823(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': dependencies: '@mintlify/common': 1.0.666(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/prebuild': 1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/previewing': 4.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + '@mintlify/prebuild': 1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/previewing': 4.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) '@mintlify/scraping': 4.0.522(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) fs-extra: 11.1.0 unist-util-visit: 4.1.2 transitivePeerDependencies: @@ -11489,6 +11548,33 @@ snapshots: - supports-color - typescript + '@mintlify/mdx@3.0.4(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': + dependencies: + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@shikijs/transformers': 3.15.0 + '@shikijs/twoslash': 3.15.0(typescript@5.9.3) + arktype: 2.1.27 + hast-util-to-string: 3.0.1 + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm: 3.1.0 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-to-hast: 13.2.0 + next-mdx-remote-client: 1.0.7(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + rehype-katex: 7.0.1 + remark-gfm: 4.0.1 + remark-math: 6.0.0 + remark-smartypants: 3.0.2 + shiki: 3.15.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - '@types/react' + - acorn + - supports-color + - typescript + '@mintlify/models@0.0.255': dependencies: axios: 1.10.0 @@ -11512,12 +11598,12 @@ snapshots: leven: 4.0.0 yaml: 2.6.1 - '@mintlify/prebuild@1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': + '@mintlify/prebuild@1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': dependencies: '@mintlify/common': 1.0.666(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) '@mintlify/openapi-parser': 0.0.8 '@mintlify/scraping': 4.0.527(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) chalk: 5.3.0 favicons: 7.2.0 front-matter: 4.0.2 @@ -11543,11 +11629,11 @@ snapshots: - typescript - utf-8-validate - '@mintlify/previewing@4.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': + '@mintlify/previewing@4.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': dependencies: '@mintlify/common': 1.0.666(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/prebuild': 1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/prebuild': 1.0.801(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/validation': 0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) better-opn: 3.0.2 chalk: 5.2.0 chokidar: 3.5.3 @@ -11692,6 +11778,29 @@ snapshots: - supports-color - typescript + '@mintlify/validation@0.1.558(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': + dependencies: + '@mintlify/mdx': 3.0.4(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/models': 0.0.257 + arktype: 2.1.27 + js-yaml: 4.1.0 + lcm: 0.0.3 + lodash: 4.17.21 + object-hash: 3.0.0 + openapi-types: 12.1.3 + uuid: 11.1.0 + zod: 3.21.4 + zod-to-json-schema: 3.20.4(zod@3.21.4) + transitivePeerDependencies: + - '@radix-ui/react-popover' + - '@types/react' + - acorn + - debug + - react + - react-dom + - supports-color + - typescript + '@monogrid/gainmap-js@3.1.0(three@0.170.0)': dependencies: promise-worker-transferable: 1.0.4 @@ -14039,6 +14148,8 @@ snapshots: dependencies: streamsearch: 1.1.0 + bwip-js@4.8.0: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -16864,9 +16975,9 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - mintlify@4.2.280(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3): + mintlify@4.2.280(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3): dependencies: - '@mintlify/cli': 4.0.884(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + '@mintlify/cli': 4.0.884(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@25.0.6)(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) transitivePeerDependencies: - '@radix-ui/react-popover' - '@types/node' @@ -16944,6 +17055,22 @@ snapshots: - acorn - supports-color + next-mdx-remote-client@1.0.7(@types/react@19.2.13)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/code-frame': 7.27.1 + '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + '@mdx-js/react': 3.1.0(@types/react@19.2.13)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + remark-mdx-remove-esm: 1.1.0 + serialize-error: 12.0.0 + vfile: 6.0.3 + vfile-matter: 5.0.0 + transitivePeerDependencies: + - '@types/react' + - acorn + - supports-color + next-safe-action@8.0.11(next@16.1.6(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: next: 16.1.6(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -17536,6 +17663,8 @@ snapshots: - typescript - utf-8-validate + qrcode-generator@1.5.2: {} + qs@6.11.0: dependencies: side-channel: 1.1.0 @@ -17764,6 +17893,16 @@ snapshots: transitivePeerDependencies: - acorn + recma-jsx@1.0.0(acorn@8.15.0): + dependencies: + acorn-jsx: 5.3.2(acorn@8.15.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + recma-parse@1.0.0: dependencies: '@types/estree': 1.0.8 From 4285fd7f8f4c20101a0e3bfaa82e932dae03c392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 17:31:20 +0100 Subject: [PATCH 14/17] style(barcode): fix biome lint and formatting issues --- packages/barcode/src/barcode.tsx | 8 +------- packages/barcode/src/compression.ts | 20 +++++--------------- packages/barcode/src/grid-drawing.ts | 13 +++++-------- packages/barcode/src/packing.ts | 5 +++-- packages/barcode/src/table-renderer.ts | 2 +- 5 files changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/barcode/src/barcode.tsx b/packages/barcode/src/barcode.tsx index 4f3eb1aec9..d4f6c80079 100644 --- a/packages/barcode/src/barcode.tsx +++ b/packages/barcode/src/barcode.tsx @@ -59,13 +59,7 @@ export const Barcode = React.forwardRef( moduleRows = result.moduleRows; moduleCols = result.moduleCols; } else { - const result = generateBwip( - type, - value, - cfg.hasEc, - errorCorrection, - pad, - ); + const result = generateBwip(type, value, cfg.hasEc, errorCorrection, pad); grid = result.grid; moduleRows = result.moduleRows; moduleCols = result.moduleCols; diff --git a/packages/barcode/src/compression.ts b/packages/barcode/src/compression.ts index a1c7d2a449..d6eb00dce7 100644 --- a/packages/barcode/src/compression.ts +++ b/packages/barcode/src/compression.ts @@ -31,12 +31,7 @@ export function buildProtectionMask( // 1. Quiet zone (all padding cells) for (let r = 0; r < totalSize; r++) for (let c = 0; c < totalSize; c++) - if ( - r < pad || - r >= totalSize - pad || - c < pad || - c >= totalSize - pad - ) + if (r < pad || r >= totalSize - pad || c < pad || c >= totalSize - pad) p[r][c] = 1; // 2. Finder patterns (7x7) + separator (1 module border) @@ -56,13 +51,13 @@ export function buildProtectionMask( if (i + pad < totalSize) p[i + pad][8 + pad] = 1; } for (let i = 0; i < 8; i++) { - if (n - 1 - i >= 0) p[8 + pad][(n - 1 - i) + pad] = 1; + if (n - 1 - i >= 0) p[8 + pad][n - 1 - i + pad] = 1; } for (let i = 0; i < 7; i++) { - if (n - 1 - i >= 0) p[(n - 1 - i) + pad][8 + pad] = 1; + if (n - 1 - i >= 0) p[n - 1 - i + pad][8 + pad] = 1; } // Dark module - if (4 * 1 + 9 < n) p[(n - 8) + pad][8 + pad] = 1; + if (4 * 1 + 9 < n) p[n - 8 + pad][8 + pad] = 1; // 5. Alignment patterns (5x5 each) const version = Math.ceil((n - 17) / 4); @@ -102,12 +97,7 @@ export function buildQuietZoneMask( if (pad > 0) { for (let r = 0; r < totalRows; r++) for (let c = 0; c < totalCols; c++) - if ( - r < pad || - r >= totalRows - pad || - c < pad || - c >= totalCols - pad - ) + if (r < pad || r >= totalRows - pad || c < pad || c >= totalCols - pad) p[r][c] = 1; } return p; diff --git a/packages/barcode/src/grid-drawing.ts b/packages/barcode/src/grid-drawing.ts index 9385bdaf0c..ea955c0401 100644 --- a/packages/barcode/src/grid-drawing.ts +++ b/packages/barcode/src/grid-drawing.ts @@ -45,7 +45,10 @@ export class GridDrawing { ) { const half = linew / 2; const w = Math.round(linew); - let minX: number, maxX: number, minY: number, maxY: number; + let minX: number; + let maxX: number; + let minY: number; + let maxY: number; if (Math.abs(y0 - y1) < 0.1) { // Horizontal line @@ -134,13 +137,7 @@ export class GridDrawing { clip(_polys: number[][][]) {} unclip() {} - text( - _x: number, - _y: number, - _str: string, - _rgb: string, - _font: unknown, - ) {} + text(_x: number, _y: number, _str: string, _rgb: string, _font: unknown) {} end(): boolean[][] { return this._grid; diff --git a/packages/barcode/src/packing.ts b/packages/barcode/src/packing.ts index 1e482f6809..c5f4128d80 100644 --- a/packages/barcode/src/packing.ts +++ b/packages/barcode/src/packing.ts @@ -9,8 +9,9 @@ export function packGreedy( totalRows: number, totalCols: number, ): { spans: (Span | null)[][]; cellCount: number } { - const used = Array.from({ length: totalRows }, () => - new Uint8Array(totalCols), + const used = Array.from( + { length: totalRows }, + () => new Uint8Array(totalCols), ); function findBestRect(r: number, c: number) { diff --git a/packages/barcode/src/table-renderer.ts b/packages/barcode/src/table-renderer.ts index a9ad59a95b..6f11aacb9e 100644 --- a/packages/barcode/src/table-renderer.ts +++ b/packages/barcode/src/table-renderer.ts @@ -7,7 +7,7 @@ function shortHex(hex: string): string { hex[3] === hex[4] && hex[5] === hex[6] ) - return '#' + hex[1] + hex[3] + hex[5]; + return `#${hex[1]}${hex[3]}${hex[5]}`; return hex; } From 59398ba17b424adbc6f03ce1454ddb64c2525f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 18:34:48 +0100 Subject: [PATCH 15/17] fix(render): mock fetch in Suspense tests to avoid TLS failures in CI --- packages/render/src/browser/render-web.spec.tsx | 8 ++++---- packages/render/src/node/render-node.spec.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/render/src/browser/render-web.spec.tsx b/packages/render/src/browser/render-web.spec.tsx index f782ba635a..7dd0306952 100644 --- a/packages/render/src/browser/render-web.spec.tsx +++ b/packages/render/src/browser/render-web.spec.tsx @@ -135,6 +135,9 @@ describe('render on the browser environment', () => { }); it('waits for Suspense boundaries to ending before resolving', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response('

Hello

'), + ); const htmlPromise = fetch('https://example.com').then((res) => res.text()); const EmailTemplate = () => { const html = use(htmlPromise); @@ -144,10 +147,7 @@ describe('render on the browser environment', () => { const renderedTemplate = await render(); - expect(renderedTemplate).toMatchInlineSnapshot(` - "" - `); + expect(renderedTemplate).toMatchInlineSnapshot(`"

Hello

"`); }); // See https://github.com/resend/react-email/issues/2263 diff --git a/packages/render/src/node/render-node.spec.tsx b/packages/render/src/node/render-node.spec.tsx index fd9ecb455a..519a7ce99c 100644 --- a/packages/render/src/node/render-node.spec.tsx +++ b/packages/render/src/node/render-node.spec.tsx @@ -95,6 +95,9 @@ describe('render on node environments', () => { }); it('that it properly waits for Suspense boundaries to resolve before resolving', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response('

Hello

'), + ); const htmlPromise = fetch('https://example.com').then((res) => res.text()); const EmailTemplate = () => { const html = use(htmlPromise); @@ -108,10 +111,7 @@ describe('render on node environments', () => { , ); - expect(renderedTemplate).toMatchInlineSnapshot(` - "
Example Domain

Example Domain

This domain is for use in documentation examples without needing permission. Avoid use in operations.

Learn more

-
" - `); + expect(renderedTemplate).toMatchInlineSnapshot(`"

Hello

"`); }); it('converts a React component into HTML', async () => { From 5e24d24e26587bd895ad61a50b6d74ec9dc768ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Sun, 15 Feb 2026 18:37:03 +0100 Subject: [PATCH 16/17] style(render): format Suspense test snapshots --- packages/render/src/browser/render-web.spec.tsx | 4 +++- packages/render/src/node/render-node.spec.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/render/src/browser/render-web.spec.tsx b/packages/render/src/browser/render-web.spec.tsx index 7dd0306952..3500fb1fe2 100644 --- a/packages/render/src/browser/render-web.spec.tsx +++ b/packages/render/src/browser/render-web.spec.tsx @@ -147,7 +147,9 @@ describe('render on the browser environment', () => { const renderedTemplate = await render(); - expect(renderedTemplate).toMatchInlineSnapshot(`"

Hello

"`); + expect(renderedTemplate).toMatchInlineSnapshot( + `"

Hello

"`, + ); }); // See https://github.com/resend/react-email/issues/2263 diff --git a/packages/render/src/node/render-node.spec.tsx b/packages/render/src/node/render-node.spec.tsx index 519a7ce99c..d83e0eaf98 100644 --- a/packages/render/src/node/render-node.spec.tsx +++ b/packages/render/src/node/render-node.spec.tsx @@ -111,7 +111,9 @@ describe('render on node environments', () => { , ); - expect(renderedTemplate).toMatchInlineSnapshot(`"

Hello

"`); + expect(renderedTemplate).toMatchInlineSnapshot( + `"

Hello

"`, + ); }); it('converts a React component into HTML', async () => { From 0d5bdfe001c03c4a68393e99c6e5f6b3abea0c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Wed, 18 Feb 2026 14:27:29 +0100 Subject: [PATCH 17/17] revert(render): remove unrelated Suspense test changes --- packages/render/src/browser/render-web.spec.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/render/src/browser/render-web.spec.tsx b/packages/render/src/browser/render-web.spec.tsx index 3500fb1fe2..f782ba635a 100644 --- a/packages/render/src/browser/render-web.spec.tsx +++ b/packages/render/src/browser/render-web.spec.tsx @@ -135,9 +135,6 @@ describe('render on the browser environment', () => { }); it('waits for Suspense boundaries to ending before resolving', async () => { - vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response('

Hello

'), - ); const htmlPromise = fetch('https://example.com').then((res) => res.text()); const EmailTemplate = () => { const html = use(htmlPromise); @@ -147,9 +144,10 @@ describe('render on the browser environment', () => { const renderedTemplate = await render(); - expect(renderedTemplate).toMatchInlineSnapshot( - `"

Hello

"`, - ); + expect(renderedTemplate).toMatchInlineSnapshot(` + "" + `); }); // See https://github.com/resend/react-email/issues/2263