Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/padding #6

Merged
merged 3 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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