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/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/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'); + }); +}); diff --git a/packages/barcode/src/barcode.tsx b/packages/barcode/src/barcode.tsx new file mode 100644 index 0000000000..d4f6c80079 --- /dev/null +++ b/packages/barcode/src/barcode.tsx @@ -0,0 +1,122 @@ +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'; diff --git a/packages/barcode/src/compression.ts b/packages/barcode/src/compression.ts new file mode 100644 index 0000000000..d6eb00dce7 --- /dev/null +++ b/packages/barcode/src/compression.ts @@ -0,0 +1,208 @@ +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]; + } +} diff --git a/packages/barcode/src/generate-bwip.ts b/packages/barcode/src/generate-bwip.ts new file mode 100644 index 0000000000..b8efe38da1 --- /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 }; +} diff --git a/packages/barcode/src/generate-qr.ts b/packages/barcode/src/generate-qr.ts new file mode 100644 index 0000000000..f75745e98b --- /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, 'Byte'); + 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 }; +} diff --git a/packages/barcode/src/grid-drawing.ts b/packages/barcode/src/grid-drawing.ts new file mode 100644 index 0000000000..ea955c0401 --- /dev/null +++ b/packages/barcode/src/grid-drawing.ts @@ -0,0 +1,145 @@ +/** + * 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; + let maxX: number; + let minY: number; + let 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; + } +} 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/src/packing.ts b/packages/barcode/src/packing.ts new file mode 100644 index 0000000000..c5f4128d80 --- /dev/null +++ b/packages/barcode/src/packing.ts @@ -0,0 +1,152 @@ +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; +} diff --git a/packages/barcode/src/table-renderer.ts b/packages/barcode/src/table-renderer.ts new file mode 100644 index 0000000000..6f11aacb9e --- /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; +} diff --git a/packages/barcode/src/types.ts b/packages/barcode/src/types.ts new file mode 100644 index 0000000000..4bea2ed50b --- /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; +} 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"] +} diff --git a/packages/render/src/node/render-node.spec.tsx b/packages/render/src/node/render-node.spec.tsx index fd9ecb455a..d83e0eaf98 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,9 @@ 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 () => { 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