Changes
5 changed files (+487/-285)
-
-
@@ -27,9 +27,3 @@ <path d="M1 1L28 0.999999" stroke="#B3B3B3" stroke-width="2"/><path d="M0 27L28 27" stroke="#B3B3B3" stroke-width="2"/> </svg> `; export const BorderRadiusIcon = () => svg` <svg title="vertical padding" width="14" height="14" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M29.0046 2C2.00465 2 2 2.2 2 29" stroke="#B3B3B3" stroke-width="3"/> </svg> `;
-
-
-
@@ -1,272 +0,0 @@import { css, html } from "lit-element"; import * as Figma from "figma-js"; import * as copy from "copy-to-clipboard"; import { BorderRadiusIcon, HorizontalPaddingIcon, VerticalPaddingIcon, CloseIcon, CopyIcon, } from "./Icons"; type Color = { a: number; r: number; g: number; b: number; }; export type FigmaNode = Figma.Node & { name: string; characters: string; background: { color: Color }[]; backgroundColor: Color; fills: { color: Color }[]; absoluteBoundingBox: { height: number; width: number; }; cornerRadius?: number; rectangleCornerRadii?: number[]; horizontalPadding: number; verticalPadding: number; style?: { fontFamily: string; fontSize: number; fontWeight: number; lineHeightPx: number; textAlignHorizontal: string; textAlignVertical: string; }; type: "TEXT" | "INSTANCE" | "FRAME" | "VECTOR" | "RECTANGLE"; }; export type InspectorViewProps = { node: FigmaNode; onClose: () => void; }; /** * Figma returns rbg in values from 0 to 1. We multiply by 255 and truncate them * https://www.figma.com/plugin-docs/api/RGB/ */ const rgbToHex = ({ r, g, b }: { r: number; g: number; b: number }) => "#" + ( (1 << 24) + (Math.trunc(255 * r) << 16) + (Math.trunc(255 * g) << 8) + Math.trunc(255 * b) ) .toString(16) .slice(1); class NodeStyles { background; borderRadius; color; fontFamily; fontSize; fontWeight; height; horizontalPadding; lineHeight; verticalPadding; width; hasStyles = false; hasPadding = false; constructor(node: FigmaNode) { this.height = `${Math.trunc(node.absoluteBoundingBox.height)}px`; this.width = `${Math.trunc(node.absoluteBoundingBox.width)}px`; if (node.horizontalPadding || node.verticalPadding) { this.hasPadding = true; this.horizontalPadding = `${node.horizontalPadding}px`; this.verticalPadding = `${node.verticalPadding}px`; } if (node.style) { this.fontFamily = node.style.fontFamily; this.fontWeight = node.style.fontWeight; this.fontSize = `${node.style.fontSize}px`; this.lineHeight = `${Math.trunc(node.style.lineHeightPx)}px`; } if (node.rectangleCornerRadii) { this.borderRadius = node.rectangleCornerRadii.filter( (radius) => radius === node.cornerRadius ).length < 4 ? `${node.rectangleCornerRadii.join("px, ")}px` : `${node.cornerRadius}px`; } if (node.backgroundColor || node.backgroundColor) { const colors = node.backgroundColor || node.background?.[0].color || node.fills?.[0].color; this.background = rgbToHex(colors); } if (node.fills && node.fills.length > 0) { this.color = rgbToHex(node.fills[0].color); } this.hasStyles = !!( node.style || node.rectangleCornerRadii || this.background || this.color ); } getStyles() { return [ this.height && `height: ${this.height};`, this.width && `width: ${this.width};`, this.fontFamily && `font-family: ${this.fontFamily};`, this.fontSize && `font-size: ${this.fontSize};`, this.fontWeight && `font-weight: ${this.fontWeight};`, this.lineHeight && `line-height: ${this.lineHeight};`, this.borderRadius && `border-radius: ${this.borderRadius};`, this.background && `background: ${this.background};`, this.color && `color: ${this.color};`, ].filter(Boolean) as string[]; } copyStyleSheet() { copy(this.getStyles().join("\n")); } } export const View = ({ node, onClose }: InspectorViewProps) => { if (!node) { return null; } const nodeStyles = new NodeStyles(node); return html` <div class="inspector-view"> <div class="inspector-section title-section"> <h4>${node.name}</h4> ${CloseIcon({ onClick: onClose })} </div> <div class="inspector-section"> <h4>Properties</h4> <p class="inspector-property">W: ${nodeStyles.width}</p> <p class="inspector-property">H: ${nodeStyles.height}</p> ${nodeStyles.borderRadius ? html`<p class="inspector-property"> ${BorderRadiusIcon()} ${nodeStyles.borderRadius} </p>` : null} </div> ${nodeStyles.hasPadding ? html`<div class="inspector-section"> <h4>Layout</h4> ${nodeStyles.horizontalPadding && html`<p class="inspector-property"> ${HorizontalPaddingIcon()} ${nodeStyles.horizontalPadding} </p>`} ${nodeStyles.verticalPadding && html`<p class="inspector-property"> ${VerticalPaddingIcon()} ${nodeStyles.verticalPadding} </p>`} </div>` : null} ${node.characters ? html`<div class="inspector-section"> <div class="title-section"> <h4>Content</h4> ${CopyIcon({ onClick: () => copy(node.characters) })} </div> <p class="node-content node-code">${node.characters}</p> </div>` : null} ${nodeStyles.hasStyles ? StylesSection(nodeStyles) : null} </div> `; }; export const StylesSection = (nodeStyles: NodeStyles) => { const onClick = () => nodeStyles.copyStyleSheet(); const styles = nodeStyles.getStyles(); return html`<div class="inspector-section"> <div class="title-section style-section"> <h4>CSS</h4> ${CopyIcon({ onClick })} </div> <div class="node-code"> ${styles.map( (style) => html`<p class="css-property" @click=${() => copy(style)}>${style}</p>` )} </div> </div> `; }; export const styles = css` .css-property:hover { cursor: pointer; background-color: #e8e8e8; } .style-section { padding: 1rem 0; } .title-section { display: flex; align-items: center; } .title-section svg { cursor: pointer; margin-left: auto; } .node-content { cursor: text; user-select: text; } .node-code { padding: 0.5rem; background: #f3f3f3; font-family: monospace; } .inspector-view { height: 100vh; width: 300px; position: fixed; right: 0; background: white; border-left: 1px solid #ccc; } .inspector-section { padding: 1rem; border-bottom: 1px solid #ccc; } .inspector-section h4 { margin: 0; } .inspector-property { display: flex; align-items: center; margin-bottom: 0; } .inspector-property svg { margin-right: 8px; } `;
-
-
-
@@ -0,0 +1,213 @@import { css, html } from "lit-element"; import * as copy from "copy-to-clipboard"; import { HorizontalPaddingIcon, VerticalPaddingIcon, CloseIcon, CopyIcon, } from "../Icons"; import { FigmaNode, getStyleRule, NodeStyles } from "./utils"; import type { CSSRule } from "./utils"; export type InspectorViewProps = { node: FigmaNode; onClose: () => void; }; export const View = ({ node, onClose }: InspectorViewProps) => { if (!node) { return null; } const nodeStyles = new NodeStyles(node); return html` <div class="inspector-view"> <div class="inspector-section selectable-content"> <div class="title-section"> <h4>${node.name}</h4> ${CloseIcon({ onClick: onClose })} </div> <div class="properties-overview"> <div class="title-section"> <p class="inspector-property"> <span>W: </span>${nodeStyles.width} </p> <p class="inspector-property" style="margin-left: 16px;"> <span>H: </span>${nodeStyles.height} </p> </div> ${nodeStyles.fontPostScriptName ? html`<p class="inspector-property"> <span>Font:</span> ${nodeStyles.fontPostScriptName} </p>` : null} </div> </div> ${nodeStyles.hasPadding ? html`<div class="inspector-section"> <h4>Layout</h4> ${nodeStyles.horizontalPadding && html`<p class="inspector-property"> ${HorizontalPaddingIcon()} ${nodeStyles.horizontalPadding} </p>`} ${nodeStyles.verticalPadding && html`<p class="inspector-property"> ${VerticalPaddingIcon()} ${nodeStyles.verticalPadding} </p>`} </div>` : null} ${node.characters ? html`<div class="inspector-section"> <div class="title-section"> <h4>Content</h4> ${CopyIcon({ onClick: () => copy(node.characters) })} </div> <p class="node-content code-section selectable-content"> ${node.characters} </p> </div>` : null} ${StylesSection(nodeStyles)} </div> `; }; export const StylesSection = (nodeStyles: NodeStyles) => { const onClick = () => copy(nodeStyles.getStyleSheet()); const styles = nodeStyles.getStyles(); return html`<div class="inspector-section"> <div class="title-section style-section"> <h4>CSS</h4> ${CopyIcon({ onClick })} </div> <div class="code-section selectable-content"> ${styles.map(CSSProperty)} </div> </div>`; }; const CSSProperty = (cssProperty: CSSRule) => { const { property, value, color } = cssProperty; let coloredSquare = null; switch (property) { case "background": case "fill": case "border": case "box-shadow": case "color": coloredSquare = html`<span class="color-preview" style="background-color: ${color}" ></span>`; break; case "background-image": coloredSquare = html`<span class="color-preview" style="background-image: ${value}" ></span>`; break; } return html`<div class="css-property" @click=${() => copy(getStyleRule(cssProperty))}> <span>${property}:</span>${coloredSquare}<span class="css-value">${value}</span>;</span> </div>`; }; export const styles = css` .inspector-view { height: 100vh; width: 300px; position: fixed; right: 0; background: white; border-left: 1px solid #ccc; } .inspector-view h4 { font-size: 16px; margin: 0; } .style-section { margin-bottom: 12px; } .title-section { display: flex; align-items: center; } .code-section { padding: 8px; background: #f3f3f3; font-family: monospace; } .title-section svg { cursor: pointer; margin-left: auto; } .inspector-section { padding: 16px; border-bottom: 1px solid #eee; } .properties-overview { font-family: monospace; color: #518785; } .properties-overview p span { color: #121212; } .inspector-property { display: flex; align-items: center; margin-bottom: 0; } .inspector-property span { color: #b3b3b3; margin-right: 4px; } .inspector-property svg { margin-right: 8px; } .css-property { margin: 8px; transition: background-color ease-in-out 100ms; } .css-property:hover { cursor: pointer; background-color: #e8e8e8; } .css-value { color: #518785; margin-left: 4px; } .color-preview { display: inline-block; width: 12px; height: 12px; border: 1px solid #ccc; margin-left: 4px; vertical-align: middle; } .selectable-content { cursor: text; user-select: text; } `;
-
-
-
@@ -0,0 +1,271 @@import * as Figma from "figma-js"; type ElementColor = Figma.Color; type GradientStop = { color: ElementColor; position: number }; type GradientHandlePosition = { x: number; y: number; }; type ElementGradientColor = { gradientHandlePositions: GradientHandlePosition[]; gradientStops: GradientStop[]; }; export type FigmaNode = Figma.Node & { name: string; characters: string; background: { color: ElementColor }[]; backgroundColor: ElementColor; fills: { color: ElementColor }[]; absoluteBoundingBox: { height: number; width: number; }; cornerRadius?: number; rectangleCornerRadii?: number[]; horizontalPadding: number; verticalPadding: number; style?: { fontFamily: string; fontPostScriptName: string; fontSize: number; fontWeight: number; lineHeightPx: number; textAlignHorizontal: string; textAlignVertical: string; }; type: "TEXT" | "INSTANCE" | "FRAME" | "VECTOR" | "RECTANGLE"; }; export type CSSRule = { property: string; value: string; color?: string; }; const extractColorStyle = (color: ElementColor) => { if (color.a === 0) { return "transparent"; } else if (color.a < 1) { return `rgba(${rgbToIntArray(color).join(", ")}, ${color.a.toFixed(2)})`; } else { return rgbToHex(color); } }; const extractGradientColorStyle = (color: ElementGradientColor) => { return new Gradient(color).cssColor; }; export class Gradient { colors; colorObjects; angle; gradientHandles: { start: GradientHandlePosition; end: GradientHandlePosition; }; constructor(data: ElementGradientColor) { this.gradientHandles = { start: data.gradientHandlePositions[0], end: data.gradientHandlePositions[1], }; this.colors = data.gradientStops; this.colorObjects = this.createColorObjects(this.colors); this.angle = this.calculateAngle( this.gradientHandles.start, this.gradientHandles.end ); } get cssGradientArray() { return this.colorObjects.map((color, index) => { const position = this.floatToPercent(this.colors[index].position); return color + " " + position; }); } get cssColor() { const cssGradientArray = this.cssGradientArray; cssGradientArray.unshift(this.angle + "deg"); return `linear-gradient(${cssGradientArray.join(", ")})`; } private createColorObjects(colors: GradientStop[]) { return colors.map(({ color }) => extractColorStyle(color)); } private floatToPercent(value: number) { return (value *= 100).toFixed(0) + "%"; } private calculateAngle( startHandle: GradientHandlePosition, endHandle: GradientHandlePosition ) { const radians = Math.atan(this.calculateGradient(startHandle, endHandle)); return parseInt(this.radToDeg(radians).toFixed(1)); } private calculateGradient( startHandle: GradientHandlePosition, endHandle: GradientHandlePosition ) { return ((endHandle.y - startHandle.y) / (endHandle.x - startHandle.x)) * -1; } private radToDeg(radian: number) { return (180 * radian) / Math.PI; } } export class NodeStyles { background; backgroundImage; border; borderColor; borderRadius; boxShadow; boxShadowColor; color; fontFamily; fontPostScriptName; fontSize; fontWeight; height; horizontalPadding; lineHeight; verticalPadding; width; hasPadding = false; constructor(node: FigmaNode) { this.height = `${Math.trunc(node.absoluteBoundingBox.height)}px`; this.width = `${Math.trunc(node.absoluteBoundingBox.width)}px`; // paddings if (node.horizontalPadding || node.verticalPadding) { this.hasPadding = true; this.horizontalPadding = `${node.horizontalPadding}px`; this.verticalPadding = `${node.verticalPadding}px`; } // font styles if (node.style) { this.fontFamily = node.style.fontFamily; this.fontPostScriptName = node.style.fontPostScriptName?.replace( "-", " " ); this.fontWeight = node.style.fontWeight; this.fontSize = `${Math.ceil(node.style.fontSize)}px`; this.lineHeight = `${Math.trunc(node.style.lineHeightPx)}px`; } // border radii if (node.rectangleCornerRadii) { this.borderRadius = node.rectangleCornerRadii.filter( (radius) => radius === node.cornerRadius ).length < 4 ? `${node.rectangleCornerRadii.join("px ")}px` : `${node.cornerRadius}px`; } // colors, background, fill if (node.backgroundColor || node.backgroundColor) { const color = node.backgroundColor || node.background?.[0].color; this.background = extractColorStyle(color); } const fillColor = node.fills?.[0]; if (fillColor && fillColor.visible !== false) { if (node.type === "TEXT") { this.color = extractColorStyle(fillColor.color); } else if (fillColor.type.includes("GRADIENT")) { this.backgroundImage = extractGradientColorStyle( (fillColor as unknown) as ElementGradientColor ); } else if (fillColor.type === "SOLID") { this.background = extractColorStyle(fillColor.color); } } // borders if (node.strokes && node.strokes.length > 0) { this.borderColor = extractColorStyle( node.strokes[0].color as ElementColor ); this.border = `${node.strokeWeight}px solid ${this.borderColor}`; } // box-shadow if (node.effects && node.effects.length > 0) { const { offset, radius, color } = node.effects[0]; this.boxShadowColor = extractColorStyle(color as Figma.Color); this.boxShadow = `${offset?.x || 0}px ${offset?.y || 0}px 0 ${radius} ${ this.boxShadowColor }`; } } getStyles() { return [ this.height && { property: "height", value: this.height }, this.width && { property: "width", value: this.width }, this.fontFamily && { property: "font-family", value: this.fontFamily }, this.fontSize && { property: "font-size", value: this.fontSize }, this.fontWeight && { property: "font-weight", value: this.fontWeight }, this.lineHeight && { property: "line-height", value: this.lineHeight }, this.borderRadius && { property: "border-radius", value: this.borderRadius, }, this.backgroundImage && { property: "background-image", value: this.backgroundImage, }, this.boxShadow && { property: "box-shadow", value: this.boxShadow, color: this.boxShadowColor, }, this.border && { property: "border", value: this.border, color: this.borderColor, }, this.background && { property: "background", value: this.background, color: this.background, }, this.color && { property: "color", value: this.color, color: this.color }, ].filter(Boolean) as CSSRule[]; } getStyleSheet() { return this.getStyles().map(getStyleRule).join("\n"); } } const rgbToIntArray = (color: ElementColor) => [ Math.trunc(255 * color.r), Math.trunc(255 * color.g), Math.trunc(255 * color.b), ]; const rgbToHex = (color: ElementColor) => { const [r, g, b] = rgbToIntArray(color); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); }; export const getStyleRule = ({ property, value }: CSSRule) => `${property}: ${value};`;
-
-
-
@@ -15,7 +15,8 @@ import { INodeSelectable, NodeSelectableMixin } from "./NodeSelectableMixin";import { Positioned, PositionedMixin } from "./PositionedMixin"; import * as DistanceGuide from "./DistanceGuide"; import * as InspectorView from "./InspectorView"; import * as InspectorView from "./InspectorView/InspectorView"; import type { FigmaNode } from "./InspectorView/utils"; import * as ErrorMessage from "./ErrorMessage"; import * as Node from "./Node";
-
@@ -138,10 +139,6 @@ #flattenedNodes?: readonly SizedNode[];constructor(...args: any[]) { super(...args); // this.addEventListener("click", () => { // this.selectedNode = null; // }); } deselectNode() {
-
@@ -193,7 +190,6 @@ const computedGuideTooltipFontSize = parseFloat(getComputedStyle(this).getPropertyValue("--guide-tooltip-font-size") ); console.log(this.selectedNode); return html` <div> <div
-
@@ -295,7 +291,7 @@ </svg>`} </div> ${InspectorView.View({ node: this.selectedNode as InspectorView.FigmaNode, node: this.selectedNode as FigmaNode, onClose: this.deselectNode, })} </div>
-