Skip to content

Commit

Permalink
Merge pull request #8 from bitovi/feat/paragraphs
Browse files Browse the repository at this point in the history
Feat/paragraphs
  • Loading branch information
Mattchewone authored Jan 30, 2025
2 parents 8d4366d + db62238 commit 4796f3c
Show file tree
Hide file tree
Showing 13 changed files with 2,469 additions and 209 deletions.
50 changes: 50 additions & 0 deletions src/processors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Layout Processing Logic

## Figma to CSS Flex Layout Mapping

The layout processors convert Figma's Auto Layout properties to CSS Flexbox properties. Here's how the mapping works:

### Display Property
- When `layoutMode !== "NONE"` -> `display: flex`
- When `layoutMode === "NONE"` -> no display property output

### Flex Direction
- `layoutMode === "HORIZONTAL"` -> `flex-direction: row`
- `layoutMode === "VERTICAL"` -> `flex-direction: column`

### Justify Content (Primary Axis Alignment)
Controlled by `primaryAxisAlignItems`:
- `"MIN"` -> `justify-content: flex-start`
- `"CENTER"` -> `justify-content: center`
- `"MAX"` -> `justify-content: flex-end`
- `"SPACE_BETWEEN"` -> `justify-content: space-between`

### Align Items (Counter Axis Alignment)
Controlled by `counterAxisAlignItems`:
- `"MIN"` -> `align-items: flex-start`
- `"CENTER"` -> `align-items: center`
- `"MAX"` -> `align-items: flex-end`
- `"BASELINE"` -> `align-items: baseline`

## Complete Mapping Table

| Figma layoutMode | Figma primaryAxisAlignItems | Figma counterAxisAlignItems | CSS display | CSS flex-direction | CSS justify-content | CSS align-items |
|-----------------|---------------------------|---------------------------|------------|------------------|-------------------|----------------|
| "HORIZONTAL" | "MIN" | "MIN" | flex | row | flex-start | flex-start |
| "HORIZONTAL" | "CENTER" | "MIN" | flex | row | center | flex-start |
| "HORIZONTAL" | "MAX" | "MIN" | flex | row | flex-end | flex-start |
| "HORIZONTAL" | "SPACE_BETWEEN" | "MIN" | flex | row | space-between | flex-start |
| "VERTICAL" | "MIN" | "MIN" | flex | column | flex-start | flex-start |
| "VERTICAL" | "CENTER" | "MIN" | flex | column | center | flex-start |
| "VERTICAL" | "MAX" | "MIN" | flex | column | flex-end | flex-start |
| "VERTICAL" | "SPACE_BETWEEN" | "MIN" | flex | column | space-between | flex-start |
[...additional combinations omitted for brevity]

## Implementation Notes

1. Layout properties are only processed for nodes with Auto Layout enabled (`layoutMode !== "NONE"`)
2. The primary axis alignment maps to justify-content
3. The counter axis alignment maps to align-items
4. Gap is handled separately using the itemSpacing property

See `layout.processor.ts` for the implementation.
121 changes: 108 additions & 13 deletions src/processors/font.processor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { StyleProcessor, VariableToken, ProcessedValue } from '../types';
import { rgbaToString } from '../utils/color.utils';

interface NodeWithFont {
fontSize?: number;
fontName?: FontName;
fontWeight?: number; // Direct API property
lineHeight?: LineHeight | number;
letterSpacing?: LetterSpacing | number;
}

function hasFont(node: SceneNode): node is SceneNode & NodeWithFont {
return 'fontSize' in node || 'fontName' in node || 'fontWeight' in node;
}

export const fontProcessors: StyleProcessor[] = [
{
property: "color",
bindingKey: "fills",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const colorVariable = variables.find(v => v.property === 'color');
const colorVariable = variables.find(v => v.property === 'fills');
if (colorVariable) {
return {
value: colorVariable.value,
Expand All @@ -30,7 +42,7 @@ export const fontProcessors: StyleProcessor[] = [
property: "font-family",
bindingKey: "fontFamily",
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
const fontVariable = variables.find(v => v.property === 'font-family');
const fontVariable = variables.find(v => v.property === 'fontFamily');
if (fontVariable) {
return {
value: fontVariable.value,
Expand All @@ -49,7 +61,7 @@ export const fontProcessors: StyleProcessor[] = [
property: "font-size",
bindingKey: "fontSize",
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
const sizeVariable = variables.find(v => v.property === 'font-size');
const sizeVariable = variables.find(v => v.property === 'fontSize');
if (sizeVariable) {
return {
value: sizeVariable.value,
Expand All @@ -67,27 +79,54 @@ export const fontProcessors: StyleProcessor[] = [
{
property: "font-weight",
bindingKey: "fontWeight",
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
const weightVariable = variables.find(v => v.property === 'font-weight');
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
if (!node || !hasFont(node)) return null;

if (node.fontWeight) {
return {
value: String(node.fontWeight),
rawValue: String(node.fontWeight)
};
}

const weightVariable = variables.find(v => v.property === 'fontWeight');
if (weightVariable) {
return {
value: weightVariable.value,
rawValue: weightVariable.rawValue
};
}

if (node?.type === "TEXT") {
const value = String(node.fontWeight);
return { value, rawValue: value };
if (node.fontName && typeof node.fontName === 'object') {
const weightMap: Record<string, string> = {
'Thin': '100',
'Extra Light': '200',
'Light': '300',
'Regular': '400',
'Medium': '500',
'Semi Bold': '600',
'Bold': '700',
'Extra Bold': '800',
'Black': '900',
};

const style = node.fontName.style;
const weight = weightMap[style] || '400';

return {
value: weight,
rawValue: weight
};
}

return null;
}
},
{
property: "line-height",
bindingKey: "lineHeight",
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
const heightVariable = variables.find(v => v.property === 'line-height');
const heightVariable = variables.find(v => v.property === 'lineHeight');
if (heightVariable) {
return {
value: heightVariable.value,
Expand All @@ -101,8 +140,8 @@ export const fontProcessors: StyleProcessor[] = [
if (lineHeight.unit === "AUTO") {
return { value: "normal", rawValue: "normal" };
}
const value = lineHeight.unit.toLowerCase() === "percent" ?
`${lineHeight.value}%` :
const value = lineHeight.unit.toLowerCase() === "percent" ?
`${lineHeight.value}%` :
(lineHeight.value > 4 ? `${lineHeight.value}px` : String(lineHeight.value));
return { value, rawValue: value };
}
Expand All @@ -114,7 +153,7 @@ export const fontProcessors: StyleProcessor[] = [
property: "letter-spacing",
bindingKey: "letterSpacing",
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
const spacingVariable = variables.find(v => v.property === 'letter-spacing');
const spacingVariable = variables.find(v => v.property === 'letterSpacing');
if (spacingVariable) {
return {
value: spacingVariable.value,
Expand All @@ -131,5 +170,61 @@ export const fontProcessors: StyleProcessor[] = [
}
return null;
}
}
},
{
property: "display",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'textAlignVertical' in node) {
return { value: "flex", rawValue: "flex" };
}
return null;
}
},
{
property: "flex-direction",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'textAlignVertical' in node) {
return { value: "column", rawValue: "column" };
}
return null;
}
},
{
property: "justify-content",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'textAlignVertical' in node) {
const alignMap = {
TOP: "flex-start",
CENTER: "center",
BOTTOM: "flex-end"
};
const value = alignMap[node.textAlignVertical];
return { value, rawValue: value };
}
return null;
}
},
{
property: "width",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'width' in node) {
return { value: `${node.width}px`, rawValue: `${node.width}px` };
}
return null;
}
},
{
property: "height",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'height' in node) {
return { value: `${node.height}px`, rawValue: `${node.height}px` };
}
return null;
}
},
];
8 changes: 6 additions & 2 deletions src/processors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { fontProcessors } from './font.processor';
import { layoutProcessors } from './layout.processor';
import { borderProcessors } from './border.processor';
import { spacingProcessors } from './spacing.processor';
import { textAlignProcessors } from './text-align.processor';

export function getProcessorsForNode(node: SceneNode): StyleProcessor[] {
switch (node.type) {
case "TEXT":
return fontProcessors;
return [
...fontProcessors,
...textAlignProcessors
];
case "FRAME":
case "RECTANGLE":
case "INSTANCE":
return [
backgroundProcessor,
...layoutProcessors,
...borderProcessors,
...spacingProcessors
...spacingProcessors,
];
default:
return [];
Expand Down
27 changes: 23 additions & 4 deletions src/processors/layout.processor.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { StyleProcessor, ProcessedValue, VariableToken } from '../types';
import { StyleProcessor, ProcessedValue } from '../types';

export const layoutProcessors: StyleProcessor[] = [
{
property: "display",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node && 'layoutMode' in node && node.layoutMode !== "NONE") {
const value = node.layoutAlign !== "STRETCH" ? "inline-flex" : "flex";
if (node && ('layoutMode' in node) && node.layoutMode !== "NONE") {
const value = "flex";
return { value, rawValue: value };
}

return null;
}
},
Expand All @@ -24,7 +25,7 @@ export const layoutProcessors: StyleProcessor[] = [
}
},
{
property: "align-items",
property: "justify-content",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node && 'layoutMode' in node && node.layoutMode !== "NONE" &&
Expand All @@ -40,6 +41,24 @@ export const layoutProcessors: StyleProcessor[] = [
return null;
}
},
{
property: "align-items",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node && 'layoutMode' in node && node.layoutMode !== "NONE" &&
'counterAxisAlignItems' in node) {
const alignMap = {
MIN: "flex-start",
CENTER: "center",
MAX: "flex-end",
BASELINE: "baseline"
};
const value = alignMap[node.counterAxisAlignItems];
return { value, rawValue: value };
}
return null;
}
},
{
property: "gap",
bindingKey: "itemSpacing",
Expand Down
35 changes: 35 additions & 0 deletions src/processors/text-align.processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { StyleProcessor, ProcessedValue } from '../types';

interface NodeWithTextAlign {
textAlignHorizontal: string;
}

function hasTextAlign(node: SceneNode): node is SceneNode & NodeWithTextAlign {
return 'textAlignHorizontal' in node;
}

export const textAlignProcessors: StyleProcessor[] = [
{
property: "text-align",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (!node || !hasTextAlign(node)) return null;

// Map Figma's text align values to CSS values
const alignmentMap: Record<string, string> = {
LEFT: 'left',
CENTER: 'center',
RIGHT: 'right',
JUSTIFIED: 'justify'
};

const alignment = alignmentMap[node.textAlignHorizontal.toUpperCase()];
if (!alignment) return null;

return {
value: alignment,
rawValue: alignment
};
}
}
];
Loading

0 comments on commit 4796f3c

Please sign in to comment.