Skip to content

Commit

Permalink
Merge pull request #6 from bitovi/feat/padding
Browse files Browse the repository at this point in the history
Feat/padding
  • Loading branch information
Mattchewone authored Jan 29, 2025
2 parents f3effdc + dc5380b commit 53477aa
Show file tree
Hide file tree
Showing 22 changed files with 3,729 additions and 243 deletions.
4 changes: 2 additions & 2 deletions src/github.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Utils from './utils';
import { toBase64 } from './utils/index';

export interface PRResult {
prUrl: string;
Expand Down Expand Up @@ -102,7 +102,7 @@ export default {
headers,
body: JSON.stringify({
message: 'Update SCSS variables from Figma',
content: Utils.toBase64(content),
content: toBase64(content),
branch: branchName,
...(fileSha && { sha: fileSha }) // Include SHA if file exists
})
Expand Down
2 changes: 1 addition & 1 deletion src/processors/background.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const backgroundProcessor: StyleProcessor = {

const backgrounds = await Promise.all(visibleFills.map(async (fill: Paint) => {
if (fill.type === "SOLID") {
const fillVariable = variables.find(v => v.property === 'background');
const fillVariable = variables.find(v => v.property === 'fills');
if (fillVariable) {
return {
value: fillVariable.value,
Expand Down
8 changes: 4 additions & 4 deletions src/processors/border.processor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { StyleProcessor, ProcessedValue } from '../types';
import Utils from '../utils';
import { rgbaToString } from '../utils/index';

export const borderProcessors: StyleProcessor[] = [
{
property: "border-color",
bindingKey: "strokes",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const borderVariable = variables.find(v => v.property === 'border-color');
const borderVariable = variables.find(v => v.property === 'strokes');
if (borderVariable) {
return {
value: borderVariable.value,
Expand All @@ -19,7 +19,7 @@ export const borderProcessors: StyleProcessor[] = [
if (stroke?.type === "SOLID") {
const { r, g, b } = stroke.color;
const a = stroke.opacity ?? 1;
const value = Utils.rgbaToString(r, g, b, a);
const value = rgbaToString(r, g, b, a);
return { value, rawValue: value };
}
}
Expand All @@ -30,7 +30,7 @@ export const borderProcessors: StyleProcessor[] = [
property: "border-width",
bindingKey: "strokeWeight",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const widthVariable = variables.find(v => v.property === 'border-width');
const widthVariable = variables.find(v => v.property === 'strokeWeight');
if (widthVariable) {
return {
value: widthVariable.value,
Expand Down
38 changes: 0 additions & 38 deletions src/processors/layout.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,42 +59,4 @@ export const layoutProcessors: StyleProcessor[] = [
return null;
}
},
{
property: "padding",
bindingKey: undefined,
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const top = variables.find(v => v.property === 'padding-top');
const right = variables.find(v => v.property === 'padding-right');
const bottom = variables.find(v => v.property === 'padding-bottom');
const left = variables.find(v => v.property === 'padding-left');

if (top || right || bottom || left) {
const getValue = (v: VariableToken | undefined, fallback: string) => v ? v.value : fallback;
const getRawValue = (v: VariableToken | undefined, fallback: string) => v ? v.rawValue : fallback;

return {
value: `${getValue(top, '0')} ${getValue(right, '0')} ${getValue(bottom, '0')} ${getValue(left, '0')}`,
rawValue: `${getRawValue(top, '0')} ${getRawValue(right, '0')} ${getRawValue(bottom, '0')} ${getRawValue(left, '0')}`
};
}

if (node && 'paddingTop' in node) {
const topVal = `${node.paddingTop}px`;
const rightVal = `${node.paddingRight}px`;
const bottomVal = `${node.paddingBottom}px`;
const leftVal = `${node.paddingLeft}px`;

if (topVal === rightVal && rightVal === bottomVal && bottomVal === leftVal) {
return { value: topVal, rawValue: topVal };
}
if (topVal === bottomVal && leftVal === rightVal) {
const value = `${topVal} ${leftVal}`;
return { value, rawValue: value };
}
const value = `${topVal} ${rightVal} ${bottomVal} ${leftVal}`;
return { value, rawValue: value };
}
return null;
}
}
];
125 changes: 60 additions & 65 deletions src/processors/spacing.processor.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,75 @@
import { StyleProcessor, ProcessedValue } from '../types';
import { StyleProcessor, ProcessedValue, VariableToken } from '../types';

interface NodeWithPadding {
paddingTop: number;
paddingRight: number;
paddingBottom: number;
paddingLeft: number;
}

function hasNodePadding(node: SceneNode): node is SceneNode & NodeWithPadding {
return 'paddingTop' in node && 'paddingRight' in node && 'paddingBottom' in node && 'paddingLeft' in node;
}

interface PaddingValues {
top: string;
right: string;
bottom: string;
left: string;
}

export const spacingProcessors: StyleProcessor[] = [
{
property: "padding-top",
bindingKey: "paddingTop",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const paddingVariable = variables.find(v => v.property === 'padding-top');
if (paddingVariable) {
return {
value: paddingVariable.value,
rawValue: paddingVariable.rawValue
};
}
property: "padding",
bindingKey: undefined,
process: async (variables: VariableToken[], node?: SceneNode): Promise<ProcessedValue | null> => {
if (!node || !hasNodePadding(node)) return null;

if (node && 'paddingTop' in node && node.paddingTop > 0) {
const value = `${node.paddingTop}px`;
return { value, rawValue: value };
}
return null;
}
},
{
property: "padding-right",
bindingKey: "paddingRight",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const paddingVariable = variables.find(v => v.property === 'padding-right');
if (paddingVariable) {
return {
value: paddingVariable.value,
rawValue: paddingVariable.rawValue
};
}
// Get pixel values
const pixelValues: PaddingValues = {
top: `${node.paddingTop}px`,
right: `${node.paddingRight}px`,
bottom: `${node.paddingBottom}px`,
left: `${node.paddingLeft}px`,
};

if (node && 'paddingRight' in node && node.paddingRight > 0) {
const value = `${node.paddingRight}px`;
return { value, rawValue: value };
}
return null;
}
},
{
property: "padding-bottom",
bindingKey: "paddingBottom",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const paddingVariable = variables.find(v => v.property === 'padding-bottom');
if (paddingVariable) {
// Find variable values from passed in variables
const varValues: Partial<PaddingValues> = {
top: variables.find(v => v.property === 'paddingTop')?.value,
right: variables.find(v => v.property === 'paddingRight')?.value,
bottom: variables.find(v => v.property === 'paddingBottom')?.value,
left: variables.find(v => v.property === 'paddingLeft')?.value,
};

// Helper to get final value (variable or pixel)
const getValue = (key: keyof PaddingValues) => varValues[key] || pixelValues[key];

// Determine the most concise padding format
if (allEqual([pixelValues.top, pixelValues.right, pixelValues.bottom, pixelValues.left])) {
// All sides equal - use single value
return {
value: paddingVariable.value,
rawValue: paddingVariable.rawValue
value: getValue('top'),
rawValue: pixelValues.top
};
}

if (node && 'paddingBottom' in node && node.paddingBottom > 0) {
const value = `${node.paddingBottom}px`;
return { value, rawValue: value };
}
return null;
}
},
{
property: "padding-left",
bindingKey: "paddingLeft",
process: async (variables, node?: SceneNode): Promise<ProcessedValue | null> => {
const paddingVariable = variables.find(v => v.property === 'padding-left');
if (paddingVariable) {
if (pixelValues.top === pixelValues.bottom && pixelValues.left === pixelValues.right) {
// Vertical/horizontal pairs equal - use two values
return {
value: paddingVariable.value,
rawValue: paddingVariable.rawValue
value: `${getValue('top')} ${getValue('left')}`,
rawValue: `${pixelValues.top} ${pixelValues.left}`
};
}

if (node && 'paddingLeft' in node && node.paddingLeft > 0) {
const value = `${node.paddingLeft}px`;
return { value, rawValue: value };
}
return null;
// 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}`
};
}
}
];
];

function allEqual(values: string[]): boolean {
return values.every(v => v === values[0]);
}
68 changes: 39 additions & 29 deletions src/services/token.service.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,63 @@
import { StyleToken, VariableToken } from '../types';
import { StyleProcessor, VariableBindings } from '../types/processors';
import Utils from '../utils';
import { getVariableFallback } from './variable.service';
import { collectBoundVariable } from './variable.service';

export async function extractNodeToken(
node: SceneNode,
processor: StyleProcessor,
path: string[]
): Promise<(StyleToken | VariableToken)[]> {
const tokens: (StyleToken | VariableToken)[] = [];
const variableTokensMap = new Map<string, VariableToken>();

// Step 1: Handle Variable Bindings
// Helper to check or add variable token
const getOrCreateVariableToken = async (varId: string, property: string) => {
const key = `${varId}-${property}`;
if (variableTokensMap.has(key)) {
return variableTokensMap.get(key)!;
}

const token = await collectBoundVariable(varId, property, path, node);
if (token) {
variableTokensMap.set(key, token);
tokens.push(token);
}
return token;
};

// Step 1 & 2: Handle Variable Bindings
const customBoundVariables = node.boundVariables as unknown as VariableBindings;
const bindings = processor.bindingKey
? (Array.isArray(customBoundVariables[processor.bindingKey])
? customBoundVariables[processor.bindingKey] as VariableAlias[]
: [customBoundVariables[processor.bindingKey]] as VariableAlias[])
: [];

// Step 2: Create Variable Tokens
const variableTokens: VariableToken[] = [];
for (const binding of bindings) {
if (!binding?.id) continue;

const variable = await figma.variables.getVariableByIdAsync(binding.id);
if (!variable) continue;
if (binding?.id) {
await getOrCreateVariableToken(binding.id, processor.property);
}
}

const rawValue = await getVariableFallback(variable, processor.property);
const variableToken: VariableToken = {
type: 'variable',
name: variable.name,
value: `$${Utils.sanitizeName(variable.name)}`,
rawValue,
property: processor.property,
path,
metadata: {
figmaId: node.id,
variableId: variable.id,
variableName: variable.name,
// Step 3: Collect variables from boundVariables
if ('boundVariables' in node && node.boundVariables) {
for (const [key, value] of Object.entries(node.boundVariables)) {
if (typeof key === 'string' && value) {
if (Array.isArray(value)) {
for (const v of value) {
if (v.type === 'VARIABLE_ALIAS') {
await getOrCreateVariableToken(v.id, key);
}
}
} else if (value?.type === 'VARIABLE_ALIAS') {
await getOrCreateVariableToken(String(value.id), key);
}
}
};

variableTokens.push(variableToken);
tokens.push(variableToken);
}
}

// Step 3: Process the node and create Style Token
const processedValue = await processor.process(variableTokens, node);
// Step 4: Process the node and create Style Token
const processedValue = await processor.process([...variableTokensMap.values()], node);
if (processedValue) {
const styleToken: StyleToken = {
type: 'style',
Expand All @@ -55,12 +66,11 @@ export async function extractNodeToken(
rawValue: processedValue.rawValue,
property: processor.property,
path: path.length > 1 ? path.slice(1) : path,
variables: variableTokens.length > 0 ? variableTokens : undefined,
variables: variableTokensMap.size > 0 ? [...variableTokensMap.values()] : undefined,
metadata: {
figmaId: node.id,
}
};

tokens.push(styleToken);
}

Expand Down
22 changes: 21 additions & 1 deletion src/services/variable.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VariableToken } from '../types';
import { rgbaToString } from '../utils/color.utils';

export async function getVariableFallback(variable: Variable | null, propertyName: string = ''): Promise<string> {
Expand Down Expand Up @@ -34,6 +35,25 @@ export async function getVariableFallback(variable: Variable | null, propertyNam
}
}

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;

return {
type: 'variable',
path,
property,
name: variable.name,
value: `$${variable.name}`,
rawValue: await getVariableFallback(variable, property),
metadata: {
figmaId: node.id,
variableId: variable.id,
variableName: variable.name,
}
};
}

function shouldHaveUnits(propertyName: string, value: number): boolean {
const unitlessProperties = ['font-weight', 'opacity'];
const propertyLower = propertyName.toLowerCase();
Expand All @@ -46,4 +66,4 @@ function shouldHaveUnits(propertyName: string, value: number): boolean {
}

return true;
}
}
Loading

0 comments on commit 53477aa

Please sign in to comment.