diff --git a/change/@adaptive-web-adaptive-ui-designer-core-be44768f-8691-4d5f-a685-254d5d53400b.json b/change/@adaptive-web-adaptive-ui-designer-core-be44768f-8691-4d5f-a685-254d5d53400b.json new file mode 100644 index 00000000..fae9ef23 --- /dev/null +++ b/change/@adaptive-web-adaptive-ui-designer-core-be44768f-8691-4d5f-a685-254d5d53400b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Improve tracking of applied plugin data", + "packageName": "@adaptive-web/adaptive-ui-designer-core", + "email": "47367562+bheston@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@adaptive-web-adaptive-ui-designer-figma-7bed13ad-4844-42eb-9f20-6cd57e65a3c5.json b/change/@adaptive-web-adaptive-ui-designer-figma-7bed13ad-4844-42eb-9f20-6cd57e65a3c5.json new file mode 100644 index 00000000..7dcecabb --- /dev/null +++ b/change/@adaptive-web-adaptive-ui-designer-figma-7bed13ad-4844-42eb-9f20-6cd57e65a3c5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Improve tracking of applied plugin data", + "packageName": "@adaptive-web/adaptive-ui-designer-figma", + "email": "47367562+bheston@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/package-lock.json b/package-lock.json index aaf7c9ac..080fa8fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13585,8 +13585,7 @@ "node_modules/@types/culori": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/culori/-/culori-2.0.0.tgz", - "integrity": "sha512-bKpEra39sQS9UZ+1JoWhuGJEzwKS0dUkNCohVYmn6CAEBkqyIXimKiPDRZWtiOB7sKgkWMaTUpHFimygRoGIlg==", - "dev": true + "integrity": "sha512-bKpEra39sQS9UZ+1JoWhuGJEzwKS0dUkNCohVYmn6CAEBkqyIXimKiPDRZWtiOB7sKgkWMaTUpHFimygRoGIlg==" }, "node_modules/@types/debug": { "version": "4.1.7", @@ -33300,7 +33299,8 @@ "license": "MIT", "dependencies": { "@adaptive-web/adaptive-ui": "^0.12.0", - "@microsoft/fast-foundation": "^3.0.0-alpha.31" + "@microsoft/fast-foundation": "^3.0.0-alpha.31", + "@types/culori": "^2.0.0" }, "devDependencies": { "rimraf": "^3.0.2", @@ -35190,6 +35190,7 @@ "requires": { "@adaptive-web/adaptive-ui": "^0.12.0", "@microsoft/fast-foundation": "^3.0.0-alpha.31", + "@types/culori": "^2.0.0", "rimraf": "^3.0.2", "typescript": "^5.4.5" }, @@ -45651,8 +45652,7 @@ "@types/culori": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/culori/-/culori-2.0.0.tgz", - "integrity": "sha512-bKpEra39sQS9UZ+1JoWhuGJEzwKS0dUkNCohVYmn6CAEBkqyIXimKiPDRZWtiOB7sKgkWMaTUpHFimygRoGIlg==", - "dev": true + "integrity": "sha512-bKpEra39sQS9UZ+1JoWhuGJEzwKS0dUkNCohVYmn6CAEBkqyIXimKiPDRZWtiOB7sKgkWMaTUpHFimygRoGIlg==" }, "@types/debug": { "version": "4.1.7", diff --git a/packages/adaptive-ui-designer-core/package.json b/packages/adaptive-ui-designer-core/package.json index 57f075c9..9fee591d 100644 --- a/packages/adaptive-ui-designer-core/package.json +++ b/packages/adaptive-ui-designer-core/package.json @@ -33,7 +33,8 @@ "homepage": "https://github.com/adaptive-web-community/adaptive-web-components#readme", "dependencies": { "@adaptive-web/adaptive-ui": "^0.12.0", - "@microsoft/fast-foundation": "^3.0.0-alpha.31" + "@microsoft/fast-foundation": "^3.0.0-alpha.31", + "@types/culori": "^2.0.0" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/adaptive-ui-designer-core/src/controller.ts b/packages/adaptive-ui-designer-core/src/controller.ts index f0bb57d9..7231e9d5 100644 --- a/packages/adaptive-ui-designer-core/src/controller.ts +++ b/packages/adaptive-ui-designer-core/src/controller.ts @@ -27,20 +27,20 @@ export abstract class Controller { /** * Gets a Node from the design tool by ID. - * @param id The ID of the node. + * @param id - The ID of the node. * @returns The PluginNode or null if no node by the provided ID exists. */ public abstract getNode(id: string): Promise; /** * Provides the state object to the UI component and updates the UI. - * @param state The UI state object. + * @param state - The UI state object. */ protected abstract sendStateToUI(state: PluginUIState): void; /** * Sets the selected node IDs - Setting the IDs will trigger a UI refresh. - * @param ids The node IDs. + * @param ids - The node IDs. */ public async setSelectedNodes(ids: string[]): Promise { Controller.nodeCount = 0; @@ -60,7 +60,7 @@ export abstract class Controller { /** * Handle the updated state that's posted from the UI. - * @param state The state from the UI. + * @param state - The state from the UI. */ public async receiveStateFromUI(state: PluginUIState): Promise { // console.log("--------------------------------"); diff --git a/packages/adaptive-ui-designer-core/src/index.ts b/packages/adaptive-ui-designer-core/src/index.ts index 97977e31..12ef87b0 100644 --- a/packages/adaptive-ui-designer-core/src/index.ts +++ b/packages/adaptive-ui-designer-core/src/index.ts @@ -14,6 +14,6 @@ export { DesignTokenValues } from "./model.js"; export { mapReplacer, mapReviver, deserializeMap, serializeMap } from "./serialization.js"; -export { State, StatesState, PluginNode, focusIndicatorNodeName, } from "./node.js"; +export { State, StatesState, PluginNode, PluginNodeDataAccessor, focusIndicatorNodeName, } from "./node.js"; export { AdaptiveDesignToken, AdaptiveDesignTokenOrGroup, DesignTokenRegistry } from "./registry/design-token-registry.js"; export { registerAppliableTokens, registerTokens } from "./registry/recipes.js"; diff --git a/packages/adaptive-ui-designer-core/src/model.ts b/packages/adaptive-ui-designer-core/src/model.ts index 50f58c9f..bb41f90c 100644 --- a/packages/adaptive-ui-designer-core/src/model.ts +++ b/packages/adaptive-ui-designer-core/src/model.ts @@ -64,7 +64,7 @@ export class DesignTokenValue { * A token + value pair from an applied design token. */ export class AppliedDesignToken { - constructor(public tokenID: string, public value: string) { + constructor(public tokenID: string) { } } @@ -89,12 +89,12 @@ export type ReadonlyDesignTokenValues = ReadonlyMap; /** * Array of style modules applied to the style of a node. */ -export class AppliedStyleModules extends Array {} +export class AppliedStyleModules extends Array {} // TODO This is more accurate as a Set /** * Readonly Array of style modules applied to the style of a node. */ -export type ReadonlyAppliedStyleModules = ReadonlyArray; +export type ReadonlyAppliedStyleModules = ReadonlyArray; // TODO This is more accurate as a Set /** * Map of design tokens applied to the style of a node. The key is the target style property. diff --git a/packages/adaptive-ui-designer-core/src/node.ts b/packages/adaptive-ui-designer-core/src/node.ts index 4faa1f2d..6afd7d0f 100644 --- a/packages/adaptive-ui-designer-core/src/node.ts +++ b/packages/adaptive-ui-designer-core/src/node.ts @@ -42,8 +42,59 @@ export const StatesState = { export type StatesState = ValuesOf; +/** + * The states for an interactive component. + */ export type State = "Rest" | "Hover" | "Active" | "Focus" | "Disabled"; +/** + * Interface for accessing plugin node data storage. + */ +export interface PluginNodeDataAccessor { + /** + * Gets custom data from the design tool storage. + * @param key - The data storage key. + */ + getPluginData(node: PluginNode, key: K): string | null; + + /** + * Sets custom data to the design tool storage. + * @param key - The data storage key. + * @param value - The new serialized value. + */ + setPluginData(node: PluginNode, key: K, value: string): void; + + /** + * Deletes custom data from the design tool storage. + * @param key - The data storage key. + */ + deletePluginData(node: PluginNode, key: K): void; + + /** + * Gets the local design tokens for a given node, without inherited values. + * @param node - The node to get the local design tokens for. + * @param rawData - The raw data to extract the design tokens from. + * @returns The local design tokens for the node. + */ + getLocalDesignTokens(node: PluginNode): Promise; + + /** + * Gets the local applied design tokens for a given node, without inherited values. + * @param node - The node to get the local applied design tokens for. + * @param rawData - The raw data to extract the applied design tokens from. + * @returns The local applied design tokens for the node. + */ + getAppliedDesignTokens(node: PluginNode): Promise; + + /** + * Gets the local styles for a given node, without inherited values. + * @param node - The node to get the local styles for. + * @param rawData - The raw data to extract the styles from. + * @returns The local styles for the node. + */ + getAppliedStyleModules(node: PluginNode): Promise; +} + /** * The abstract class the plugin Controller interacts with. * Acts as a basic intermediary for node structure and data storage only. @@ -51,6 +102,11 @@ export type State = "Rest" | "Hover" | "Active" | "Focus" | "Disabled"; * for each design tool. */ export abstract class PluginNode { + /** + * Accessor for plugin node data storage. + */ + public static pluginDataAccessor: PluginNodeDataAccessor; + /** * Design tokens inherited by an instance node from the main component. * @@ -116,7 +172,7 @@ export abstract class PluginNode { ...await parent.getInheritedDesignTokens(), ...(parent.componentDesignTokens ? parent.componentDesignTokens - : new DesignTokenValues()), + : []), ...parent.localDesignTokens, ]); } @@ -132,28 +188,50 @@ export abstract class PluginNode { * Gets the applied style modules inherited by an instance node from the main component. */ public get componentAppliedStyleModules(): ReadonlyAppliedStyleModules | undefined { - return this._componentAppliedStyleModules; + const refNode = this.getRefNode(); + const appliedStyleModules = new AppliedStyleModules(); + + if (refNode && refNode.componentAppliedStyleModules) { + appliedStyleModules.push(...refNode.componentAppliedStyleModules); + } + + if (this._componentAppliedStyleModules) { + appliedStyleModules.push(...this._componentAppliedStyleModules); + } + + return appliedStyleModules; } /** * Gets the applied design tokens inherited by an instance node from the main component. */ public get componentAppliedDesignTokens(): ReadonlyAppliedDesignTokens | undefined { - return this._componentAppliedDesignTokens; - } - - /** - * Gets the design tokens set for this node. - */ - public get localDesignTokens(): ReadonlyDesignTokenValues { - return this._localDesignTokens; + const refNode = this.getRefNode(); + const appliedDesignTokens = new AppliedDesignTokens([ + ...(refNode && refNode.componentAppliedDesignTokens + ? refNode.componentAppliedDesignTokens + : []), + ...this._componentAppliedDesignTokens + ? this._componentAppliedDesignTokens + : [], + ]); + return appliedDesignTokens; } /** * Gets the design tokens inherited by an instance node from the main component. */ public get componentDesignTokens(): ReadonlyDesignTokenValues | undefined { - return this._componentDesignTokens; + const refNode = this.getRefNode(); + const designTokens = new DesignTokenValues([ + ...(refNode && refNode.componentDesignTokens + ? refNode.componentDesignTokens + : []), + ...this._componentDesignTokens + ? this._componentDesignTokens + : [], + ]); + return designTokens; } /** @@ -196,6 +274,11 @@ export abstract class PluginNode { */ public abstract getState(): Promise; + /** + * Gets the reference node for this node, if it is part of an instance or composition. + */ + public abstract getRefNode(): PluginNode | null; + /** * Gets whether this type of node can have children or not. */ @@ -226,9 +309,22 @@ export abstract class PluginNode { */ public config: Config = new Config(); - protected deserializeLocalDesignTokens(): DesignTokenValues { - const json = this.getPluginData("designTokens"); - // console.log(" deserializeLocalDesignTokens", this.debugInfo, json); + /** + * Gets the design tokens set for this node. + */ + public get localDesignTokens(): ReadonlyDesignTokenValues { + return this._localDesignTokens; + } + + /** + * Deserializes design tokens set to the node. + * @param json - The raw plugin data string. + * @returns The deserialized design tokens set to the node. + */ + public deserializeLocalDesignTokens(json: string | null): DesignTokenValues { + if (json !== null) { + // console.log(" deserializeLocalDesignTokens", this.debugInfo, json); + } const map: DesignTokenValues = deserializeMap(json); // A future feature of this tooling is to support renaming tokens. For now, use a list for the reference tokens. @@ -243,15 +339,15 @@ export abstract class PluginNode { /** * Sets the design tokens to the node and design tool. - * @param tokens The complete design tokens override map. + * @param tokens - The complete design tokens override map. */ public async setDesignTokens(tokens: DesignTokenValues) { this._localDesignTokens = tokens; if (tokens.size) { const json = serializeMap(tokens); - this.setPluginData("designTokens", json); + PluginNode.pluginDataAccessor.setPluginData(this, "designTokens", json); } else { - this.deletePluginData("designTokens"); + PluginNode.pluginDataAccessor.deletePluginData(this, "designTokens"); } await this.invalidateDesignTokenCache(); @@ -264,9 +360,15 @@ export abstract class PluginNode { return this._appliedDesignTokens; } - protected deserializeAppliedDesignTokens(): AppliedDesignTokens { - const json = this.getPluginData("appliedDesignTokens"); - // console.log(" deserializeAppliedDesignTokens", this.debugInfo, json); + /** + * Deserializes the design tokens applied to the style of this node. + * @param json - The raw plugin data string. + * @returns The deserialized applied design tokens. + */ + public deserializeAppliedDesignTokens(json: string | null): AppliedDesignTokens { + if (json !== null) { + // console.log(" deserializeAppliedDesignTokens", this.debugInfo, json); + } const map: AppliedDesignTokens = deserializeMap(json); // A future feature of this tooling is to support renaming tokens. For now, use a list for the reference tokens. @@ -284,15 +386,15 @@ export abstract class PluginNode { /** * Sets the design tokens applied to the style of this node. - * @param appliedTokens The complete design tokens applied to the style. + * @param appliedTokens - The complete design tokens applied to the style. */ public setAppliedDesignTokens(appliedTokens: AppliedDesignTokens) { this._appliedDesignTokens = appliedTokens; if (appliedTokens.size) { const json = serializeMap(appliedTokens); - this.setPluginData("appliedDesignTokens", json); + PluginNode.pluginDataAccessor.setPluginData(this, "appliedDesignTokens", json); } else { - this.deletePluginData("appliedDesignTokens"); + PluginNode.pluginDataAccessor.deletePluginData(this, "appliedDesignTokens"); } } @@ -303,23 +405,29 @@ export abstract class PluginNode { return this._appliedStyleModules; } - protected deserializeAppliedStyleModules(): AppliedStyleModules { - const json = this.getPluginData("appliedStyleModules"); - // console.log(" deserializeAppliedStyleModules", this.debugInfo, json); + /** + * Deserializes the style modules applied to the style of this node. + * @param json - The raw plugin data string. + * @returns The deserialized applied style modules. + */ + public deserializeAppliedStyleModules(json: string | null): AppliedStyleModules { + if (json !== null) { + // console.log(" deserializeAppliedStyleModules", this.debugInfo, json); + } return JSON.parse(json || "[]"); } /** * Sets the style modules applied to the style of this node. - * @param appliedModules The complete style modules applied to the style. + * @param appliedModules - The complete style modules applied to the style. */ public setAppliedStyleModules(appliedModules: AppliedStyleModules) { this._appliedStyleModules = appliedModules; if (appliedModules.length) { const json = JSON.stringify(appliedModules); - this.setPluginData("appliedStyleModules", json); + PluginNode.pluginDataAccessor.setPluginData(this, "appliedStyleModules", json); } else { - this.deletePluginData("appliedStyleModules"); + PluginNode.pluginDataAccessor.deletePluginData(this, "appliedStyleModules"); } } @@ -355,7 +463,7 @@ export abstract class PluginNode { /** * Updates the style property applied to this node. - * @param values All applied style value. + * @param values - All applied style value. */ public abstract paint(values: AppliedStyleValues): Promise; @@ -383,30 +491,14 @@ export abstract class PluginNode { } } - protected get debugInfo() { + /** + * Gets debug information for this node. + */ + public get debugInfo() { return { id: this.id, type: this.type, name: this.name, }; } - - /** - * Gets custom data from the design tool storage. - * @param key The data storage key. - */ - protected abstract getPluginData(key: K): string | undefined; - - /** - * Sets custom data to the design tool storage. - * @param key The data storage key. - * @param value The new serialized value. - */ - protected abstract setPluginData(key: K, value: string): void; - - /** - * Deletes custom data from the design tool storage. - * @param key The data storage key. - */ - protected abstract deletePluginData(key: K): void; } diff --git a/packages/adaptive-ui-designer-core/src/registry/design-token-registry.ts b/packages/adaptive-ui-designer-core/src/registry/design-token-registry.ts index fb2bb64b..5ea99e44 100644 --- a/packages/adaptive-ui-designer-core/src/registry/design-token-registry.ts +++ b/packages/adaptive-ui-designer-core/src/registry/design-token-registry.ts @@ -10,7 +10,7 @@ export class DesignTokenRegistry value.intendedFor?.includes(target)); diff --git a/packages/adaptive-ui-designer-core/src/serialization.ts b/packages/adaptive-ui-designer-core/src/serialization.ts index 13cbbc58..e602053a 100644 --- a/packages/adaptive-ui-designer-core/src/serialization.ts +++ b/packages/adaptive-ui-designer-core/src/serialization.ts @@ -28,7 +28,7 @@ export function mapReviver(key: string, value: any): any { return value; } -export function deserializeMap(json?: string): Map { +export function deserializeMap(json: string | null): Map { if (json) { try { const map = JSON.parse(json, mapReviver) as Map; diff --git a/packages/adaptive-ui-designer-figma-plugin/src/figma/main.ts b/packages/adaptive-ui-designer-figma-plugin/src/figma/main.ts index 4257e564..457532ec 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/figma/main.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/figma/main.ts @@ -11,7 +11,7 @@ figma.showUI(__html__, { /** * Displays a notification when running a function that takes some time. - * @param callback The function to call + * @param callback - The function to call */ function notifyProcessing(callback: () => void) { const notify = figma.notify("Processing design tokens", { timeout: Infinity }); diff --git a/packages/adaptive-ui-designer-figma-plugin/src/figma/node.ts b/packages/adaptive-ui-designer-figma-plugin/src/figma/node.ts index 8fc368af..784e8c1c 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/figma/node.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/figma/node.ts @@ -1,9 +1,9 @@ import { type Color as CuloriColor, modeLrgb, type Rgb, useMode, wcagLuminance } from "culori/fn"; import { Color, Gradient, Shadow, StyleProperty } from "@adaptive-web/adaptive-ui"; -import { AppliedStyleModules, AppliedStyleValues, Controller, focusIndicatorNodeName, PluginNode, PluginNodeData, State, StatesState, STYLE_REMOVE } from "@adaptive-web/adaptive-ui-designer-core"; -import { FIGMA_SHARED_DATA_NAMESPACE } from "@adaptive-web/adaptive-ui-designer-figma"; -import { colorToRgb, colorToRgba, variantBooleanHelper } from "./utility.js"; +import { AppliedStyleValues, Controller, focusIndicatorNodeName, PluginNode, State, StatesState, STYLE_REMOVE } from "@adaptive-web/adaptive-ui-designer-core"; +import { canHaveChildren, canHaveIndividualStrokes, colorToRgb, colorToRgba, isContainerNode, isInstanceNode, isLineNode, isRectangleNode, isShapeNode, isTextNode, isVectorNode, SOLID_BLACK, SOLID_TRANSPARENT, variantBooleanHelper } from "./utility.js"; import { gradientToGradientPaint } from "./gradient.js"; +import { PluginDataResolver } from "./plugin-data-resolver.js"; // For luminance useMode(modeLrgb); @@ -11,110 +11,6 @@ useMode(modeLrgb); const stateVariant = "State"; const disabledVariant = "Disabled"; -const SOLID_BLACK: SolidPaint = { - type: "SOLID", - visible: true, - opacity: 1, - blendMode: "NORMAL", - color: { - r: 0, - g: 0, - b: 0, - }, -}; - -const SOLID_TRANSPARENT: SolidPaint = { - type: "SOLID", - visible: true, - opacity: 0, - blendMode: "NORMAL", - color: { - r: 1, - g: 1, - b: 1, - }, -}; - -function isNodeType(type: NodeType): (node: BaseNode) => node is T { - return (node: BaseNode): node is T => node.type === type; -} - -const isDocumentNode = isNodeType("DOCUMENT"); -const isPageNode = isNodeType("PAGE"); -const isFrameNode = isNodeType("FRAME"); -const isGroupNode = isNodeType("GROUP"); -const isComponentNode = isNodeType("COMPONENT"); -const isComponentSetNode = isNodeType("COMPONENT_SET"); -const isInstanceNode = isNodeType("INSTANCE"); -const isBooleanOperationNode = isNodeType("BOOLEAN_OPERATION"); -const isVectorNode = isNodeType("VECTOR"); -const isStarNode = isNodeType("STAR"); -const isLineNode = isNodeType("LINE"); -const isEllipseNode = isNodeType("ELLIPSE"); -const isPolygonNode = isNodeType("POLYGON"); -const isRectangleNode = isNodeType("RECTANGLE"); -const isTextNode = isNodeType("TEXT"); - -function isContainerNode(node: BaseNode): node is - FrameNode | - ComponentNode | - InstanceNode { - return [ - isFrameNode, - isComponentNode, - isInstanceNode, - ].some((test: (node: BaseNode) => boolean) => test(node)); -} - -function isShapeNode(node: BaseNode): node is - RectangleNode | - EllipseNode | - PolygonNode | - StarNode | - BooleanOperationNode | - VectorNode { - return [ - isRectangleNode, - isEllipseNode, - isPolygonNode, - isStarNode, - isBooleanOperationNode, - isVectorNode, - ].some((test: (node: BaseNode) => boolean) => test(node)); -} - -function canHaveIndividualStrokes(node: BaseNode): node is - FrameNode | - ComponentNode | - InstanceNode | - RectangleNode { - return [ - isContainerNode, - isRectangleNode, - ].some((test: (node: BaseNode) => boolean) => test(node)); -} - -function canHaveChildren(node: BaseNode): node is - DocumentNode | - PageNode | - FrameNode | - GroupNode | - BooleanOperationNode | - InstanceNode | - ComponentNode | - ComponentSetNode { - return [ - isDocumentNode, - isPageNode, - isFrameNode, - isGroupNode, - isBooleanOperationNode, - isInstanceNode, - isComponentNode, - isComponentSetNode, - ].some((test: (node: BaseNode) => boolean) => test(node)); -} - export class FigmaPluginNode extends PluginNode { public id: string; public type: string; @@ -122,12 +18,17 @@ export class FigmaPluginNode extends PluginNode { public fillColor: CuloriColor | null = null; public states?: StatesState; private _node: BaseNode; + private _refNode: FigmaPluginNode | null = null; private _state?: State; public supportsCodeGen: boolean = false; public codeGenName: string | undefined; private static NodeCache: Map = new Map(); + static { + PluginNode.pluginDataAccessor = new PluginDataResolver(); + } + private constructor(node: BaseNode) { super(); @@ -142,103 +43,45 @@ export class FigmaPluginNode extends PluginNode { } private async init() { - /* - This data model and token processing is to handle an unfortunate consequence of the Figma component model. - - An instance component will inherit plugin data from the main component by default. For instance: - - Main component (set plugin data "A=1") --> Instance (get plugin data "A=1") - - This is mostly beneficial as when it comes to applying design tokens, we want to apply whatever was defined on main. - - However, once you override the value at an instance level, you no longer directly get the value from main: - - Instance (set plugin data "A=2") --> Instance (get plugin data "A=2") - - In our normal workflow we're reevaluating the applied tokens, so the instance may have the same overall structure - but with different values. This would also normally be fine as we're going to evaluate for current values anyway. - - The problem is that once we've updated the values on the instance don't directly get any _changes_ made on the main. - - Main component (set plugin data "A=1, B=2") --> Instance (get plugin data "A=2") - - Notice we don't have the addition of "B=2". It's a simple string and the value is already set, so it doesn't change. - - The solution is to *store* the fully evaluated tokens on the instance node, but to *read* back both the main and instance - values, and to deduplicate them. - - In the example above, we'd read "A=2" from the instance, "A=1, B=2" from the main, then assemble the full list, keeping - any overrides from the instance: "A=2, B=2". - - Refinement: The main component is not the most authoritative, but rather the reference component in the chain. This is - to handle composed nesting, where another component places an instance of a main, overrides a value, and then an instance - of the second component is being evaluated. - - In the case of the "design tokens" the key will be the name of the token, like "corner-radius". - In the case of the "applied design tokens" the key will be the style target, like "backgroundFill". - */ - // Find the reference node if this node is part of an instance or composition. - let refComponentNode: BaseNode | null = null; + let refNode: BaseNode | null = null; if (this.id.startsWith("I")) { // Check this first to handle nested components, we want the overrides on the reference instance before the main component. // Child nodes of an instance have an ID like `I##:##;##:##;##:##` // where each `##:##` points to the containing instance, and the last always points to the main node. const ids = this.id.split(";"); - ids.shift(); - const refId = "I" + ids.join(";"); - // console.log(" looking for ref", refId); - refComponentNode = await figma.getNodeByIdAsync(refId); + // Updated to support more complex nested instances of instances + while (ids.length >= 2 && !refNode) { + ids.shift(); + // First look with the I + const refId = "I" + ids.join(";"); + // console.log(" looking for ref (I)", refId); + refNode = await figma.getNodeByIdAsync(refId); + if (!refNode) { + // Then look without the I + const refId = ids.join(";"); + // console.log(" looking for ref (no I)", refId); + refNode = await figma.getNodeByIdAsync(refId); + } + } } else if (isInstanceNode(this._node)) { - refComponentNode = await (this._node as InstanceNode).getMainComponentAsync(); + // console.log(" getting main component"); + refNode = await this._node.getMainComponentAsync(); } - const deserializedDesignTokens = this.deserializeLocalDesignTokens(); - const parsedAppliedStyleModules = this.deserializeAppliedStyleModules(); - let filteredAppliedStyleModules = parsedAppliedStyleModules; - const deserializedAppliedDesignTokens = this.deserializeAppliedDesignTokens(); - - // Reconcile plugin data with the reference component. - if (refComponentNode) { - // console.log(" getting refComponentNode"); - const refFigmaNode = await FigmaPluginNode.get(refComponentNode, false); - // console.log(" refComponentNode for", this.debugInfo, " is ", refFigmaNode.debugInfo); - - this._componentDesignTokens = refFigmaNode.localDesignTokens; - this._componentAppliedStyleModules = refFigmaNode.appliedStyleModules; - this._componentAppliedDesignTokens = refFigmaNode.appliedDesignTokens; - - refFigmaNode.localDesignTokens.forEach((value, tokenId) => { - // If the token values are the same between the nodes, remove it from the local. - if (deserializedDesignTokens.get(tokenId)?.value === value.value) { - // console.log(" removing design token", this.debugInfo, tokenId); - deserializedDesignTokens.delete(tokenId); - } - }); - - filteredAppliedStyleModules = parsedAppliedStyleModules - .filter((parsedName) => refFigmaNode.appliedStyleModules.indexOf(parsedName) === -1) as AppliedStyleModules; - - refFigmaNode.appliedDesignTokens.forEach((applied, target) => { - // If the target and token are the same between the nodes, remove it from the local. - if (deserializedAppliedDesignTokens.get(target)?.tokenID === applied.tokenID) { - // console.log(" removing applied design token", this.debugInfo, target, applied.tokenID); - deserializedAppliedDesignTokens.delete(target); - } - }); - - if (deserializedDesignTokens.size) { - // console.log(" reconciled design tokens", this.debugInfo, serializeMap(deserializedDesignTokens)); - } + if (refNode) { + // console.log(" getting refNode"); + this._refNode = await FigmaPluginNode.get(refNode, false); + // console.log(" refNode for", this.debugInfo, " is ", this._refNode.debugInfo); - if (deserializedAppliedDesignTokens.size) { - // console.log(" reconciled applied design tokens", this.debugInfo, serializeMap(deserializedAppliedDesignTokens)); - } + this._componentDesignTokens = this._refNode.localDesignTokens; + this._componentAppliedStyleModules = this._refNode.appliedStyleModules; + this._componentAppliedDesignTokens = this._refNode.appliedDesignTokens; } - this._localDesignTokens = deserializedDesignTokens; - this._appliedStyleModules = filteredAppliedStyleModules; - this._appliedDesignTokens = deserializedAppliedDesignTokens; + this._localDesignTokens = await PluginNode.pluginDataAccessor.getLocalDesignTokens(this); + this._appliedStyleModules = await PluginNode.pluginDataAccessor.getAppliedStyleModules(this); + this._appliedDesignTokens = await PluginNode.pluginDataAccessor.getAppliedDesignTokens(this); // Check for and/or remove legacy FAST plugin data. // const fastKeys = this._node.getSharedPluginDataKeys("fast"); @@ -251,10 +94,6 @@ export class FigmaPluginNode extends PluginNode { // }); // } - // if (this._appliedDesignTokens.size) { - // console.log(" final applied design tokens", this._appliedDesignTokens.serialize()); - // } - this.fillColor = this.getFillColor(); this.states = this._node.type === "COMPONENT_SET" ? @@ -270,7 +109,7 @@ export class FigmaPluginNode extends PluginNode { if (this.supportsCodeGen) { switch (this._node.type) { case "INSTANCE": - this.codeGenName = refComponentNode?.name; + this.codeGenName = this._refNode?.name; break; case "COMPONENT": { const parent = await this.getParent(); @@ -318,6 +157,22 @@ export class FigmaPluginNode extends PluginNode { FigmaPluginNode.NodeCache.clear(); } + /** + * Gets the Figma node associated with this plugin node. + * @returns The Figma node + */ + public getFigmaNode(): BaseNode { + return this._node; + } + + /** + * Gets the reference Node for this node, if it is part of an instance or composition. + * @returns The reference Node + */ + public getRefNode(): FigmaPluginNode | null { + return this._refNode; + } + public async getState(): Promise { if (this._state) { return this._state; @@ -527,7 +382,7 @@ export class FigmaPluginNode extends PluginNode { } } - protected async handleFontFamily(node: FigmaPluginNode, values: AppliedStyleValues) { + protected async handleFontFamily(node: FigmaPluginNode, values: AppliedStyleValues, inherited: boolean) { const fontFamily = values.get(StyleProperty.fontFamily)?.value; // We'll only set the font if the family is provided. if (fontFamily) { @@ -550,9 +405,11 @@ export class FigmaPluginNode extends PluginNode { (node._node as TextNode).fontName = fontName; } }); - } else if (isContainerNode(node._node)) { + } else if (isContainerNode(node._node) && !inherited) { + // Only support one layer of children for font family inheritance. + // This may cause problems with the `Text` component, but it breaks other styles by setting too deeply. for (const child of await node.getChildren()) { - await this.handleFontFamily(child, values); + await this.handleFontFamily(child, values, true); } } } @@ -596,7 +453,7 @@ export class FigmaPluginNode extends PluginNode { public async paint(values: AppliedStyleValues): Promise { // Fonts are complicated in Figma, so pull them out of the normal loop. - this.handleFontFamily(this, values); + this.handleFontFamily(this, values, false); this.handleStroke(values); @@ -824,25 +681,6 @@ export class FigmaPluginNode extends PluginNode { return false; } - protected getPluginData(key: K): string | undefined { - let value: string | undefined = this._node.getSharedPluginData(FIGMA_SHARED_DATA_NAMESPACE, key as string); - if (value === "") { - value = undefined; - } - // console.log(" getPluginData", this.debugInfo, key, value); - return value; - } - - protected setPluginData(key: K, value: string): void { - // console.log(" setPluginData", this.debugInfo, key, value); - this._node.setSharedPluginData(FIGMA_SHARED_DATA_NAMESPACE, key, value); - } - - protected deletePluginData(key: K): void { - // console.log(" deletePluginData", this.debugInfo, key); - this._node.setSharedPluginData(FIGMA_SHARED_DATA_NAMESPACE, key, ""); - } - private setBoxSizing() { if (isContainerNode(this._node) && this._node.layoutMode !== "NONE") { (this._node as BaseFrameMixin).strokesIncludedInLayout = true; diff --git a/packages/adaptive-ui-designer-figma-plugin/src/figma/plugin-data-resolver.ts b/packages/adaptive-ui-designer-figma-plugin/src/figma/plugin-data-resolver.ts new file mode 100644 index 00000000..3ec98f71 --- /dev/null +++ b/packages/adaptive-ui-designer-figma-plugin/src/figma/plugin-data-resolver.ts @@ -0,0 +1,261 @@ +import { AppliedDesignTokens, AppliedStyleModules, DesignTokenValues, PluginNodeData, PluginNodeDataAccessor, serializeMap } from "@adaptive-web/adaptive-ui-designer-core"; +import { FIGMA_SHARED_DATA_NAMESPACE } from "@adaptive-web/adaptive-ui-designer-figma"; +import { FigmaPluginNode } from "./node.js"; + +/** + * Represents the plugin data value with the sourceID wrapper. + */ +interface FigmaPluginDataWrapper { + /** + * The actual plugin data payload. + */ + data: string | null; + + /** + * The ID of the node that explicitly set this value. + */ + sourceID: string; +} + +/** + * A universal return type that clearly indicates the schema type and provides the appropriate data structure. + */ +interface OverrideReadResult { + type: "NEW_MODEL" | "LEGACY"; + // For NEW_MODEL, this is the validated delta map. + // For LEGACY, this is the full map requiring deduplication. + data: string | null; +} + +/* +This data model and token processing is to handle an unfortunate consequence of the Figma component model. + +An instance component will inherit plugin data from the main component by default. For instance: + +Main component (set plugin data "A=1") --> Instance (get plugin data "A=1") + +This is mostly beneficial as when it comes to applying design tokens, we want to apply whatever was defined on main. + +However, once you override the value at an instance level, you no longer directly get the value from main: + +Instance (set plugin data "A=2") --> Instance (get plugin data "A=2") + +In our normal workflow we're reevaluating the applied tokens, so the instance may have the same overall structure +but with different values. This would also normally be fine as we're going to evaluate for current values anyway. + +The problem is that once we've updated the values on the instance don't directly get any _changes_ made on the main. + +Main component (set plugin data "A=1, B=2") --> Instance (get plugin data "A=2") + +Notice we don't have the addition of "B=2". It's a simple string and the value is already set, so it doesn't change. + +The solution is to *store* the fully evaluated tokens on the instance node, but to *read* back both the main and instance +values, and to deduplicate them. + +In the example above, we'd read "A=2" from the instance, "A=1, B=2" from the main, then assemble the full list, keeping +any overrides from the instance: "A=2, B=2". + +Refinement: The main component is not the most authoritative, but rather the reference component in the chain. This is +to handle composed nesting, where another component places an instance of a main, overrides a value, and then an instance +of the second component is being evaluated. + +In the case of the "design tokens" the key will be the name of the token, like "corner-radius". +In the case of the "applied design tokens" the key will be the style target, like "backgroundFill". +*/ + +/** + * Utility functions for resolving plugin data for Figma nodes. + */ +export class PluginDataResolver implements PluginNodeDataAccessor { + public getPluginData(node: FigmaPluginNode, key: K): string | null { + let value: string | null = node.getFigmaNode().getSharedPluginData(FIGMA_SHARED_DATA_NAMESPACE, key as string); + if (value === "") { + value = null; + } + if (value !== null) { + // console.log(" getPluginData", node.debugInfo, key, value, typeof value); + } + return value; + } + + public setPluginData(node: FigmaPluginNode, key: K, value: string): void { + // console.log(" setPluginData", node.debugInfo, key, value); + // Handle the empty value case like a delete + const valueToSet = (value == null || value === "") ? "" : JSON.stringify({ + sourceID: node.id, + data: value, + } as FigmaPluginDataWrapper); + node.getFigmaNode().setSharedPluginData(FIGMA_SHARED_DATA_NAMESPACE, key, valueToSet); + } + + public deletePluginData(node: FigmaPluginNode, key: K): void { + // console.log(" deletePluginData", node.debugInfo, key); + node.getFigmaNode().setSharedPluginData(FIGMA_SHARED_DATA_NAMESPACE, key, ""); + } + + /** + * Gets the local design tokens for a given node, without inherited values. + * @param node - The node to get the local design tokens for. + * @param rawData - The raw data to extract the design tokens from. + * @returns The local design tokens for the node. + */ + public async getLocalDesignTokens(node: FigmaPluginNode): Promise { + const rawData = this.getPluginData(node, "designTokens"); + const result = this.getLayerOverride(rawData, node.id); + const deserializedDesignTokens = node.deserializeLocalDesignTokens(result.data); + + if (result.type === "LEGACY") { + const refNode = node.getRefNode(); + if (refNode !== null) { + // console.log(" found ref node, deduplicating local design tokens"); + refNode.localDesignTokens.forEach((value, tokenId) => { + // If the token values are the same between the nodes, remove it from the local. + if (deserializedDesignTokens.get(tokenId)?.value === value.value) { + // console.log(" removing design token", node.debugInfo, tokenId); + deserializedDesignTokens.delete(tokenId); + } + }); + } + + // Write back the cleaned up local data. + const updatedValue = deserializedDesignTokens.size ? serializeMap(deserializedDesignTokens) : ""; + // console.log(" writing back cleaned local design tokens", node.debugInfo, updatedValue); + this.setPluginData(node, "designTokens", updatedValue); + } + + if (deserializedDesignTokens.size) { + // console.log(" local design tokens", node.debugInfo, serializeMap(deserializedDesignTokens)); + } + + return deserializedDesignTokens; + } + + /** + * Gets the local applied design tokens for a given node, without inherited values. + * @param node - The node to get the local applied design tokens for. + * @param rawData - The raw data to extract the applied design tokens from. + * @returns The local applied design tokens for the node. + */ + public async getAppliedDesignTokens(node: FigmaPluginNode): Promise { + const rawData = this.getPluginData(node, "appliedDesignTokens"); + const result = this.getLayerOverride(rawData, node.id); + const deserializedAppliedDesignTokens = node.deserializeAppliedDesignTokens(result.data); + + if (result.type === "LEGACY") { + // There is a historic data issue problem we're cleaning up here. + if (node.name === "Focus indicator" && node.id !== "1548:20317") { + // console.log(" cleaning up errant focus indicator overrides", node.debugInfo); + this.deletePluginData(node, "appliedDesignTokens"); + } else { + const refNode = node.getRefNode(); + if (refNode !== null) { + // console.log(" found ref node, deduplicating applied design tokens"); + refNode.appliedDesignTokens.forEach((applied, target) => { + // If the target and token are the same between the nodes, remove it from the local. + if (deserializedAppliedDesignTokens.get(target)?.tokenID === applied.tokenID) { + // console.log(" removing applied design token", node.debugInfo, target, applied.tokenID); + deserializedAppliedDesignTokens.delete(target); + } + }); + } + + deserializedAppliedDesignTokens.forEach((applied) => { + // console.log(" removing legacy 'value' property"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (applied as any).value; + }); + + // Write back the cleaned up local data. + const updatedValue = deserializedAppliedDesignTokens.size ? serializeMap(deserializedAppliedDesignTokens) : ""; + // console.log(" writing back cleaned applied design tokens", node.debugInfo, updatedValue); + this.setPluginData(node, "appliedDesignTokens", updatedValue); + } + } + + if (deserializedAppliedDesignTokens.size) { + // console.log(" applied design tokens", node.debugInfo, serializeMap(deserializedAppliedDesignTokens)); + } + + return deserializedAppliedDesignTokens; + } + + /** + * Gets the local styles for a given node, without inherited values. + * @param node - The node to get the local styles for. + * @param rawData - The raw data to extract the styles from. + * @returns The local styles for the node. + */ + public async getAppliedStyleModules(node: FigmaPluginNode): Promise { + const rawData = this.getPluginData(node, "appliedStyleModules"); + const result = this.getLayerOverride(rawData, node.id); + const deserializedAppliedStyleModules = node.deserializeAppliedStyleModules(result.data); + let filteredAppliedStyleModules = deserializedAppliedStyleModules + + if (result.type === "LEGACY") { + const refNode = node.getRefNode(); + if (refNode !== null) { + // console.log(" found ref node, deduplicating applied style modules"); + filteredAppliedStyleModules = deserializedAppliedStyleModules + .filter((parsedName) => refNode.appliedStyleModules.indexOf(parsedName) === -1); + } + + // Remove duplicates while preserving order (legacy data bug, should not happen) + filteredAppliedStyleModules.reverse(); + filteredAppliedStyleModules = [...new Set(filteredAppliedStyleModules)]; + filteredAppliedStyleModules.reverse(); + + // Write back the cleaned up local data. + const updatedValue = filteredAppliedStyleModules.length ? JSON.stringify(filteredAppliedStyleModules) : ""; + // console.log(" writing back cleaned applied style modules", node.debugInfo, updatedValue); + this.setPluginData(node, "appliedStyleModules", updatedValue); + } + + if (filteredAppliedStyleModules.length > 0) { + // console.log(" applied style modules", node.debugInfo, filteredAppliedStyleModules.join(",")); + } + + return filteredAppliedStyleModules; + } + + /** + * Implements schema detection and node ID validation. + * @returns The OverrideReadResult for the layer's stored data. + */ + private getLayerOverride(rawData: string | null, nodeID: string): OverrideReadResult { + if (rawData && rawData !== "") { + try { + const data = JSON.parse(rawData); + + // Schema detection and validation + + // Check for the sourceID property, which defines the NEW_MODEL schema + if (typeof (data as FigmaPluginDataWrapper).sourceID === "string" && (data as FigmaPluginDataWrapper).data) { + // console.log(" found data with sourceID", (data as FigmaPluginDataWrapper).sourceID); + + // Potential NEW_MODEL data found + const wrapper = data as FigmaPluginDataWrapper; + + // NEW_MODEL node ID validation + if (wrapper.sourceID === nodeID) { + // console.log(" sourceID matches nodeID"); + // Validated local override + return { type: "NEW_MODEL", data: wrapper.data }; + } + + // NEW_MODEL inherited value (ignore and treat as no local data) + // console.log(" sourceID does not match nodeID, treating as inherited"); + return { type: "NEW_MODEL", data: null }; + } else { + // console.log(" no sourceID found, treating as legacy data"); + // LEGACY: No sourceID property means it's the old schema. + // The legacy data is the entire parsed object. + return { type: "LEGACY", data: rawData }; + } + } catch (e) { + console.warn(`Error inspecting plugin data for node ${nodeID}:`, e); + } + } + + return { type: "NEW_MODEL", data: null }; // Treat as no local data + } +} diff --git a/packages/adaptive-ui-designer-figma-plugin/src/figma/utility.ts b/packages/adaptive-ui-designer-figma-plugin/src/figma/utility.ts index 85d50303..cce8f774 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/figma/utility.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/figma/utility.ts @@ -1,5 +1,109 @@ import { Rgb } from "culori/fn/index.js"; +export const SOLID_BLACK: SolidPaint = { + type: "SOLID", + visible: true, + opacity: 1, + blendMode: "NORMAL", + color: { + r: 0, + g: 0, + b: 0, + }, +}; + +export const SOLID_TRANSPARENT: SolidPaint = { + type: "SOLID", + visible: true, + opacity: 0, + blendMode: "NORMAL", + color: { + r: 1, + g: 1, + b: 1, + }, +}; + +export function isNodeType(type: NodeType): (node: BaseNode) => node is T { + return (node: BaseNode): node is T => node.type === type; +} + +export const isDocumentNode = isNodeType("DOCUMENT"); +export const isPageNode = isNodeType("PAGE"); +export const isFrameNode = isNodeType("FRAME"); +export const isGroupNode = isNodeType("GROUP"); +export const isComponentNode = isNodeType("COMPONENT"); +export const isComponentSetNode = isNodeType("COMPONENT_SET"); +export const isInstanceNode = isNodeType("INSTANCE"); +export const isBooleanOperationNode = isNodeType("BOOLEAN_OPERATION"); +export const isVectorNode = isNodeType("VECTOR"); +export const isStarNode = isNodeType("STAR"); +export const isLineNode = isNodeType("LINE"); +export const isEllipseNode = isNodeType("ELLIPSE"); +export const isPolygonNode = isNodeType("POLYGON"); +export const isRectangleNode = isNodeType("RECTANGLE"); +export const isTextNode = isNodeType("TEXT"); + +export function isContainerNode(node: BaseNode): node is + FrameNode | + ComponentNode | + InstanceNode { + return [ + isFrameNode, + isComponentNode, + isInstanceNode, + ].some((test: (node: BaseNode) => boolean) => test(node)); +} + +export function isShapeNode(node: BaseNode): node is + RectangleNode | + EllipseNode | + PolygonNode | + StarNode | + BooleanOperationNode | + VectorNode { + return [ + isRectangleNode, + isEllipseNode, + isPolygonNode, + isStarNode, + isBooleanOperationNode, + isVectorNode, + ].some((test: (node: BaseNode) => boolean) => test(node)); +} + +export function canHaveIndividualStrokes(node: BaseNode): node is + FrameNode | + ComponentNode | + InstanceNode | + RectangleNode { + return [ + isContainerNode, + isRectangleNode, + ].some((test: (node: BaseNode) => boolean) => test(node)); +} + +export function canHaveChildren(node: BaseNode): node is + DocumentNode | + PageNode | + FrameNode | + GroupNode | + BooleanOperationNode | + InstanceNode | + ComponentNode | + ComponentSetNode { + return [ + isDocumentNode, + isPageNode, + isFrameNode, + isGroupNode, + isBooleanOperationNode, + isInstanceNode, + isComponentNode, + isComponentSetNode, + ].some((test: (node: BaseNode) => boolean) => test(node)); +} + /** * Gets a string representation of `isTrue` based on the format of `booleanFormat`. * diff --git a/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-styles.ts b/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-styles.ts index fc7e856f..954c19a8 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-styles.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-styles.ts @@ -24,7 +24,7 @@ export type StyleModuleDisplayList = Map; export interface AppliedDesignTokenItem { targets: Set; tokenID: string; - values: Set; + // values: Set; // TODO: consider bringing this back } function nameToTitle(name: string): string { @@ -154,7 +154,7 @@ export class StylesController { tokens.push({ targets: new Set([target]), tokenID: applied.tokenID, - values: new Set([applied.value]) + // values: new Set([applied.value]) }); } }); @@ -168,7 +168,7 @@ export class StylesController { if (found) { found.targets = new Set([...found.targets, ...current.targets]); - found.values = new Set([...found.values, ...current.values]); + // found.values = new Set([...found.values, ...current.values]); } else { accumulated.push(current); } @@ -244,7 +244,7 @@ export class StylesController { // console.log("StylesController.applyDesignToken - targets", targets, token); targets.forEach(target => - node.appliedDesignTokens.set(target, new AppliedDesignToken(token.name, null)) + node.appliedDesignTokens.set(target, new AppliedDesignToken(token.name)) ); // console.log(" added applied design token to node", node); @@ -268,7 +268,7 @@ export class StylesController { const applied = node.appliedDesignTokens.get(target); if (applied?.tokenID === tokenID) { // Set to null so we can process the removal - node.appliedDesignTokens.set(target, new AppliedDesignToken(STYLE_REMOVE, STYLE_REMOVE)); + node.appliedDesignTokens.set(target, new AppliedDesignToken(STYLE_REMOVE)); // console.log("--------------------------------"); // console.log(" removed applied design token from node", target, node); } diff --git a/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-tokens.ts b/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-tokens.ts index ae271b77..d1dc1eb7 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-tokens.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller-tokens.ts @@ -76,33 +76,32 @@ export class DesignTokenController { * @returns Applied design tokens. */ private getDesignTokenValues(): UIDesignTokenValue[] { - const tokenValues = new Map>(); - const designTokens: UIDesignTokenValue[] = []; - - this.controller.selectedNodes.forEach(node => - node.designTokens.forEach((designToken, designTokenId) => { - if (designToken.value) { - const values = tokenValues.get(designTokenId) || new Set(); - values.add(designToken.value); - tokenValues.set(designTokenId, values); - } - }) - ); + const tokenValuesMap = this.controller.selectedNodes + .flatMap(node => + Array.from(node.designTokens.entries()).map(([designTokenId, designToken]) => ({ + designTokenId, + value: designToken.value + })) + ) + .filter(item => item.value) + .reduce((acc, item) => { + const values = acc.get(item.designTokenId) || new Set(); + values.add(item.value); + acc.set(item.designTokenId, values); + return acc; + }, new Map>()); const allDesignTokens = this.controller.designTokenRegistry.entries; - allDesignTokens.forEach(token => { - if (tokenValues.has(token.name)) { - const set = tokenValues.get(token.name); - designTokens.push({ - token: token, - value: set.size === 1 ? set.values().next().value : undefined, - multipleValues: set.size > 1 ? [...set].join(", ") : undefined, - }); - } + // This must include all design tokens, not only the ones in the registry. + return Array.from(tokenValuesMap.entries()).map(([tokenName, valueSet]) => { + const token = allDesignTokens.find(t => t.name === tokenName); + return { + token: token || { name: tokenName } as DesignToken, + value: valueSet.size === 1 ? valueSet.values().next().value : undefined, + multipleValues: valueSet.size > 1 ? [...valueSet].join(", ") : undefined, + }; }); - - return designTokens; } private valueToString(value: any): string { diff --git a/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller.ts b/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller.ts index 0e0cc9ed..ed734863 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/ui/ui-controller.ts @@ -237,7 +237,7 @@ export class UIController { const registry = this.appliableDesignTokenRegistry; function appliedDesignTokensHandler(source: AppliedTokenSource): (applied: AppliedDesignToken, target: StyleProperty) => void { return function(applied, target) { - if (applied && applied.value !== STYLE_REMOVE) { + if (applied && applied.tokenID !== STYLE_REMOVE) { const token = registry.get(applied.tokenID); if (token) { if (token instanceof CSSDesignToken) { @@ -247,7 +247,7 @@ export class UIController { source, }); } else if (token instanceof DesignToken) { - console.error("Token is not appliable:", applied.tokenID, node.name, node.type, node.id, applied.value); + console.error("Token is not appliable:", applied.tokenID, node.name, node.type, node.id); } else { const group = (token as InteractiveTokenGroup); if (group && group[state]) { @@ -258,7 +258,7 @@ export class UIController { source, }); } else { - console.warn(" token type not supported >", typeof token, token, applied.tokenID, applied.value); + console.warn(" token type not supported >", typeof token, token, applied.tokenID); } } } else if (applied.tokenID) { @@ -274,10 +274,10 @@ export class UIController { source, }); } else { - console.error("Token not found:", applied.tokenID, node.name, node.type, node.id, applied.value); + console.error("Token not found:", applied.tokenID, node.name, node.type, node.id); } } else { - console.error("Token not found:", applied.tokenID, node.name, node.type, node.id, applied.value); + console.error("Token not found:", applied.tokenID, node.name, node.type, node.id); } } } else { // Removed @@ -427,7 +427,7 @@ export class UIController { node.effectiveAppliedStyleValues.set(target, applied); if (info.source === AppliedTokenSource.local) { - const appliedToken = new AppliedDesignToken(info.name, value); + const appliedToken = new AppliedDesignToken(info.name); node.appliedDesignTokens.set(target, appliedToken); } diff --git a/packages/adaptive-ui-designer-figma-plugin/src/ui/util.ts b/packages/adaptive-ui-designer-figma-plugin/src/ui/util.ts index 08b149bf..83523feb 100644 --- a/packages/adaptive-ui-designer-figma-plugin/src/ui/util.ts +++ b/packages/adaptive-ui-designer-figma-plugin/src/ui/util.ts @@ -2,7 +2,7 @@ import { sentenceCase } from "change-case"; export function designTokenTitle(token?: { name: string }): string { if (token === undefined || token.name === undefined) { - console.log(token); + // console.log(token); return "-"; } diff --git a/packages/adaptive-ui-designer-figma/src/cli/schema-validator.ts b/packages/adaptive-ui-designer-figma/src/cli/schema-validator.ts index f7f5312a..0499b9e7 100644 --- a/packages/adaptive-ui-designer-figma/src/cli/schema-validator.ts +++ b/packages/adaptive-ui-designer-figma/src/cli/schema-validator.ts @@ -6,7 +6,7 @@ const ajv = new Ajv(); export interface ISchemaValidator { /** * Validates a JSON schema against the validator - * @param json the JSON string to validate, or a JavaScript object + * @param json - The JSON string to validate, or a JavaScript object */ validate(json: string | object): Promise; } diff --git a/packages/adaptive-ui-designer-figma/src/lib/node-parser.ts b/packages/adaptive-ui-designer-figma/src/lib/node-parser.ts index 186bb627..5a848ac3 100644 --- a/packages/adaptive-ui-designer-figma/src/lib/node-parser.ts +++ b/packages/adaptive-ui-designer-figma/src/lib/node-parser.ts @@ -28,8 +28,8 @@ function hasChildren(node: T): node is FigmaRestAPI /** * Convert a Figma REST API node to a {@link PluginUINodeData} - * @param node - * @returns + * @param node - The Figma REST API node. + * @returns The corresponding PluginUINodeData. */ export function parseNode(node: FigmaRestAPI.Node): PluginUINodeData { const children = hasChildren(node) ? node.children : [];