Skip to content

Global templates #30

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

Merged
merged 6 commits into from
Feb 9, 2024
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
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ Translate component variants, properties, and more into dynamic code snippets fo
- [`autolayout`](#autolayout)
- [`component`](#component)
- [`css`](#css)
- [`figma`](#figma)
- [`node`](#node)
- [`property`](#property)
- [`variables`](#variables)
- [Filters](#filters)
- [Global Templates](#global-templates)
- [“Details mode”](#details-mode)
- [Bulk Operations](#bulk-operations)
- [Import/Export](#importexport)
Expand Down Expand Up @@ -308,6 +310,15 @@ Contains the name, type, and child count (when applicable) for the selected node
}
```

For text nodes, `node.characters` will be available. When text styles are applied to the node, `node.textStyle` will be present. Mixed text styles will have a value of [`"figma.mixed"`](https://www.figma.com/plugin-docs/api/properties/figma-mixed/).

```json
{
"node.characters": "hello world",
"node.textStyle": "heading-01"
}
```

### `property`

If the current node is a Component/Instance containing component properties, these will be under the `property.*` params namespace. A basic button component might look like this:
Expand Down Expand Up @@ -387,6 +398,43 @@ figma-is-great
FIGMA_IS_GREAT
```

## Global Templates

Templates can also be stored in Figma's [clientStorage](https://www.figma.com/plugin-docs/api/figma-clientStorage/). This is the only way to store templates for non-component nodes in a way that all nodes can inherit them.

These templates are stored in an object with the following schema:

```json
{
"types": {
"FRAME": [
{
"title": "Sample",
"language": "HTML",
"code": "<p>Hello world! {{node.name}}</p>"
}
]
},
"components": {
"componentKeyABC123": [
{
"title": "Sample React",
"language": "JAVASCRIPT",
"code": "<MyComponent />"
}
]
}
}
```

Check out [./src/index.d.ts](./src/index.d.ts) for documentation on the `CodeSnippetGlobalTemplates` type.

See [./examples.json](./examples.json) for real world examples.

> Important: Figma's client storage is local to the user, device, and Figma context. If you save global templates in the Figma app and then open Figma in the web browser, the templates will not be available.

Syncing this JSON exernally is on the roadmap, but for now, the only way to add global templates is to select "Open Global Template Editor" from the plugin settings menu, paste the JSON into the text box, and hit save.

## “Details Mode”

Details mode can be enabled from the plugin settings menu.
Expand Down
109 changes: 88 additions & 21 deletions code.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ ${indent}`);
}
async function findChildrenSnippets(codegenResult, nodeChildren, indent, recursionIndex, globalTemplates) {
const string = [];
const childPromises = nodeChildren.map(async (child) => {
const childPromises = nodeChildren.map(async (child, index) => {
const paramsMap = await paramsFromNode(child);
const snippets = await nodeSnippetTemplateDataArrayFromNode(
child,
Expand All @@ -255,12 +255,12 @@ ${indent}`);
)
).find(Boolean);
if (snippet) {
string.push(snippet.code);
string[index] = snippet.code;
}
return;
});
await Promise.all(childPromises);
return string.join("\n");
return string.filter(Boolean).join("\n");
}
function lineConditionalMatch(line, params) {
const matches = [...line.matchAll(regexConditional)];
Expand Down Expand Up @@ -400,6 +400,22 @@ ${indent}`);
paramsRaw["node.children"] = childCount;
params["node.children"] = childCount;
}
if (node.type === "TEXT") {
paramsRaw["node.characters"] = node.characters;
params["node.characters"] = safeString(node.characters);
if (node.textStyleId) {
if (node.textStyleId === figma.mixed) {
paramsRaw["node.textStyle"] = "figma.mixed";
params["node.textStyle"] = "figma.mixed";
} else {
const style = figma.getStyleById(node.textStyleId);
if (style) {
paramsRaw["node.textStyle"] = style.name;
params["node.textStyle"] = safeString(style.name);
}
}
}
}
if (componentNode && "key" in componentNode) {
paramsRaw["component.key"] = componentNode.key;
paramsRaw["component.type"] = componentNode.type;
Expand Down Expand Up @@ -561,7 +577,7 @@ ${indent}`);
}
});
const message = {
type: "EXPORT",
type: "BULK_EXPORT",
code: JSON.stringify(data, null, 2)
};
figma.ui.postMessage(message);
Expand Down Expand Up @@ -589,7 +605,7 @@ ${indent}`);
return into;
}, componentData);
const message = {
type: "COMPONENT_DATA",
type: "BULK_COMPONENT_DATA",
code: JSON.stringify(data, null, 2)
};
figma.ui.postMessage(message);
Expand All @@ -604,7 +620,7 @@ ${indent}`);
})
);
const message = {
type: "NODE_DATA",
type: "BULK_NODE_DATA",
code: JSON.stringify(data, null, 2)
};
figma.ui.postMessage(message);
Expand All @@ -627,6 +643,32 @@ ${indent}`);
return data;
}

// src/templates.ts
var CLIENT_STORAGE_GLOBAL_TEMPLATES_KEY = "global-templates";
function templatesIsCodeSnippetGlobalTemplates(templates) {
if (typeof templates === "object" && !Array.isArray(templates)) {
const keys = Object.keys(templates);
if (keys.find((k) => k !== "components" && k !== "types")) {
return false;
}
return true;
}
return false;
}
async function getGlobalTemplatesFromClientStorage() {
const templates = await figma.clientStorage.getAsync(
CLIENT_STORAGE_GLOBAL_TEMPLATES_KEY
);
return templates && templatesIsCodeSnippetGlobalTemplates(templates) ? templates : null;
}
async function setGlobalTemplatesInClientStorage(templates) {
await figma.clientStorage.setAsync(
CLIENT_STORAGE_GLOBAL_TEMPLATES_KEY,
templates
);
return;
}

// src/code.ts
if (figma.mode === "codegen") {
initializeCodegenMode();
Expand All @@ -637,17 +679,27 @@ ${indent}`);
figma.codegen.on("preferenceschange", async (event) => {
if (event.propertyName === "editor") {
openCodeSnippetEditorUI();
} else if (event.propertyName === "templates") {
openTemplateUI();
}
});
figma.ui.on("message", async (event) => {
if (event.type === "INITIALIZE") {
handleCurrentSelection();
} else if (event.type === "SAVE") {
setCodegenResultsInPluginData(figma.currentPage.selection[0], event.data);
} else {
console.log("UNKNOWN EVENT", event);
figma.ui.on(
"message",
async (event) => {
if (event.type === "EDITOR_INITIALIZE") {
handleCurrentSelection();
} else if (event.type === "EDITOR_SAVE") {
setCodegenResultsInPluginData(
figma.currentPage.selection[0],
event.data
);
} else if (event.type === "TEMPLATES_DATA") {
setGlobalTemplatesInClientStorage(event.data);
} else {
console.log("UNKNOWN EVENT", event);
}
}
});
);
figma.on("selectionchange", () => handleCurrentSelection);
figma.codegen.on("generate", async () => {
try {
Expand All @@ -656,7 +708,12 @@ ${indent}`);
const hasDefaultMessage = defaultSnippet === "message";
const currentNode = handleCurrentSelection();
const paramsMap = await paramsFromNode(currentNode);
const nodeSnippetTemplateDataArray = await nodeSnippetTemplateDataArrayFromNode(currentNode, paramsMap, {});
const templates = await getGlobalTemplatesFromClientStorage() || {};
const nodeSnippetTemplateDataArray = await nodeSnippetTemplateDataArrayFromNode(
currentNode,
paramsMap,
templates
);
const snippets = codegenResultsFromNodeSnippetTemplateDataArray(
nodeSnippetTemplateDataArray,
isDetailsMode
Expand Down Expand Up @@ -694,15 +751,15 @@ ${indent}`);
}
function initializeDesignMode() {
figma.ui.on("message", async (event) => {
if (event.type === "INITIALIZE") {
if (event.type === "BULK_INITIALIZE") {
handleCurrentSelection();
} else if (event.type === "COMPONENT_DATA") {
} else if (event.type === "BULK_COMPONENT_DATA") {
bulk.performGetComponentData();
} else if (event.type === "NODE_DATA") {
} else if (event.type === "BULK_NODE_DATA") {
await bulk.performGetNodeData();
} else if (event.type === "EXPORT") {
} else if (event.type === "BULK_EXPORT") {
bulk.performExport();
} else if (event.type === "IMPORT") {
} else if (event.type === "BULK_IMPORT") {
bulk.performImport(event.data);
}
});
Expand All @@ -728,6 +785,16 @@ ${indent}`);
themeColors: true
});
}
async function openTemplateUI() {
figma.showUI(__uiFiles__.templates, {
width: 600,
height: 600,
themeColors: true
});
const templates = await getGlobalTemplatesFromClientStorage() || "{}";
const message = { type: "TEMPLATES_INITIALIZE", templates };
figma.ui.postMessage(message);
}
function codegenResultsFromNodeSnippetTemplateDataArray(nodeSnippetTemplateDataArray, isDetailsMode) {
const codegenResult = [];
nodeSnippetTemplateDataArray.forEach((nodeSnippetTemplateData) => {
Expand All @@ -750,7 +817,7 @@ ${indent}`);
const nodeId = node ? node.id : null;
const nodeType = node ? node.type : null;
const message = {
type: "SELECTION",
type: "EDITOR_SELECTION",
nodeId,
nodeType,
nodePluginData
Expand Down
19 changes: 19 additions & 0 deletions examples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"types": {
"FRAME": [
{
"title": "React",
"language": "JAVASCRIPT",
"code": "<Grid \n direction=\"{{autolayout.layoutMode}}\"\n background={theme.{{variables.fills|camel}}}\n padding=\\{{ \\\n {{?variables.paddingTop}}top: theme.{{variables.paddingTop|camel}},\\\n {{!variables.paddingTop}}top: {{autolayout.paddingTop}},\\\n {{?variables.paddingRight}}right: theme.{{variables.paddingRight|camel}},\\\n {{!variables.paddingRight}}right: {{autolayout.paddingRight}},\\\n {{?variables.paddingBottom}}bottom: theme.{{variables.paddingBottom|camel}},\\\n {{!variables.paddingBottom}}bottom: {{autolayout.paddingBottom}},\\\n {{?variables.paddingLeft}}left: theme.{{variables.paddingLeft|camel}}\\\n {{!variables.paddingLeft}}left: {{autolayout.paddingLeft}}\\\n }}\n {{?variables.itemSpacing}}gap={theme.{{variables.itemSpacing|camel}}}\n {{!variables.itemSpacing}}gap={{{autolayout.itemSpacing}}}\n {{?autolayout.layoutMode=horizontal}}verticalAlign=\"{{autolayout.counterAxisAlignItems}}\"\n {{!autolayout.layoutMode=horizontal}}verticalAlign=\"{{autolayout.primaryAxisAlignItems}}\"\n {{?autolayout.layoutMode=horizontal}}horizontalAlign=\"{{autolayout.primaryAxisAlignItems}}\"\n {{!autolayout.layoutMode=horizontal}}horizontalAlign=\"{{autolayout.counterAxisAlignItems}}\"\n{{?node.children=0}} />\n{{!node.children=0}}>\n {{figma.children}}\n{{!node.children=0}}</Grid>"
}
],
"TEXT": [
{
"title": "React",
"language": "JAVASCRIPT",
"code": "<Typography\\\nvariant=\"{{node.textStyle}}\"\\\n{{!node.textStyle}}variant=\"unknown\"\\\n\\>{{node.characters|raw}}</Typography>"
}
]
},
"components": {}
}
8 changes: 7 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"propertyName": "editor",
"label": "Open Editor"
},
{
"itemType": "action",
"propertyName": "templates",
"label": "Open Global Templates Editor"
},
{
"itemType": "select",
"propertyName": "defaultSnippet",
Expand All @@ -34,7 +39,8 @@
"main": "code.js",
"ui": {
"bulk": "ui/bulk.html",
"editor": "ui/editor.html"
"editor": "ui/editor.html",
"templates": "ui/templates.html"
},
"networkAccess": { "allowedDomains": ["none"] }
}
6 changes: 3 additions & 3 deletions src/bulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function performExport() {
}
});
const message: EventToBulk = {
type: "EXPORT",
type: "BULK_EXPORT",
code: JSON.stringify(data, null, 2),
};
figma.ui.postMessage(message);
Expand Down Expand Up @@ -84,7 +84,7 @@ function performGetComponentData() {
return into;
}, componentData);
const message: EventToBulk = {
type: "COMPONENT_DATA",
type: "BULK_COMPONENT_DATA",
code: JSON.stringify(data, null, 2),
};
figma.ui.postMessage(message);
Expand All @@ -105,7 +105,7 @@ async function performGetNodeData() {
})
);
const message: EventToBulk = {
type: "NODE_DATA",
type: "BULK_NODE_DATA",
code: JSON.stringify(data, null, 2),
};
figma.ui.postMessage(message);
Expand Down
Loading