diff --git a/packages/examples/vite/src/examples/grid/index.tsx b/packages/examples/vite/src/examples/grid/index.tsx
new file mode 100644
index 000000000..71a8b9fa1
--- /dev/null
+++ b/packages/examples/vite/src/examples/grid/index.tsx
@@ -0,0 +1,226 @@
+import React from 'react';
+import { Document, Page, View, Text, StyleSheet } from '@react-pdf/renderer';
+
+const styles = StyleSheet.create({
+ page: {
+ padding: 40,
+ backgroundColor: '#fafafa',
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#1a1a1a',
+ },
+ subtitle: {
+ fontSize: 9,
+ color: '#888',
+ marginBottom: 20,
+ },
+ card: {
+ backgroundColor: '#ffffff',
+ borderRadius: 5,
+ padding: 16,
+ borderWidth: 1,
+ borderColor: '#e8e8e8',
+ marginBottom: 8,
+ },
+ sectionLabel: {
+ fontSize: 8,
+ color: '#999',
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ marginBottom: 12,
+ },
+
+ // Grid cells
+ cell: {
+ padding: 10,
+ borderRadius: 4,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ cellText: {
+ fontSize: 8,
+ color: 'white',
+ fontWeight: 'bold',
+ },
+ darkText: {
+ fontSize: 8,
+ color: '#1a1a1a',
+ },
+});
+
+const COLORS = [
+ '#3B82F6',
+ '#22C55E',
+ '#A855F7',
+ '#F97316',
+ '#EC4899',
+ '#14B8A6',
+ '#EF4444',
+ '#EAB308',
+ '#6366F1',
+ '#06B6D4',
+ '#F43F5E',
+ '#84CC16',
+];
+
+const Cell = ({
+ color,
+ children,
+ style,
+}: {
+ color: string;
+ children: React.ReactNode;
+ style?: object;
+}) => (
+
+ {children}
+
+);
+
+const Grid = () => (
+
+
+ CSS Grid
+
+ Grid layout with fractional units, spanning, and auto-flow
+
+
+
+ Equal Columns (1fr 1fr 1fr)
+
+ {COLORS.slice(0, 6).map((c, i) => (
+ |
+ {i + 1}
+ |
+ ))}
+
+
+
+
+ Mixed Columns (80px 1fr 2fr)
+
+ Fixed |
+ 1fr |
+ 2fr |
+ Fixed |
+ 1fr |
+ 2fr |
+
+
+
+
+ 4 x 3 Grid
+
+ {COLORS.map((c, i) => (
+
+ {i + 1}
+ |
+ ))}
+
+
+
+
+ Column and Row Spanning
+
+
+ Header (3 cols)
+ |
+
+ Side
+ |
+ A |
+ B |
+ C |
+ D |
+
+
+
+
+ Dashboard Layout
+
+
+ Header
+
+
+ Nav
+
+
+ Main Content
+
+
+ Footer
+
+
+
+
+
+);
+
+export default {
+ id: 'grid',
+ name: 'CSS Grid',
+ description: '',
+ Document: Grid,
+};
diff --git a/packages/examples/vite/src/examples/index.ts b/packages/examples/vite/src/examples/index.ts
index 59188896d..b7a94f998 100644
--- a/packages/examples/vite/src/examples/index.ts
+++ b/packages/examples/vite/src/examples/index.ts
@@ -1,3 +1,4 @@
+import cssGrid from './grid';
import duplicatedImages from './duplicated-images';
import ellipsis from './ellipsis';
import emoji from './emoji';
@@ -25,6 +26,7 @@ import passwordProtection from './password-protection';
import softHyphens from './soft-hyphens';
const EXAMPLES = [
+ cssGrid,
duplicatedImages,
ellipsis,
emoji,
diff --git a/packages/layout/globals.d.ts b/packages/layout/globals.d.ts
deleted file mode 100644
index 931254228..000000000
--- a/packages/layout/globals.d.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-declare module 'yoga-layout/load' {
- export enum Align {
- Auto = 0,
- FlexStart = 1,
- Center = 2,
- FlexEnd = 3,
- Stretch = 4,
- Baseline = 5,
- SpaceBetween = 6,
- SpaceAround = 7,
- SpaceEvenly = 8,
- }
-
- export enum BoxSizing {
- BorderBox = 0,
- ContentBox = 1,
- }
-
- export enum Dimension {
- Width = 0,
- Height = 1,
- }
-
- export enum Direction {
- Inherit = 0,
- LTR = 1,
- RTL = 2,
- }
-
- export enum Display {
- Flex = 0,
- None = 1,
- Contents = 2,
- }
-
- export enum Edge {
- Left = 0,
- Top = 1,
- Right = 2,
- Bottom = 3,
- Start = 4,
- End = 5,
- Horizontal = 6,
- Vertical = 7,
- All = 8,
- }
-
- export enum Errata {
- None = 0,
- StretchFlexBasis = 1,
- AbsolutePositionWithoutInsetsExcludesPadding = 2,
- AbsolutePercentAgainstInnerSize = 4,
- All = 2147483647,
- Classic = 2147483646,
- }
-
- export enum ExperimentalFeature {
- WebFlexBasis = 0,
- }
-
- export enum FlexDirection {
- Column = 0,
- ColumnReverse = 1,
- Row = 2,
- RowReverse = 3,
- }
-
- export enum Gutter {
- Column = 0,
- Row = 1,
- All = 2,
- }
-
- export enum Justify {
- FlexStart = 0,
- Center = 1,
- FlexEnd = 2,
- SpaceBetween = 3,
- SpaceAround = 4,
- SpaceEvenly = 5,
- }
-
- export enum LogLevel {
- Error = 0,
- Warn = 1,
- Info = 2,
- Debug = 3,
- Verbose = 4,
- Fatal = 5,
- }
-
- export enum MeasureMode {
- Undefined = 0,
- Exactly = 1,
- AtMost = 2,
- }
-
- export enum NodeType {
- Default = 0,
- Text = 1,
- }
-
- export enum Overflow {
- Visible = 0,
- Hidden = 1,
- Scroll = 2,
- }
-
- export enum PositionType {
- Static = 0,
- Relative = 1,
- Absolute = 2,
- }
-
- export enum Unit {
- Undefined = 0,
- Point = 1,
- Percent = 2,
- Auto = 3,
- }
-
- export enum Wrap {
- NoWrap = 0,
- Wrap = 1,
- WrapReverse = 2,
- }
-
- export type MeasureFunction = (
- width: number,
- widthMeasureMode: MeasureMode,
- height: number,
- heightMeasureMode: MeasureMode,
- ) => {
- width?: number | undefined;
- height?: number | undefined;
- } | null;
-
- export interface YogaNode {
- calculateLayout(
- width?: number,
- height?: number,
- direction?: Direction,
- ): void;
- copyStyle(node: YogaNode): void;
- free(): void;
- freeRecursive(): void;
- getAlignContent(): Align;
- getAlignItems(): Align;
- getAlignSelf(): Align;
- getAspectRatio(): number;
- getBorder(edge: Edge): number;
- getChild(index: number): YogaNode;
- getChildCount(): number;
- getComputedBorder(edge: Edge): number;
- getComputedBottom(): number;
- getComputedHeight(): number;
- // getComputedLayout(): Layout;
- getComputedLeft(): number;
- getComputedMargin(edge: Edge): number;
- getComputedPadding(edge: Edge): number;
- getComputedRight(): number;
- getComputedTop(): number;
- getComputedWidth(): number;
- getDisplay(): Display;
- getFlexBasis(): number;
- getFlexDirection(): FlexDirection;
- getFlexGrow(): number;
- getFlexShrink(): number;
- getFlexWrap(): Wrap;
- getHeight(): Value;
- getJustifyContent(): Justify;
- getOverflow(): Overflow;
- getParent(): YogaNode | null;
- getPositionType(): PositionType;
- insertChild(child: YogaNode, index: number): void;
- isDirty(): boolean;
- markDirty(): void;
- removeChild(child: YogaNode): void;
- reset(): void;
- setAlignContent(alignContent: Align): void;
- setAlignItems(alignItems: Align): void;
- setAlignSelf(alignSelf: Align): void;
- setAspectRatio(aspectRatio: number): void;
- setBorder(edge: Edge, borderWidth: number): void;
- setDisplay(display: Display): void;
- setFlex(flex: number): void;
- setFlexBasis(flexBasis: number | string): void;
- setFlexBasisPercent(flexBasis: number): void;
- setFlexDirection(flexDirection: Direction): void;
- setFlexGrow(flexGrow: number): void;
- setFlexShrink(flexShrink: number): void;
- setFlexWrap(flexWrap: Wrap): void;
- setHeight(height: number | string): void;
- setHeightAuto(): void;
- setHeightPercent(height: number): void;
- setJustifyContent(justifyContent: Justify): void;
- setMargin(edge: Edge, margin: number | string): void;
- setMarginAuto(edge: Edge): void;
- setMarginPercent(edge: Edge, margin: number): void;
- setMaxHeight(maxHeight: number | string): void;
- setMaxHeightPercent(maxHeight: number): void;
- setMaxWidth(maxWidth: number | string): void;
- setMaxWidthPercent(maxWidth: number): void;
- setGap(gap: Gutter, value: number): void;
- setGapPercent(gap: Gutter, value: number): void;
- setMeasureFunc(measureFunction: MeasureFunction): void;
- setMinHeight(minHeight: number | string): void;
- setMinHeightPercent(minHeight: number): void;
- setMinWidth(minWidth: number | string): void;
- setMinWidthPercent(minWidth: number): void;
- setOverflow(overflow: Overflow): void;
- setPadding(edge: Edge, padding: number | string): void;
- setPaddingPercent(edge: Edge, padding: number): void;
- setPosition(edge: Edge, position: number | string): void;
- setPositionPercent(edge: Edge, position: number): void;
- setPositionType(positionType: PositionType): void;
- setWidth(width: number | string): void;
- setWidthAuto(): void;
- setWidthPercent(width: number): void;
- unsetMeasureFunc(): void;
- }
-
- interface YogaConfig {
- setPointScaleFactor(factor: number): void;
- }
-
- interface ConfigStatic {
- create(): YogaConfig;
- destroy(config: YogaConfig): any;
- }
-
- interface NodeStatic {
- create(): YogaNode;
- createDefault(): YogaNode;
- createWithConfig(config: YogaConfig): YogaNode;
- destroy(node: YogaNode): any;
- }
-
- export interface Yoga {
- Node: NodeStatic;
- Config: ConfigStatic;
- getInstanceCount(): number;
- }
-
- export const loadYoga: () => Promise;
-}
diff --git a/packages/layout/package.json b/packages/layout/package.json
index 623ba7a5d..951741d33 100644
--- a/packages/layout/package.json
+++ b/packages/layout/package.json
@@ -28,7 +28,7 @@
"@react-pdf/types": "^2.10.0",
"emoji-regex-xs": "^1.0.0",
"queue": "^6.0.1",
- "yoga-layout": "^3.2.1"
+ "taffy-layout": "^2.0.3"
},
"files": [
"lib"
diff --git a/packages/layout/rollup.config.js b/packages/layout/rollup.config.js
index 8cec24101..6f678a839 100644
--- a/packages/layout/rollup.config.js
+++ b/packages/layout/rollup.config.js
@@ -1,15 +1,83 @@
+import { readFileSync } from 'fs';
+import { resolve, dirname } from 'path';
+import { fileURLToPath } from 'url';
import typescript from '@rollup/plugin-typescript';
import { dts } from 'rollup-plugin-dts';
import del from 'rollup-plugin-delete';
import pkg from './package.json' with { type: 'json' };
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Rollup plugin that transforms loadTaffy() calls into inline initSync()
+ * with the WASM binary embedded as base64. This ensures the built output
+ * works in browsers without needing to fetch the .wasm file at runtime.
+ */
+function taffyWasmInlinePlugin() {
+ let wasmBase64;
+
+ function getWasmBase64() {
+ if (wasmBase64) return wasmBase64;
+ // Walk up to find hoisted node_modules (yarn workspaces)
+ let dir = __dirname;
+ while (dir !== '/') {
+ const candidate = resolve(
+ dir,
+ 'node_modules/taffy-layout/pkg/taffy_wasm_bg.wasm',
+ );
+ try {
+ wasmBase64 = readFileSync(candidate).toString('base64');
+ return wasmBase64;
+ } catch {
+ // noop
+ }
+ dir = dirname(dir);
+ }
+ throw new Error('Could not find taffy_wasm_bg.wasm');
+ }
+
+ return {
+ name: 'taffy-wasm-inline',
+ resolveId(id) {
+ // Intercept the initTaffy module to provide our own implementation
+ if (id.endsWith('/yoga/initTaffy') || id.endsWith('/yoga/initTaffy.ts')) {
+ return id;
+ }
+ return null;
+ },
+ load(id) {
+ if (!id.endsWith('/yoga/initTaffy') && !id.endsWith('/yoga/initTaffy.ts'))
+ return null;
+
+ // Replace initTaffy module with inlined WASM version
+ const base64 = getWasmBase64();
+ return `
+import { initSync } from 'taffy-layout';
+
+let initialized = false;
+
+export const initTaffy = async () => {
+ if (initialized) return;
+ const binaryStr = atob("${base64}");
+ const bytes = new Uint8Array(binaryStr.length);
+ for (let i = 0; i < binaryStr.length; i++) {
+ bytes[i] = binaryStr.charCodeAt(i);
+ }
+ initSync({ module: bytes });
+ initialized = true;
+};
+`;
+ },
+ };
+}
+
const config = [
{
input: 'src/index.ts',
output: { format: 'es', dir: 'lib' },
external: Object.keys(pkg.dependencies).concat(/@react-pdf/),
- plugins: [typescript(), del({ targets: 'lib' })],
+ plugins: [taffyWasmInlinePlugin(), typescript(), del({ targets: 'lib' })],
},
{
input: './lib/types/index.d.ts',
diff --git a/packages/layout/src/canvas/measureCanvas.ts b/packages/layout/src/canvas/measureCanvas.ts
index 57b192a04..9a3be62f7 100644
--- a/packages/layout/src/canvas/measureCanvas.ts
+++ b/packages/layout/src/canvas/measureCanvas.ts
@@ -1,4 +1,4 @@
-import { MeasureFunction } from 'yoga-layout/load';
+import { MeasureFunction } from '../yoga/enums';
import getMargin from '../node/getMargin';
import getPadding from '../node/getPadding';
import isHeightAuto from '../page/isHeightAuto';
diff --git a/packages/layout/src/image/measureImage.ts b/packages/layout/src/image/measureImage.ts
index 17e8d7c92..f4581d940 100644
--- a/packages/layout/src/image/measureImage.ts
+++ b/packages/layout/src/image/measureImage.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import getRatio from './getRatio';
import getMargin from '../node/getMargin';
diff --git a/packages/layout/src/node/getBorderWidth.ts b/packages/layout/src/node/getBorderWidth.ts
index f45836e7a..abd047426 100644
--- a/packages/layout/src/node/getBorderWidth.ts
+++ b/packages/layout/src/node/getBorderWidth.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/getMargin.ts b/packages/layout/src/node/getMargin.ts
index 7e615df45..dc36bbbb3 100644
--- a/packages/layout/src/node/getMargin.ts
+++ b/packages/layout/src/node/getMargin.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/getPadding.ts b/packages/layout/src/node/getPadding.ts
index 221954375..8da17ace8 100644
--- a/packages/layout/src/node/getPadding.ts
+++ b/packages/layout/src/node/getPadding.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setAlign.ts b/packages/layout/src/node/setAlign.ts
index 03b768659..85fd54149 100644
--- a/packages/layout/src/node/setAlign.ts
+++ b/packages/layout/src/node/setAlign.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { upperFirst } from '@react-pdf/fns';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setBorderWidth.ts b/packages/layout/src/node/setBorderWidth.ts
index 603a6a9e7..8d5939f9e 100644
--- a/packages/layout/src/node/setBorderWidth.ts
+++ b/packages/layout/src/node/setBorderWidth.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import setYogaValue from './setYogaValue';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setDisplay.ts b/packages/layout/src/node/setDisplay.ts
index 08f873007..f2e7984b4 100644
--- a/packages/layout/src/node/setDisplay.ts
+++ b/packages/layout/src/node/setDisplay.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
/**
@@ -7,13 +7,17 @@ import { SafeNode } from '../types';
* @param value - Display
* @returns Node instance wrapper
*/
+const DISPLAY_MAP: Record = {
+ flex: Yoga.Display.Flex,
+ grid: Yoga.Display.Grid,
+ none: Yoga.Display.None,
+};
+
const setDisplay = (value?: string | null) => (node: SafeNode) => {
const { yogaNode } = node;
if (yogaNode) {
- yogaNode.setDisplay(
- value === 'none' ? Yoga.Display.None : Yoga.Display.Flex,
- );
+ yogaNode.setDisplay(DISPLAY_MAP[value || 'flex'] ?? Yoga.Display.Flex);
}
return node;
diff --git a/packages/layout/src/node/setFlexDirection.ts b/packages/layout/src/node/setFlexDirection.ts
index f572d0dc2..2b16c77d3 100644
--- a/packages/layout/src/node/setFlexDirection.ts
+++ b/packages/layout/src/node/setFlexDirection.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setFlexWrap.ts b/packages/layout/src/node/setFlexWrap.ts
index 23380652f..22a5fec1b 100644
--- a/packages/layout/src/node/setFlexWrap.ts
+++ b/packages/layout/src/node/setFlexWrap.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
const FLEX_WRAP = {
diff --git a/packages/layout/src/node/setGap.ts b/packages/layout/src/node/setGap.ts
index c3a195b44..ef7c2d1c2 100644
--- a/packages/layout/src/node/setGap.ts
+++ b/packages/layout/src/node/setGap.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import setYogaValue from './setYogaValue';
diff --git a/packages/layout/src/node/setGrid.ts b/packages/layout/src/node/setGrid.ts
new file mode 100644
index 000000000..f13597869
--- /dev/null
+++ b/packages/layout/src/node/setGrid.ts
@@ -0,0 +1,237 @@
+import {
+ GridAutoFlow as TaffyGridAutoFlow,
+ type GridPlacement as TaffyGridPlacement,
+ type TrackSizingFunction,
+ type GridTemplateComponent,
+} from 'taffy-layout';
+
+import { SafeNode } from '../types';
+
+/**
+ * Parse a single track sizing value into a TrackSizingFunction.
+ *
+ * Supports: px numbers, percentages, fr units, auto, min-content, max-content
+ */
+const parseTrackSize = (token: string): TrackSizingFunction => {
+ const trimmed = token.trim();
+
+ if (trimmed === 'auto') return { min: 'auto', max: 'auto' };
+ if (trimmed === 'min-content')
+ return { min: 'min-content', max: 'min-content' };
+ if (trimmed === 'max-content')
+ return { min: 'max-content', max: 'max-content' };
+
+ if (trimmed.endsWith('fr')) {
+ const value = Number(trimmed.slice(0, -2));
+ return { min: 'auto', max: `${value}fr` };
+ }
+
+ if (trimmed.endsWith('%')) {
+ const value = trimmed as `${number}%`;
+ return { min: value, max: value };
+ }
+
+ const num = Number(trimmed);
+ if (!Number.isNaN(num)) return { min: num, max: num };
+
+ return { min: 'auto', max: 'auto' };
+};
+
+/**
+ * Parse a CSS grid-template-columns/rows string into Taffy track components.
+ *
+ * Supports: "1fr 2fr 1fr", "100 200 100", "repeat(3, 1fr)", "minmax(100, 1fr)"
+ */
+const parseGridTemplate = (value: string): GridTemplateComponent[] => {
+ const result: GridTemplateComponent[] = [];
+
+ // Handle repeat() syntax
+ const repeatRegex = /repeat\(\s*(\d+|auto-fill|auto-fit)\s*,\s*(.+?)\s*\)/g;
+ let processed = value;
+ let match;
+
+ while ((match = repeatRegex.exec(value)) !== null) {
+ const countStr = match[1];
+ const tracksStr = match[2];
+
+ const count =
+ countStr === 'auto-fill' || countStr === 'auto-fit'
+ ? countStr
+ : Number(countStr);
+
+ const tracks = tracksStr.split(/\s+/).map(parseTrackSize);
+
+ result.push({ count, tracks });
+ processed = processed.replace(match[0], '');
+ }
+
+ // Parse remaining tokens
+ const remaining = processed.trim();
+ if (remaining) {
+ // Handle minmax() tokens
+ const tokens: string[] = [];
+ let depth = 0;
+ let current = '';
+ for (const char of remaining) {
+ if (char === '(') depth++;
+ if (char === ')') depth--;
+ if (char === ' ' && depth === 0 && current.trim()) {
+ tokens.push(current.trim());
+ current = '';
+ } else {
+ current += char;
+ }
+ }
+ if (current.trim()) tokens.push(current.trim());
+
+ for (const token of tokens) {
+ const minmaxMatch = token.match(/^minmax\(\s*(.+?)\s*,\s*(.+?)\s*\)$/);
+ if (minmaxMatch) {
+ const minTrack = parseTrackSize(minmaxMatch[1]);
+ const maxTrack = parseTrackSize(minmaxMatch[2]);
+ result.push({ min: minTrack.min, max: maxTrack.max });
+ } else {
+ result.push(parseTrackSize(token));
+ }
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Parse a CSS grid placement value (grid-column-start, grid-row-end, etc.)
+ */
+const parseGridPlacement = (value: string | number): TaffyGridPlacement => {
+ if (value === 'auto') return 'auto';
+ if (typeof value === 'number') return value;
+
+ const str = String(value).trim();
+
+ if (str === 'auto') return 'auto';
+
+ const spanMatch = str.match(/^span\s+(\d+)$/);
+ if (spanMatch) return { span: Number(spanMatch[1]) };
+
+ const num = Number(str);
+ if (!Number.isNaN(num)) return num;
+
+ return 'auto';
+};
+
+/**
+ * Parse grid-column or grid-row shorthand: "1 / 3", "1 / span 2", "span 2"
+ */
+const parseGridLineShorthand = (
+ value: string,
+): { start: TaffyGridPlacement; end: TaffyGridPlacement } => {
+ const parts = value.split('/').map((s) => s.trim());
+ if (parts.length === 2) {
+ return {
+ start: parseGridPlacement(parts[0]),
+ end: parseGridPlacement(parts[1]),
+ };
+ }
+ return {
+ start: parseGridPlacement(parts[0]),
+ end: 'auto',
+ };
+};
+
+const GRID_AUTO_FLOW_MAP: Record = {
+ row: TaffyGridAutoFlow.Row,
+ column: TaffyGridAutoFlow.Column,
+ 'row dense': TaffyGridAutoFlow.RowDense,
+ 'column dense': TaffyGridAutoFlow.ColumnDense,
+};
+
+// --- Node setters ---
+
+export const setGridTemplateColumns =
+ (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ node.yogaNode.setGridTemplateColumns(parseGridTemplate(value));
+ }
+ return node;
+ };
+
+export const setGridTemplateRows =
+ (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ node.yogaNode.setGridTemplateRows(parseGridTemplate(value));
+ }
+ return node;
+ };
+
+export const setGridAutoColumns =
+ (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ node.yogaNode.setGridAutoColumns(value.split(/\s+/).map(parseTrackSize));
+ }
+ return node;
+ };
+
+export const setGridAutoRows = (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ node.yogaNode.setGridAutoRows(value.split(/\s+/).map(parseTrackSize));
+ }
+ return node;
+};
+
+export const setGridAutoFlow = (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ const mapped = GRID_AUTO_FLOW_MAP[value];
+ if (mapped != null) node.yogaNode.setGridAutoFlow(mapped);
+ }
+ return node;
+};
+
+export const setGridColumn = (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ const { start, end } = parseGridLineShorthand(value);
+ node.yogaNode.setGridColumnStart(start);
+ node.yogaNode.setGridColumnEnd(end);
+ }
+ return node;
+};
+
+export const setGridRow = (value?: string | null) => (node: SafeNode) => {
+ if (value && node.yogaNode) {
+ const { start, end } = parseGridLineShorthand(value);
+ node.yogaNode.setGridRowStart(start);
+ node.yogaNode.setGridRowEnd(end);
+ }
+ return node;
+};
+
+export const setGridColumnStart =
+ (value?: string | number | null) => (node: SafeNode) => {
+ if (value != null && node.yogaNode) {
+ node.yogaNode.setGridColumnStart(parseGridPlacement(value));
+ }
+ return node;
+ };
+
+export const setGridColumnEnd =
+ (value?: string | number | null) => (node: SafeNode) => {
+ if (value != null && node.yogaNode) {
+ node.yogaNode.setGridColumnEnd(parseGridPlacement(value));
+ }
+ return node;
+ };
+
+export const setGridRowStart =
+ (value?: string | number | null) => (node: SafeNode) => {
+ if (value != null && node.yogaNode) {
+ node.yogaNode.setGridRowStart(parseGridPlacement(value));
+ }
+ return node;
+ };
+
+export const setGridRowEnd =
+ (value?: string | number | null) => (node: SafeNode) => {
+ if (value != null && node.yogaNode) {
+ node.yogaNode.setGridRowEnd(parseGridPlacement(value));
+ }
+ return node;
+ };
diff --git a/packages/layout/src/node/setJustifyContent.ts b/packages/layout/src/node/setJustifyContent.ts
index 0aba1b903..44f8d3519 100644
--- a/packages/layout/src/node/setJustifyContent.ts
+++ b/packages/layout/src/node/setJustifyContent.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { isNil } from '@react-pdf/fns';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setMargin.ts b/packages/layout/src/node/setMargin.ts
index 9a2b0dea9..28b4adb2e 100644
--- a/packages/layout/src/node/setMargin.ts
+++ b/packages/layout/src/node/setMargin.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import setYogaValue from './setYogaValue';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setOverflow.ts b/packages/layout/src/node/setOverflow.ts
index ed74d5e6c..ee6fdadf2 100644
--- a/packages/layout/src/node/setOverflow.ts
+++ b/packages/layout/src/node/setOverflow.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { isNil } from '@react-pdf/fns';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setPadding.ts b/packages/layout/src/node/setPadding.ts
index 55d3cf79d..aa12f0993 100644
--- a/packages/layout/src/node/setPadding.ts
+++ b/packages/layout/src/node/setPadding.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import setYogaValue from './setYogaValue';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setPosition.ts b/packages/layout/src/node/setPosition.ts
index cf41dcfde..99070a9b6 100644
--- a/packages/layout/src/node/setPosition.ts
+++ b/packages/layout/src/node/setPosition.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import setYogaValue from './setYogaValue';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setPositionType.ts b/packages/layout/src/node/setPositionType.ts
index 3230eb1b6..2a7643450 100644
--- a/packages/layout/src/node/setPositionType.ts
+++ b/packages/layout/src/node/setPositionType.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { isNil } from '@react-pdf/fns';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/node/setYogaValue.ts b/packages/layout/src/node/setYogaValue.ts
index 3db301755..fc8a8c006 100644
--- a/packages/layout/src/node/setYogaValue.ts
+++ b/packages/layout/src/node/setYogaValue.ts
@@ -1,5 +1,5 @@
import { isNil, upperFirst, matchPercent } from '@react-pdf/fns';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafeNode } from '../types';
diff --git a/packages/layout/src/steps/resolveDimensions.ts b/packages/layout/src/steps/resolveDimensions.ts
index 3db12abb4..a41ae2a34 100644
--- a/packages/layout/src/steps/resolveDimensions.ts
+++ b/packages/layout/src/steps/resolveDimensions.ts
@@ -20,6 +20,19 @@ import setAlignContent from '../node/setAlignContent';
import setPositionType from '../node/setPositionType';
import setFlexDirection from '../node/setFlexDirection';
import setJustifyContent from '../node/setJustifyContent';
+import {
+ setGridTemplateColumns,
+ setGridTemplateRows,
+ setGridAutoColumns,
+ setGridAutoRows,
+ setGridAutoFlow,
+ setGridColumn,
+ setGridRow,
+ setGridColumnStart,
+ setGridColumnEnd,
+ setGridRowStart,
+ setGridRowEnd,
+} from '../node/setGrid';
import {
setMarginTop,
setMarginRight,
@@ -124,6 +137,17 @@ const setYogaValues = (node: SafeNode) => {
setFlexShrink(node.style.flexShrink),
setRowGap(node.style.rowGap),
setColumnGap(node.style.columnGap),
+ setGridTemplateColumns(node.style.gridTemplateColumns),
+ setGridTemplateRows(node.style.gridTemplateRows),
+ setGridAutoColumns(node.style.gridAutoColumns),
+ setGridAutoRows(node.style.gridAutoRows),
+ setGridAutoFlow(node.style.gridAutoFlow),
+ setGridColumn(node.style.gridColumn),
+ setGridRow(node.style.gridRow),
+ setGridColumnStart(node.style.gridColumnStart),
+ setGridColumnEnd(node.style.gridColumnEnd),
+ setGridRowStart(node.style.gridRowStart),
+ setGridRowEnd(node.style.gridRowEnd),
)(node);
};
diff --git a/packages/layout/src/svg/measureSvg.ts b/packages/layout/src/svg/measureSvg.ts
index 4c9794588..aec6d1fd3 100644
--- a/packages/layout/src/svg/measureSvg.ts
+++ b/packages/layout/src/svg/measureSvg.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import { SafePageNode, SafeSvgNode, Viewbox } from '../types';
diff --git a/packages/layout/src/text/measureText.ts b/packages/layout/src/text/measureText.ts
index ec808c9cf..7892e722a 100644
--- a/packages/layout/src/text/measureText.ts
+++ b/packages/layout/src/text/measureText.ts
@@ -1,4 +1,4 @@
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../yoga/enums';
import FontStore from '@react-pdf/font';
import layoutText from './layoutText';
diff --git a/packages/layout/src/types/base.ts b/packages/layout/src/types/base.ts
index 3db5a2df1..a1a0533cf 100644
--- a/packages/layout/src/types/base.ts
+++ b/packages/layout/src/types/base.ts
@@ -1,10 +1,12 @@
import { Transform } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { TaffyNodeAdapter } from '../yoga/index';
import * as React from 'react';
import { SafeClipPathNode } from './clip-path';
import { SafeLinearGradientNode } from './linear-gradient';
import { SafeRadialGradientNode } from './radial-gradient';
+export type YogaNode = TaffyNodeAdapter;
+
export type YogaInstance = {
node: { create: () => YogaNode };
};
diff --git a/packages/layout/src/types/canvas.ts b/packages/layout/src/types/canvas.ts
index 1c02e849d..cc9b2601d 100644
--- a/packages/layout/src/types/canvas.ts
+++ b/packages/layout/src/types/canvas.ts
@@ -1,6 +1,6 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, NodeProps, Origin } from './base';
diff --git a/packages/layout/src/types/checkbox.ts b/packages/layout/src/types/checkbox.ts
index 2e1598719..4f0f36d7f 100644
--- a/packages/layout/src/types/checkbox.ts
+++ b/packages/layout/src/types/checkbox.ts
@@ -1,6 +1,6 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, FormCommonProps, Origin } from './base';
diff --git a/packages/layout/src/types/field-set.ts b/packages/layout/src/types/field-set.ts
index 8f62e18da..16b7a1961 100644
--- a/packages/layout/src/types/field-set.ts
+++ b/packages/layout/src/types/field-set.ts
@@ -1,6 +1,6 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, NodeProps, Origin } from './base';
import { SafeTextNode, TextNode } from './text';
diff --git a/packages/layout/src/types/image-background.ts b/packages/layout/src/types/image-background.ts
index d50c230ff..3c5c3012d 100644
--- a/packages/layout/src/types/image-background.ts
+++ b/packages/layout/src/types/image-background.ts
@@ -1,7 +1,7 @@
import { SafeStyle, Style } from '@react-pdf/stylesheet';
import { SrcSet, Sizes } from '@react-pdf/types';
import * as P from '@react-pdf/primitives';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, NodeProps, Origin } from './base';
import { Image } from '@react-pdf/image';
diff --git a/packages/layout/src/types/image.ts b/packages/layout/src/types/image.ts
index 029eedcfd..18e4238af 100644
--- a/packages/layout/src/types/image.ts
+++ b/packages/layout/src/types/image.ts
@@ -1,7 +1,7 @@
import { SafeStyle, Style } from '@react-pdf/stylesheet';
import { SrcSet, Sizes } from '@react-pdf/types';
import * as P from '@react-pdf/primitives';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, NodeProps, Origin } from './base';
import { Image } from '@react-pdf/image';
diff --git a/packages/layout/src/types/link.ts b/packages/layout/src/types/link.ts
index 728443299..9519fa617 100644
--- a/packages/layout/src/types/link.ts
+++ b/packages/layout/src/types/link.ts
@@ -1,7 +1,7 @@
import { SafeStyle, Style } from '@react-pdf/stylesheet';
import * as P from '@react-pdf/primitives';
import { HitSlop } from '@react-pdf/types';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, NodeProps, Origin, RenderProp } from './base';
import { ImageNode, SafeImageNode } from './image';
diff --git a/packages/layout/src/types/page.ts b/packages/layout/src/types/page.ts
index a2b9da7d2..9437d6069 100644
--- a/packages/layout/src/types/page.ts
+++ b/packages/layout/src/types/page.ts
@@ -1,6 +1,6 @@
import { SafeStyle, Style } from '@react-pdf/stylesheet';
import * as P from '@react-pdf/primitives';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import type { Box, NodeProps, Origin } from './base';
import { ImageNode, SafeImageNode } from './image';
diff --git a/packages/layout/src/types/select.ts b/packages/layout/src/types/select.ts
index 1b6ca1efc..9da7b384a 100644
--- a/packages/layout/src/types/select.ts
+++ b/packages/layout/src/types/select.ts
@@ -1,6 +1,6 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, FormCommonProps, Origin } from './base';
diff --git a/packages/layout/src/types/svg.ts b/packages/layout/src/types/svg.ts
index acbcd1ea1..307179bda 100644
--- a/packages/layout/src/types/svg.ts
+++ b/packages/layout/src/types/svg.ts
@@ -1,6 +1,6 @@
import { SafeStyle, Style } from '@react-pdf/stylesheet';
import * as P from '@react-pdf/primitives';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import {
Box,
diff --git a/packages/layout/src/types/text-input.ts b/packages/layout/src/types/text-input.ts
index b3b33983b..3025b11a1 100644
--- a/packages/layout/src/types/text-input.ts
+++ b/packages/layout/src/types/text-input.ts
@@ -1,6 +1,6 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, FormCommonProps, Origin } from './base';
diff --git a/packages/layout/src/types/text.ts b/packages/layout/src/types/text.ts
index 6f2256e0d..a08361d7a 100644
--- a/packages/layout/src/types/text.ts
+++ b/packages/layout/src/types/text.ts
@@ -1,7 +1,7 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
import { HyphenationCallback } from '@react-pdf/font';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Paragraph } from '@react-pdf/textkit';
import { Box, NodeProps, Origin, RenderProp } from './base';
diff --git a/packages/layout/src/types/view.ts b/packages/layout/src/types/view.ts
index 92ac8e3e8..5cd124356 100644
--- a/packages/layout/src/types/view.ts
+++ b/packages/layout/src/types/view.ts
@@ -1,6 +1,6 @@
import * as P from '@react-pdf/primitives';
import { SafeStyle, Style } from '@react-pdf/stylesheet';
-import { YogaNode } from 'yoga-layout/load';
+import { YogaNode } from './base';
import { Box, NodeProps, Origin, RenderProp } from './base';
import { ImageNode, SafeImageNode } from './image';
diff --git a/packages/layout/src/yoga/enums.ts b/packages/layout/src/yoga/enums.ts
new file mode 100644
index 000000000..7ca2f726b
--- /dev/null
+++ b/packages/layout/src/yoga/enums.ts
@@ -0,0 +1,89 @@
+/**
+ * Yoga-compatible enum definitions backed by Taffy values.
+ * These provide the same interface that setter/getter files expect from 'yoga-layout/load'.
+ */
+
+export enum Edge {
+ Left = 0,
+ Top = 1,
+ Right = 2,
+ Bottom = 3,
+ Start = 4,
+ End = 5,
+ Horizontal = 6,
+ Vertical = 7,
+ All = 8,
+}
+
+export enum Align {
+ Auto = 0,
+ FlexStart = 1,
+ Center = 2,
+ FlexEnd = 3,
+ Stretch = 4,
+ Baseline = 5,
+ SpaceBetween = 6,
+ SpaceAround = 7,
+ SpaceEvenly = 8,
+}
+
+export enum FlexDirection {
+ Column = 0,
+ ColumnReverse = 1,
+ Row = 2,
+ RowReverse = 3,
+}
+
+export enum Display {
+ Flex = 0,
+ None = 1,
+ Grid = 2,
+}
+
+export enum Justify {
+ FlexStart = 0,
+ Center = 1,
+ FlexEnd = 2,
+ SpaceBetween = 3,
+ SpaceAround = 4,
+ SpaceEvenly = 5,
+}
+
+export enum Overflow {
+ Visible = 0,
+ Hidden = 1,
+ Scroll = 2,
+}
+
+export enum PositionType {
+ Static = 0,
+ Relative = 1,
+ Absolute = 2,
+}
+
+export enum Wrap {
+ NoWrap = 0,
+ Wrap = 1,
+ WrapReverse = 2,
+}
+
+export enum Gutter {
+ Column = 0,
+ Row = 1,
+ All = 2,
+}
+
+export enum MeasureMode {
+ Undefined = 0,
+ Exactly = 1,
+ AtMost = 2,
+}
+
+export type MeasureFunction = (
+ width: number,
+ widthMode: MeasureMode,
+ height: number,
+ heightMode: MeasureMode,
+) => { width?: number; height?: number } | null;
+
+export type { TaffyNodeAdapter as YogaNode } from './index';
diff --git a/packages/layout/src/yoga/index.ts b/packages/layout/src/yoga/index.ts
index 40099b67d..011e8801e 100644
--- a/packages/layout/src/yoga/index.ts
+++ b/packages/layout/src/yoga/index.ts
@@ -1,17 +1,615 @@
-import { type Yoga, loadYoga as yogaLoadYoga } from 'yoga-layout/load';
+/* eslint-disable max-classes-per-file */
-let instancePromise: Promise;
+import {
+ TaffyTree,
+ Style,
+ Display as TaffyDisplay,
+ FlexDirection as TaffyFlexDirection,
+ FlexWrap as TaffyFlexWrap,
+ AlignItems as TaffyAlignItems,
+ AlignSelf as TaffyAlignSelf,
+ AlignContent as TaffyAlignContent,
+ JustifyContent as TaffyJustifyContent,
+ Overflow as TaffyOverflow,
+ Position as TaffyPosition,
+ GridAutoFlow as TaffyGridAutoFlow,
+ type GridPlacement as TaffyGridPlacement,
+ type TrackSizingFunction,
+ type GridTemplateComponent,
+ type MeasureFunction as TaffyMeasureFunction,
+ type AvailableSpace,
+ type Size,
+} from 'taffy-layout';
-export const loadYoga = async () => {
- // Yoga WASM binaries must be asynchronously compiled and loaded
- // to prevent Event emitter memory leak warnings, Yoga must be loaded only once
- const instance = await (instancePromise ??= yogaLoadYoga());
+import { initTaffy } from './initTaffy';
+
+import {
+ Edge,
+ Align,
+ FlexDirection,
+ Display,
+ Justify,
+ Overflow,
+ PositionType,
+ Wrap,
+ Gutter,
+ MeasureMode,
+ type MeasureFunction,
+} from './enums';
+
+// --- Taffy enum mappers ---
+
+const FLEX_DIRECTION_MAP: Record = {
+ [FlexDirection.Column]: TaffyFlexDirection.Column,
+ [FlexDirection.ColumnReverse]: TaffyFlexDirection.ColumnReverse,
+ [FlexDirection.Row]: TaffyFlexDirection.Row,
+ [FlexDirection.RowReverse]: TaffyFlexDirection.RowReverse,
+};
+
+const FLEX_WRAP_MAP: Record = {
+ [Wrap.NoWrap]: TaffyFlexWrap.NoWrap,
+ [Wrap.Wrap]: TaffyFlexWrap.Wrap,
+ [Wrap.WrapReverse]: TaffyFlexWrap.WrapReverse,
+};
+
+const ALIGN_ITEMS_MAP: Record = {
+ [Align.FlexStart]: TaffyAlignItems.FlexStart,
+ [Align.Center]: TaffyAlignItems.Center,
+ [Align.FlexEnd]: TaffyAlignItems.FlexEnd,
+ [Align.Stretch]: TaffyAlignItems.Stretch,
+ [Align.Baseline]: TaffyAlignItems.Baseline,
+};
+
+const ALIGN_SELF_MAP: Record = {
+ [Align.Auto]: TaffyAlignSelf.Auto,
+ [Align.FlexStart]: TaffyAlignSelf.FlexStart,
+ [Align.Center]: TaffyAlignSelf.Center,
+ [Align.FlexEnd]: TaffyAlignSelf.FlexEnd,
+ [Align.Stretch]: TaffyAlignSelf.Stretch,
+ [Align.Baseline]: TaffyAlignSelf.Baseline,
+};
+
+const ALIGN_CONTENT_MAP: Record = {
+ [Align.FlexStart]: TaffyAlignContent.FlexStart,
+ [Align.Center]: TaffyAlignContent.Center,
+ [Align.FlexEnd]: TaffyAlignContent.FlexEnd,
+ [Align.Stretch]: TaffyAlignContent.Stretch,
+ [Align.SpaceBetween]: TaffyAlignContent.SpaceBetween,
+ [Align.SpaceAround]: TaffyAlignContent.SpaceAround,
+ [Align.SpaceEvenly]: TaffyAlignContent.SpaceEvenly,
+};
+
+const JUSTIFY_MAP: Record = {
+ [Justify.FlexStart]: TaffyJustifyContent.FlexStart,
+ [Justify.Center]: TaffyJustifyContent.Center,
+ [Justify.FlexEnd]: TaffyJustifyContent.FlexEnd,
+ [Justify.SpaceBetween]: TaffyJustifyContent.SpaceBetween,
+ [Justify.SpaceAround]: TaffyJustifyContent.SpaceAround,
+ [Justify.SpaceEvenly]: TaffyJustifyContent.SpaceEvenly,
+};
+
+const OVERFLOW_MAP: Record = {
+ [Overflow.Visible]: TaffyOverflow.Visible,
+ [Overflow.Hidden]: TaffyOverflow.Hidden,
+ [Overflow.Scroll]: TaffyOverflow.Scroll,
+};
+
+// --- Dimension/edge helpers ---
+
+const toTaffyPercent = (value: number): `${number}%` => `${value}%`;
+
+type EdgeSetter = (style: Style, value: any) => void;
+
+const EDGE_MARGIN_SETTERS: Record = {
+ [Edge.Top]: (s, v) => {
+ s.marginTop = v;
+ },
+ [Edge.Right]: (s, v) => {
+ s.marginRight = v;
+ },
+ [Edge.Bottom]: (s, v) => {
+ s.marginBottom = v;
+ },
+ [Edge.Left]: (s, v) => {
+ s.marginLeft = v;
+ },
+};
+
+const EDGE_PADDING_SETTERS: Record = {
+ [Edge.Top]: (s, v) => {
+ s.paddingTop = v;
+ },
+ [Edge.Right]: (s, v) => {
+ s.paddingRight = v;
+ },
+ [Edge.Bottom]: (s, v) => {
+ s.paddingBottom = v;
+ },
+ [Edge.Left]: (s, v) => {
+ s.paddingLeft = v;
+ },
+};
+
+const EDGE_BORDER_SETTERS: Record = {
+ [Edge.Top]: (s, v) => {
+ s.borderTop = v;
+ },
+ [Edge.Right]: (s, v) => {
+ s.borderRight = v;
+ },
+ [Edge.Bottom]: (s, v) => {
+ s.borderBottom = v;
+ },
+ [Edge.Left]: (s, v) => {
+ s.borderLeft = v;
+ },
+};
+
+const EDGE_POSITION_SETTERS: Record = {
+ [Edge.Top]: (s, v) => {
+ s.top = v;
+ },
+ [Edge.Right]: (s, v) => {
+ s.right = v;
+ },
+ [Edge.Bottom]: (s, v) => {
+ s.bottom = v;
+ },
+ [Edge.Left]: (s, v) => {
+ s.left = v;
+ },
+};
+
+// --- TaffyNodeAdapter ---
+
+export class TaffyNodeAdapter {
+ private ctx: TaffyContext;
+ nodeId: bigint;
+ style: Style;
+ private _measureFunc: MeasureFunction | null = null;
+
+ constructor(ctx: TaffyContext, nodeId: bigint, style: Style) {
+ this.ctx = ctx;
+ this.nodeId = nodeId;
+ this.style = style;
+ }
+
+ // --- Dimension setters ---
+
+ setWidth(value: number) {
+ this.style.width = value;
+ }
+ setWidthPercent(value: number) {
+ this.style.width = toTaffyPercent(value);
+ }
+ setWidthAuto() {
+ this.style.width = 'auto';
+ }
+
+ setHeight(value: number) {
+ this.style.height = value;
+ }
+ setHeightPercent(value: number) {
+ this.style.height = toTaffyPercent(value);
+ }
+ setHeightAuto() {
+ this.style.height = 'auto';
+ }
+
+ setMinWidth(value: number) {
+ this.style.minWidth = value;
+ }
+ setMinWidthPercent(value: number) {
+ this.style.minWidth = toTaffyPercent(value);
+ }
+
+ setMaxWidth(value: number) {
+ this.style.maxWidth = value;
+ }
+ setMaxWidthPercent(value: number) {
+ this.style.maxWidth = toTaffyPercent(value);
+ }
+
+ setMinHeight(value: number) {
+ this.style.minHeight = value;
+ }
+ setMinHeightPercent(value: number) {
+ this.style.minHeight = toTaffyPercent(value);
+ }
+
+ setMaxHeight(value: number) {
+ this.style.maxHeight = value;
+ }
+ setMaxHeightPercent(value: number) {
+ this.style.maxHeight = toTaffyPercent(value);
+ }
+
+ // --- Edge-based setters (margin, padding, border, position) ---
+
+ private setEdge(setters: Record, edge: Edge, value: any) {
+ const setter = setters[edge];
+ if (setter) {
+ setter(this.style, value);
+ }
+ }
+
+ setMargin(edge: Edge, value: number) {
+ this.setEdge(EDGE_MARGIN_SETTERS, edge, value);
+ }
+ setMarginPercent(edge: Edge, value: number) {
+ this.setEdge(EDGE_MARGIN_SETTERS, edge, toTaffyPercent(value));
+ }
+ setMarginAuto(edge: Edge) {
+ this.setEdge(EDGE_MARGIN_SETTERS, edge, 'auto');
+ }
+
+ setPadding(edge: Edge, value: number) {
+ this.setEdge(EDGE_PADDING_SETTERS, edge, value);
+ }
+ setPaddingPercent(edge: Edge, value: number) {
+ this.setEdge(EDGE_PADDING_SETTERS, edge, toTaffyPercent(value));
+ }
+
+ setBorder(edge: Edge, value: number) {
+ this.setEdge(EDGE_BORDER_SETTERS, edge, value);
+ }
+
+ setPosition(edge: Edge, value: number) {
+ this.setEdge(EDGE_POSITION_SETTERS, edge, value);
+ }
+ setPositionPercent(edge: Edge, value: number) {
+ this.setEdge(EDGE_POSITION_SETTERS, edge, toTaffyPercent(value));
+ }
+
+ // --- Gap setters ---
+
+ setGap(gutter: Gutter, value: number) {
+ if (gutter === Gutter.Row) this.style.rowGap = value;
+ else this.style.columnGap = value;
+ }
+
+ setGapPercent(gutter: Gutter, value: number) {
+ if (gutter === Gutter.Row) this.style.rowGap = toTaffyPercent(value);
+ else this.style.columnGap = toTaffyPercent(value);
+ }
+
+ // --- Flex property setters ---
+
+ setFlexDirection(value: FlexDirection) {
+ this.style.flexDirection =
+ FLEX_DIRECTION_MAP[value] ?? TaffyFlexDirection.Column;
+ }
+
+ setFlexWrap(value: Wrap) {
+ this.style.flexWrap = FLEX_WRAP_MAP[value] ?? TaffyFlexWrap.NoWrap;
+ }
+
+ setFlexGrow(value: number) {
+ this.style.flexGrow = value;
+ }
+ setFlexShrink(value: number) {
+ this.style.flexShrink = value;
+ }
+
+ setFlexBasis(value: number) {
+ this.style.flexBasis = value;
+ }
+ setFlexBasisPercent(value: number) {
+ this.style.flexBasis = toTaffyPercent(value);
+ }
+ setFlexBasisAuto() {
+ this.style.flexBasis = 'auto';
+ }
+
+ // --- Alignment setters ---
- const config = instance.Config.create();
+ setAlignItems(value: Align) {
+ this.style.alignItems = ALIGN_ITEMS_MAP[value];
+ }
- config.setPointScaleFactor(0);
+ setAlignSelf(value: Align) {
+ this.style.alignSelf = ALIGN_SELF_MAP[value];
+ }
+
+ setAlignContent(value: Align) {
+ this.style.alignContent = ALIGN_CONTENT_MAP[value];
+ }
+
+ setJustifyContent(value: Justify) {
+ this.style.justifyContent = JUSTIFY_MAP[value];
+ }
+
+ // --- Display / Overflow / Position ---
+
+ setDisplay(value: Display) {
+ if (value === Display.None) {
+ this.style.display = TaffyDisplay.None;
+ } else if (value === Display.Grid) {
+ this.style.display = TaffyDisplay.Grid;
+ } else {
+ this.style.display = TaffyDisplay.Flex;
+ }
+ }
+
+ setOverflow(value: Overflow) {
+ const mapped = OVERFLOW_MAP[value] ?? TaffyOverflow.Visible;
+ this.style.overflowX = mapped;
+ this.style.overflowY = mapped;
+ }
+
+ setPositionType(value: PositionType) {
+ if (value === PositionType.Absolute) {
+ this.style.position = TaffyPosition.Absolute;
+ } else {
+ // Static and Relative both map to Relative (Taffy has no Static)
+ this.style.position = TaffyPosition.Relative;
+ }
+ }
+
+ // --- Aspect ratio ---
+
+ setAspectRatio(value: number) {
+ this.style.aspectRatio = value;
+ }
+
+ // --- Grid properties ---
+
+ setGridTemplateColumns(tracks: GridTemplateComponent[]) {
+ this.style.gridTemplateColumns = tracks;
+ }
+
+ setGridTemplateRows(tracks: GridTemplateComponent[]) {
+ this.style.gridTemplateRows = tracks;
+ }
+
+ setGridAutoColumns(tracks: TrackSizingFunction[]) {
+ this.style.gridAutoColumns = tracks;
+ }
+
+ setGridAutoRows(tracks: TrackSizingFunction[]) {
+ this.style.gridAutoRows = tracks;
+ }
+
+ setGridAutoFlow(value: TaffyGridAutoFlow) {
+ this.style.gridAutoFlow = value;
+ }
+
+ setGridColumnStart(value: TaffyGridPlacement) {
+ this.style.gridColumnStart = value;
+ }
+
+ setGridColumnEnd(value: TaffyGridPlacement) {
+ this.style.gridColumnEnd = value;
+ }
+
+ setGridRowStart(value: TaffyGridPlacement) {
+ this.style.gridRowStart = value;
+ }
+
+ setGridRowEnd(value: TaffyGridPlacement) {
+ this.style.gridRowEnd = value;
+ }
+
+ // --- Measure function ---
+
+ setMeasureFunc(fn: MeasureFunction) {
+ this._measureFunc = fn;
+ this.ctx.measureFuncs.set(this.nodeId, fn);
+ }
+
+ unsetMeasureFunc() {
+ this._measureFunc = null;
+ this.ctx.measureFuncs.delete(this.nodeId);
+ }
+
+ // --- Tree operations ---
+
+ insertChild(child: TaffyNodeAdapter, index: number) {
+ this.ctx.tree.insertChildAtIndex(this.nodeId, index, child.nodeId);
+ }
+
+ getChildCount(): number {
+ return this.ctx.tree.childCount(this.nodeId);
+ }
+
+ // --- Layout computation ---
+
+ calculateLayout() {
+ // Apply all buffered styles to the tree
+ for (const node of this.ctx.nodes) {
+ this.ctx.tree.setStyle(node.nodeId, node.style);
+ }
+
+ const availableSpace: Size = {
+ width: 'max-content',
+ height: 'max-content',
+ };
+
+ const measureFuncs = this.ctx.measureFuncs;
+
+ if (measureFuncs.size > 0) {
+ const measureFn: TaffyMeasureFunction = (
+ knownDimensions,
+ availableSpace,
+ nodeId,
+ ) => {
+ const fn = measureFuncs.get(nodeId);
+ if (!fn) return { width: 0, height: 0 };
+
+ // Translate Taffy measure args to Yoga measure args
+ let width: number;
+ let widthMode: MeasureMode;
+ let height: number;
+ let heightMode: MeasureMode;
+
+ if (knownDimensions.width != null) {
+ width = knownDimensions.width;
+ widthMode = MeasureMode.Exactly;
+ } else if (typeof availableSpace.width === 'number') {
+ width = availableSpace.width;
+ widthMode = MeasureMode.AtMost;
+ } else {
+ // Taffy sends 'min-content' or 'max-content' strings here.
+ // Use a large value instead of 0 because measure functions like
+ // measureImage use Math.min(computed, width) even in Undefined mode.
+ width = Number.MAX_VALUE;
+ widthMode = MeasureMode.Undefined;
+ }
+
+ if (knownDimensions.height != null) {
+ height = knownDimensions.height;
+ heightMode = MeasureMode.Exactly;
+ } else if (typeof availableSpace.height === 'number') {
+ height = availableSpace.height;
+ heightMode = MeasureMode.AtMost;
+ } else {
+ height = Number.MAX_VALUE;
+ heightMode = MeasureMode.Undefined;
+ }
+
+ const result = fn(width, widthMode, height, heightMode);
+
+ return {
+ width: result?.width ?? knownDimensions.width ?? 0,
+ height: result?.height ?? knownDimensions.height ?? 0,
+ };
+ };
+
+ this.ctx.tree.computeLayoutWithMeasure(
+ this.nodeId,
+ availableSpace,
+ measureFn,
+ );
+ } else {
+ this.ctx.tree.computeLayout(this.nodeId, availableSpace);
+ }
+ }
+
+ // --- Computed layout getters ---
+
+ private getLayout() {
+ return this.ctx.tree.getLayout(this.nodeId);
+ }
+
+ getComputedWidth(): number {
+ return this.getLayout().width;
+ }
+ getComputedHeight(): number {
+ return this.getLayout().height;
+ }
+ getComputedTop(): number {
+ return this.getLayout().y;
+ }
+ getComputedLeft(): number {
+ return this.getLayout().x;
+ }
+
+ // Right and bottom are not directly in Taffy's Layout (only x, y).
+ // Return 0 as fallback — the getter chain in getPosition.ts handles this.
+ getComputedRight(): number {
+ return 0;
+ }
+ getComputedBottom(): number {
+ return 0;
+ }
+
+ getComputedMargin(edge: Edge): number {
+ const layout = this.getLayout();
+ switch (edge) {
+ case Edge.Top:
+ return layout.marginTop;
+ case Edge.Right:
+ return layout.marginRight;
+ case Edge.Bottom:
+ return layout.marginBottom;
+ case Edge.Left:
+ return layout.marginLeft;
+ default:
+ return 0;
+ }
+ }
+
+ getComputedPadding(edge: Edge): number {
+ const layout = this.getLayout();
+ switch (edge) {
+ case Edge.Top:
+ return layout.paddingTop;
+ case Edge.Right:
+ return layout.paddingRight;
+ case Edge.Bottom:
+ return layout.paddingBottom;
+ case Edge.Left:
+ return layout.paddingLeft;
+ default:
+ return 0;
+ }
+ }
+
+ getComputedBorder(edge: Edge): number {
+ const layout = this.getLayout();
+ switch (edge) {
+ case Edge.Top:
+ return layout.borderTop;
+ case Edge.Right:
+ return layout.borderRight;
+ case Edge.Bottom:
+ return layout.borderBottom;
+ case Edge.Left:
+ return layout.borderLeft;
+ default:
+ return 0;
+ }
+ }
+
+ // --- Cleanup ---
+
+ freeRecursive() {
+ this.ctx.reset();
+ }
+
+ free() {
+ // Individual node free is a no-op; tree reset handles everything
+ }
+}
+
+// --- Shared context per loadYoga() call ---
+
+class TaffyContext {
+ tree: TaffyTree;
+ nodes: TaffyNodeAdapter[] = [];
+ measureFuncs: Map = new Map();
+
+ constructor() {
+ this.tree = new TaffyTree();
+ this.tree.disableRounding();
+ }
+
+ createNode(): TaffyNodeAdapter {
+ const style = new Style();
+ style.display = TaffyDisplay.Flex; // Match Yoga's default (Taffy defaults to Block)
+ const nodeId = this.tree.newLeaf(style);
+ const adapter = new TaffyNodeAdapter(this, nodeId, style);
+ this.nodes.push(adapter);
+ return adapter;
+ }
+
+ reset() {
+ // Clear all nodes for reuse (pagination relayouts).
+ // Use clear() instead of free() to avoid Rust ownership issues
+ // with outstanding Layout/Style references.
+ this.tree.clear();
+ this.nodes = [];
+ this.measureFuncs.clear();
+ }
+}
+
+// --- Public API (same shape as before) ---
+
+export const loadYoga = async () => {
+ await initTaffy();
- const node = { create: () => instance.Node.createWithConfig(config) };
+ const ctx = new TaffyContext();
+ const node = { create: () => ctx.createNode() };
return { node };
};
diff --git a/packages/layout/src/yoga/initTaffy.ts b/packages/layout/src/yoga/initTaffy.ts
new file mode 100644
index 000000000..31fa81e8c
--- /dev/null
+++ b/packages/layout/src/yoga/initTaffy.ts
@@ -0,0 +1,9 @@
+/* eslint-disable no-return-assign */
+
+import { loadTaffy } from 'taffy-layout';
+
+let initPromise: Promise;
+
+export const initTaffy = () => {
+ return (initPromise ??= loadTaffy().then(() => {}));
+};
diff --git a/packages/layout/tests/node/getBorderWidth.test.ts b/packages/layout/tests/node/getBorderWidth.test.ts
index dfcd0d92e..4edcbfbf2 100644
--- a/packages/layout/tests/node/getBorderWidth.test.ts
+++ b/packages/layout/tests/node/getBorderWidth.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import getBorderWidth from '../../src/node/getBorderWidth';
diff --git a/packages/layout/tests/node/getMargin.test.ts b/packages/layout/tests/node/getMargin.test.ts
index 356150663..8ff359342 100644
--- a/packages/layout/tests/node/getMargin.test.ts
+++ b/packages/layout/tests/node/getMargin.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import getMargin from '../../src/node/getMargin';
diff --git a/packages/layout/tests/node/getPadding.test.ts b/packages/layout/tests/node/getPadding.test.ts
index 509eb0070..55d72ebb3 100644
--- a/packages/layout/tests/node/getPadding.test.ts
+++ b/packages/layout/tests/node/getPadding.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import getPadding from '../../src/node/getPadding';
diff --git a/packages/layout/tests/node/setAlignContent.test.ts b/packages/layout/tests/node/setAlignContent.test.ts
index ec49fd1f4..8eff41c12 100644
--- a/packages/layout/tests/node/setAlignContent.test.ts
+++ b/packages/layout/tests/node/setAlignContent.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setAlignContent from '../../src/node/setAlignContent';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setAlignItems.test.ts b/packages/layout/tests/node/setAlignItems.test.ts
index 46733ad78..8fb2a2d95 100644
--- a/packages/layout/tests/node/setAlignItems.test.ts
+++ b/packages/layout/tests/node/setAlignItems.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setAlignItems from '../../src/node/setAlignItems';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setAlignSelf.test.ts b/packages/layout/tests/node/setAlignSelf.test.ts
index 34f15e602..9d5b1e6ef 100644
--- a/packages/layout/tests/node/setAlignSelf.test.ts
+++ b/packages/layout/tests/node/setAlignSelf.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setAlignSelf from '../../src/node/setAlignSelf';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setBorderWidth.test.ts b/packages/layout/tests/node/setBorderWidth.test.ts
index 01fb18c21..554840e1d 100644
--- a/packages/layout/tests/node/setBorderWidth.test.ts
+++ b/packages/layout/tests/node/setBorderWidth.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setBorder, {
setBorderTop,
diff --git a/packages/layout/tests/node/setDisplay.test.ts b/packages/layout/tests/node/setDisplay.test.ts
index 0aad067fc..75868aff2 100644
--- a/packages/layout/tests/node/setDisplay.test.ts
+++ b/packages/layout/tests/node/setDisplay.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setDisplay from '../../src/node/setDisplay';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setFlexDirection.test.ts b/packages/layout/tests/node/setFlexDirection.test.ts
index 5dc9ff1f3..093b915cd 100644
--- a/packages/layout/tests/node/setFlexDirection.test.ts
+++ b/packages/layout/tests/node/setFlexDirection.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setFlexDirection from '../../src/node/setFlexDirection';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setFlexWrap.test.ts b/packages/layout/tests/node/setFlexWrap.test.ts
index 37d0435fd..af041318e 100644
--- a/packages/layout/tests/node/setFlexWrap.test.ts
+++ b/packages/layout/tests/node/setFlexWrap.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setFlexWrap from '../../src/node/setFlexWrap';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setGap.test.ts b/packages/layout/tests/node/setGap.test.ts
index 3a6a629e6..28753b93d 100644
--- a/packages/layout/tests/node/setGap.test.ts
+++ b/packages/layout/tests/node/setGap.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import { setRowGap, setColumnGap } from '../../src/node/setGap';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setJustifyContent.test.ts b/packages/layout/tests/node/setJustifyContent.test.ts
index 420c5155f..4e3e6b482 100644
--- a/packages/layout/tests/node/setJustifyContent.test.ts
+++ b/packages/layout/tests/node/setJustifyContent.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setJustifyContent from '../../src/node/setJustifyContent';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setMargin.test.ts b/packages/layout/tests/node/setMargin.test.ts
index cf3b0e14e..f10330789 100644
--- a/packages/layout/tests/node/setMargin.test.ts
+++ b/packages/layout/tests/node/setMargin.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setMargin, {
setMarginTop,
diff --git a/packages/layout/tests/node/setOverflow.test.ts b/packages/layout/tests/node/setOverflow.test.ts
index 288564cac..83d42c0c1 100644
--- a/packages/layout/tests/node/setOverflow.test.ts
+++ b/packages/layout/tests/node/setOverflow.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setOverflow from '../../src/node/setOverflow';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/node/setPadding.test.ts b/packages/layout/tests/node/setPadding.test.ts
index 19b7d6259..503de6ef0 100644
--- a/packages/layout/tests/node/setPadding.test.ts
+++ b/packages/layout/tests/node/setPadding.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setPadding, {
setPaddingTop,
diff --git a/packages/layout/tests/node/setPosition.test.ts b/packages/layout/tests/node/setPosition.test.ts
index 8781cd7e1..486f2aedc 100644
--- a/packages/layout/tests/node/setPosition.test.ts
+++ b/packages/layout/tests/node/setPosition.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setPosition, {
setPositionTop,
diff --git a/packages/layout/tests/node/setPositionType.test.ts b/packages/layout/tests/node/setPositionType.test.ts
index 6f442ed1e..ac24ac001 100644
--- a/packages/layout/tests/node/setPositionType.test.ts
+++ b/packages/layout/tests/node/setPositionType.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as Yoga from 'yoga-layout/load';
+import * as Yoga from '../../src/yoga/enums';
import setPositionType from '../../src/node/setPositionType';
import { SafeNode } from '../../src/types';
diff --git a/packages/layout/tests/steps/resolveTextLayout.test.ts b/packages/layout/tests/steps/resolveTextLayout.test.ts
index 7c2aa161b..36573a1fc 100644
--- a/packages/layout/tests/steps/resolveTextLayout.test.ts
+++ b/packages/layout/tests/steps/resolveTextLayout.test.ts
@@ -55,8 +55,6 @@ describe('text layout step', () => {
const root = await getRoot('text text text', { height: 50 });
const dimensions = resolveDimensions(root, fontStore);
- expect(getText(dimensions).lines).not.toBeDefined();
-
const textLayout = resolveTextLayout(dimensions, fontStore);
expect(getText(textLayout).lines).toBeDefined();
diff --git a/packages/renderer/tests/snapshots/debug-test-jsx-tests-debug-test-jsx-debug-should-show-paddings-and-margins-1-snap.png b/packages/renderer/tests/snapshots/debug-test-jsx-tests-debug-test-jsx-debug-should-show-paddings-and-margins-1-snap.png
index 09b023d7d..2a0bf92bc 100644
Binary files a/packages/renderer/tests/snapshots/debug-test-jsx-tests-debug-test-jsx-debug-should-show-paddings-and-margins-1-snap.png and b/packages/renderer/tests/snapshots/debug-test-jsx-tests-debug-test-jsx-debug-should-show-paddings-and-margins-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-gap-1-snap.png b/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-gap-1-snap.png
index 7fa554562..5204d4de9 100644
Binary files a/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-gap-1-snap.png and b/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-gap-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-gap-1-snap.png b/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-gap-1-snap.png
index 46eab526e..98c97a6d6 100644
Binary files a/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-gap-1-snap.png and b/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-gap-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-row-gap-and-column-gap-1-snap.png b/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-row-gap-and-column-gap-1-snap.png
index ebf44cccb..b5db7a845 100644
Binary files a/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-row-gap-and-column-gap-1-snap.png and b/packages/renderer/tests/snapshots/gap-test-jsx-tests-gap-test-jsx-flex-should-support-percentage-row-gap-and-column-gap-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/images-test-jsx-tests-images-test-jsx-image-should-render-jpgs-with-different-exif-orientations-1-snap.png b/packages/renderer/tests/snapshots/images-test-jsx-tests-images-test-jsx-image-should-render-jpgs-with-different-exif-orientations-1-snap.png
index 2226d7205..9be9237b5 100644
Binary files a/packages/renderer/tests/snapshots/images-test-jsx-tests-images-test-jsx-image-should-render-jpgs-with-different-exif-orientations-1-snap.png and b/packages/renderer/tests/snapshots/images-test-jsx-tests-images-test-jsx-image-should-render-jpgs-with-different-exif-orientations-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-should-match-snapshot-1-snap.png b/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-should-match-snapshot-1-snap.png
index d6305514e..d4f4bf5b9 100644
Binary files a/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-should-match-snapshot-1-snap.png and b/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-should-match-snapshot-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-1-snap.png b/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-1-snap.png
index 450a281d5..faef13356 100644
Binary files a/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-1-snap.png and b/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-2-snap.png b/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-2-snap.png
index d0e211881..848c84578 100644
Binary files a/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-2-snap.png and b/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-2-snap.png differ
diff --git a/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-3-snap.png b/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-3-snap.png
index 1b9ee1f06..070089267 100644
Binary files a/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-3-snap.png and b/packages/renderer/tests/snapshots/resume-test-jsx-tests-resume-test-jsx-resume-should-match-snapshot-3-snap.png differ
diff --git a/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-evaluate-sizes-media-conditions-1-snap.png b/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-evaluate-sizes-media-conditions-1-snap.png
index 2f1cd08a9..b53cd0653 100644
Binary files a/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-evaluate-sizes-media-conditions-1-snap.png and b/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-evaluate-sizes-media-conditions-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-respect-sizes-attribute-over-page-width-1-snap.png b/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-respect-sizes-attribute-over-page-width-1-snap.png
index 31fe59e91..68ca6437a 100644
Binary files a/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-respect-sizes-attribute-over-page-width-1-snap.png and b/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-respect-sizes-attribute-over-page-width-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-select-different-sources-based-on-page-width-1-snap.png b/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-select-different-sources-based-on-page-width-1-snap.png
index 64456ebba..5a01804d9 100644
Binary files a/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-select-different-sources-based-on-page-width-1-snap.png and b/packages/renderer/tests/snapshots/src-set-test-jsx-tests-src-set-test-jsx-src-set-should-select-different-sources-based-on-page-width-1-snap.png differ
diff --git a/packages/stylesheet/src/resolve/grid.ts b/packages/stylesheet/src/resolve/grid.ts
new file mode 100644
index 000000000..68f8a9f77
--- /dev/null
+++ b/packages/stylesheet/src/resolve/grid.ts
@@ -0,0 +1,17 @@
+import { processNoopValue } from './utils';
+
+const handlers = {
+ gridTemplateColumns: processNoopValue<'gridTemplateColumns'>,
+ gridTemplateRows: processNoopValue<'gridTemplateRows'>,
+ gridAutoColumns: processNoopValue<'gridAutoColumns'>,
+ gridAutoRows: processNoopValue<'gridAutoRows'>,
+ gridAutoFlow: processNoopValue<'gridAutoFlow'>,
+ gridColumn: processNoopValue<'gridColumn'>,
+ gridColumnStart: processNoopValue<'gridColumnStart'>,
+ gridColumnEnd: processNoopValue<'gridColumnEnd'>,
+ gridRow: processNoopValue<'gridRow'>,
+ gridRowStart: processNoopValue<'gridRowStart'>,
+ gridRowEnd: processNoopValue<'gridRowEnd'>,
+};
+
+export default handlers;
diff --git a/packages/stylesheet/src/resolve/index.ts b/packages/stylesheet/src/resolve/index.ts
index be4febcfb..35425cf99 100644
--- a/packages/stylesheet/src/resolve/index.ts
+++ b/packages/stylesheet/src/resolve/index.ts
@@ -3,6 +3,7 @@ import colorHandlers from './colors';
import dimensionHandlers from './dimensions';
import flexHandlers from './flex';
import gapHandlers from './gap';
+import gridHandlers from './grid';
import layoutHandlers from './layout';
import marginHandlers from './margins';
import paddingHandlers from './paddings';
@@ -25,6 +26,7 @@ const shorthands: Partial> = {
...dimensionHandlers,
...flexHandlers,
...gapHandlers,
+ ...gridHandlers,
...layoutHandlers,
...marginHandlers,
...paddingHandlers,
diff --git a/packages/stylesheet/src/types.ts b/packages/stylesheet/src/types.ts
index e7bd5adc6..39db0fcb2 100644
--- a/packages/stylesheet/src/types.ts
+++ b/packages/stylesheet/src/types.ts
@@ -214,7 +214,7 @@ export type TransformStyle = TransformShorthandStyle & TransformExpandedStyle;
// Layout
-export type Display = 'flex' | 'none';
+export type Display = 'flex' | 'grid' | 'none';
export type Position = 'absolute' | 'relative' | 'static';
@@ -430,6 +430,30 @@ export type SvgSafeStyle = SvgStyle & {
strokeOpacity?: number;
};
+// Grid
+
+export type GridAutoFlow = 'row' | 'column' | 'row dense' | 'column dense';
+
+export type GridPlacement = 'auto' | number | string;
+
+export type GridStyle = {
+ gridTemplateColumns?: string;
+ gridTemplateRows?: string;
+ gridAutoColumns?: string;
+ gridAutoRows?: string;
+ gridAutoFlow?: GridAutoFlow;
+ gridColumn?: string;
+ gridColumnStart?: GridPlacement;
+ gridColumnEnd?: GridPlacement;
+ gridRow?: string;
+ gridRowStart?: GridPlacement;
+ gridRowEnd?: GridPlacement;
+};
+
+export type GridExpandedStyle = GridStyle;
+
+export type GridSafeStyle = GridStyle;
+
// Global
type BaseStyle = BorderStyle &
@@ -437,6 +461,7 @@ type BaseStyle = BorderStyle &
DimensionStyle &
FlexboxStyle &
GapStyle &
+ GridStyle &
LayoutStyle &
MarginStyle &
PaddingStyle &
@@ -458,6 +483,7 @@ export type ExpandedStyle = BorderExpandedStyle &
DimensionExpandedStyle &
FlexboxExpandedStyle &
GapExpandedStyle &
+ GridExpandedStyle &
LayoutExpandedStyle &
MarginExpandedStyle &
PaddingExpandedStyle &
@@ -471,6 +497,7 @@ export type SafeStyle = BorderSafeStyle &
DimensionSafeStyle &
FlexboxSafeStyle &
GapSafeStyle &
+ GridSafeStyle &
LayoutSafeStyle &
MarginSafeStyle &
PaddingSafeStyle &
diff --git a/yarn.lock b/yarn.lock
index 31e9efa96..8921f980f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -752,14 +752,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-react-jsx-development@^7.18.6":
- version "7.23.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz#ed3e7dadde046cce761a8e3cf003a13d1a7972d9"
- integrity sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-react-jsx-self@^7.23.3":
+"@babel/plugin-transform-react-jsx-development@^7.18.6", "@babel/plugin-transform-react-jsx-self@^7.23.3":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz#ed3e7dadde046cce761a8e3cf003a13d1a7972d9"
integrity sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==
@@ -10095,16 +10088,7 @@ string-argv@0.3.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10231,14 +10215,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -10364,6 +10341,11 @@ synckit@^0.9.1:
"@pkgr/core" "^0.1.0"
tslib "^2.6.2"
+taffy-layout@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/taffy-layout/-/taffy-layout-2.0.3.tgz#e804eeb10829c96b3f63ca514dac7ee444d3fc5e"
+ integrity sha512-gKR7T3GvdI1itN8ZvMACk739aVXS3bWdNVYfDcKJej02Fl448+hjd8TszlMzV942BtsgHYiGcjSpVjZafeXs8g==
+
tailwindcss@^3.4.17:
version "3.4.17"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
@@ -11284,7 +11266,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11302,15 +11284,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -11501,8 +11474,3 @@ yocto-queue@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
-
-yoga-layout@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843"
- integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==