Skip to content

Commit

Permalink
Merge pull request #9 from bitovi/feat/typography
Browse files Browse the repository at this point in the history
feat (PD-357): Typography
  • Loading branch information
Mattchewone authored Feb 4, 2025
2 parents 16d4b62 + f11aee2 commit 2b6c0e7
Show file tree
Hide file tree
Showing 19 changed files with 1,040 additions and 134 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@
"html-inline-script-webpack-plugin": "^3.2.1",
"html-webpack-plugin": "^5.6.3",
"jest": "^29.0.0",
"lite-server": "^2.6.1",
"mini-css-extract-plugin": "^2.9.2",
"sass-rem": "^4.0.0",
"ts-jest": "^29.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.3.2",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"lite-server": "^2.6.1"
"webpack-cli": "^6.0.1"
},
"eslintConfig": {
"extends": [
Expand Down
91 changes: 78 additions & 13 deletions src/processors/font.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ interface NodeWithFont {
lineHeight?: LineHeight | number;
letterSpacing?: LetterSpacing | number;
}
interface NodeWithTextAlign {
textAlignHorizontal: string;
}

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

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

export const fontProcessors: StyleProcessor[] = [
{
property: "color",
Expand Down Expand Up @@ -65,13 +72,14 @@ export const fontProcessors: StyleProcessor[] = [
if (sizeVariable) {
return {
value: sizeVariable.value,
rawValue: sizeVariable.rawValue
rawValue: sizeVariable.rawValue,
valueType: sizeVariable.valueType
};
}

if (node?.type === "TEXT") {
const value = `${String(node.fontSize)}px`;
return { value, rawValue: value };
return { value, rawValue: value, valueType: 'px' };
}
return null;
}
Expand Down Expand Up @@ -122,6 +130,25 @@ export const fontProcessors: StyleProcessor[] = [
return null;
}
},
{
property: "font-style",
bindingKey: "fontStyle",
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
const styleVariable = variables.find(v => v.property === 'fontStyle');
if (styleVariable) {
return {
value: styleVariable.value.toLowerCase(),
rawValue: styleVariable.rawValue.toLowerCase(),
};
}
if (node?.type === "TEXT" && node.fontName && typeof node.fontName === 'object') {
const value = node.fontName.style.toLowerCase() === 'italic' ? 'italic' : 'normal';
return { value, rawValue: value };
}

return null;
}
},
{
property: "line-height",
bindingKey: "lineHeight",
Expand Down Expand Up @@ -157,15 +184,17 @@ export const fontProcessors: StyleProcessor[] = [
if (spacingVariable) {
return {
value: spacingVariable.value,
rawValue: spacingVariable.rawValue
rawValue: spacingVariable.rawValue,
valueType: spacingVariable.valueType
};
}

if (node?.type === "TEXT" && 'letterSpacing' in node) {
const letterSpacing = node.letterSpacing;
if (typeof letterSpacing === 'object' && letterSpacing.value !== 0) {
const value = `${letterSpacing.value}${letterSpacing.unit.toLowerCase() === "percent" ? '%' : 'px'}`;
return { value, rawValue: value };
const type = letterSpacing.unit.toLowerCase() === "percent" ? '%' : 'px';
const value = `${letterSpacing.value}${type}`;
return { value, rawValue: value, valueType: type };
}
}
return null;
Expand All @@ -175,7 +204,10 @@ export const fontProcessors: StyleProcessor[] = [
property: "display",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'textAlignVertical' in node) {
// Only apply flex if text has explicit sizing/alignment
if (node?.type === "TEXT" &&
(node.textAutoResize !== "WIDTH_AND_HEIGHT" ||
node.textAlignVertical !== "TOP")) {
return { value: "flex", rawValue: "flex" };
}
return null;
Expand All @@ -185,7 +217,10 @@ export const fontProcessors: StyleProcessor[] = [
property: "flex-direction",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'textAlignVertical' in node) {
// Only apply if we're using flex display
if (node?.type === "TEXT" &&
(node.textAutoResize !== "WIDTH_AND_HEIGHT" ||
node.textAlignVertical !== "TOP")) {
return { value: "column", rawValue: "column" };
}
return null;
Expand All @@ -195,7 +230,8 @@ export const fontProcessors: StyleProcessor[] = [
property: "justify-content",
bindingKey: undefined,
process: async (_, node?: SceneNode): Promise<ProcessedValue | null> => {
if (node?.type === "TEXT" && 'textAlignVertical' in node) {
// Only apply if we're using flex display and have vertical alignment
if (node?.type === "TEXT" && node.textAlignVertical !== "TOP") {
const alignMap = {
TOP: "flex-start",
CENTER: "center",
Expand All @@ -208,11 +244,38 @@ export const fontProcessors: StyleProcessor[] = [
}
},
{
property: "width",
property: "text-align",
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` };
if (!node || !hasTextAlign(node)) return null;

if (node?.type === "TEXT" && node.textAlignHorizontal !== "LEFT") {
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
};
}
return null;
}
},
{
property: "width",
bindingKey: undefined,
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
// Only apply width if text doesn't auto-resize width
if (node?.type === "TEXT" &&
!["WIDTH_AND_HEIGHT", "WIDTH"].includes(node.textAutoResize)) {
return { value: `${node.width}px`, rawValue: `${node.width}px`, valueType: 'px' };
}
return null;
}
Expand All @@ -221,8 +284,10 @@ export const fontProcessors: StyleProcessor[] = [
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` };
// Only apply height if text doesn't auto-resize height
if (node?.type === "TEXT" &&
!["WIDTH_AND_HEIGHT", "HEIGHT"].includes(node.textAutoResize)) {
return { value: `${node.height}px`, rawValue: `${node.height}px`, valueType: 'px' };
}
return null;
}
Expand Down
2 changes: 0 additions & 2 deletions src/processors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ 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,
...textAlignProcessors
];
case "FRAME":
case "RECTANGLE":
Expand Down
6 changes: 4 additions & 2 deletions src/processors/layout.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,17 @@ export const layoutProcessors: StyleProcessor[] = [
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const gapVariable = variables.find(v => v.property === 'gap');
if (gapVariable) {

return {
value: gapVariable.value,
rawValue: gapVariable.rawValue
rawValue: gapVariable.rawValue,
valueType: gapVariable.valueType
};
}

if (node && 'itemSpacing' in node && node.itemSpacing > 0) {
const value = `${node.itemSpacing}px`;
return { value, rawValue: value };
return { value, rawValue: value, valueType: 'px' };
}
return null;
}
Expand Down
9 changes: 6 additions & 3 deletions src/processors/spacing.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,25 @@ export const spacingProcessors: StyleProcessor[] = [
// All sides equal - use single value
return {
value: getValue('top'),
rawValue: pixelValues.top
rawValue: pixelValues.top,
valueType: 'px'
};
}

if (pixelValues.top === pixelValues.bottom && pixelValues.left === pixelValues.right) {
// Vertical/horizontal pairs equal - use two values
return {
value: `${getValue('top')} ${getValue('left')}`,
rawValue: `${pixelValues.top} ${pixelValues.left}`
rawValue: `${pixelValues.top} ${pixelValues.left}`,
valueType: 'px'
};
}

// All sides different - use four values
return {
value: `${getValue('top')} ${getValue('right')} ${getValue('bottom')} ${getValue('left')}`,
rawValue: `${pixelValues.top} ${pixelValues.right} ${pixelValues.bottom} ${pixelValues.left}`
rawValue: `${pixelValues.top} ${pixelValues.right} ${pixelValues.bottom} ${pixelValues.left}`,
valueType: 'px'
};
}
}
Expand Down
35 changes: 0 additions & 35 deletions src/processors/text-align.processor.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/services/collection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ export async function collectTokens(): Promise<TokenCollection> {

await processNode(figma.currentPage);
return collection;
}
}
1 change: 1 addition & 0 deletions src/services/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function extractNodeToken(
name: path.join('_'),
value: processedValue.value,
rawValue: processedValue.rawValue,
valueType: processedValue.valueType,
property: processor.property,
path: path.length > 1 ? path.slice(1) : path,
variables: variableTokensMap.size > 0 ? [...variableTokensMap.values()] : undefined,
Expand Down
5 changes: 4 additions & 1 deletion src/services/variable.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ async function getVariableFallback(variable: Variable | null, propertyName: stri
export async function collectBoundVariable(varId: string, property: string, path: string[], node: SceneNode): Promise<VariableToken | null> {
const variable = await figma.variables.getVariableByIdAsync(varId);
if (!variable) return null;
const rawValue = await getVariableFallback(variable, property);
const valueType = rawValue.includes('px') ? 'px' : null;

return {
type: 'variable',
path,
property,
name: variable.name,
value: `$${variable.name}`,
rawValue: await getVariableFallback(variable, property),
rawValue: rawValue.toLowerCase(),
valueType: valueType,
metadata: {
figmaId: node.id,
variableId: variable.id,
Expand Down
Loading

0 comments on commit 2b6c0e7

Please sign in to comment.