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==