diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4d678b --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Fix San Francisco + +**Warning:** This plugin won't fix the city's problems, but it will automatically apply the [correct font variant and tracking](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#font-usage-and-tracking) for selected texts using the San Francisco typeface. + +Usually, you'd use the built-in text styles when designing for iOS. However, if your project needs to use custom font sizes, this plugin's got your back for a more accurate representation of how they're rendered in iOS. + +Inspired by [Sketch-SF-UI-Font-Fixer](https://github.com/kylehickinson/Sketch-SF-UI-Font-Fixer). diff --git a/code.js b/code.js new file mode 100644 index 0000000..6dbc385 --- /dev/null +++ b/code.js @@ -0,0 +1,129 @@ +/** + * Fix San Francisco + * Fixes texts with SF typeface according to Apple's official tracking table: + * https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#font-usage-and-tracking + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +const FONT_DISPLAY = "SF Pro Display"; +const FONT_TEXT = "SF Pro Text"; +const SIZE_MIN = 6; +const SIZE_MAX = 79; +const SIZE_SWAP = 20; +const TRACKING_UNIT = 1000; +// Fills missing font size values inbetween defined values +function fillTracking(arr, min, max) { + let value = 0; + for (let i = min; i < max; i++) { + if (i in arr) { + value = arr[i]; + continue; + } + arr[i] = value; + } + return arr; +} +const TRACKING_DISPLAY = fillTracking({ + 20: 19, + 21: 17, + 22: 16, + 24: 15, + 25: 14, + 27: 13, + 30: 12, + 33: 11, + 40: 10, + 44: 9, + 48: 8, + 50: 7, + 53: 6, + 56: 5, + 60: 4, + 65: 3, + 69: 2 +}, SIZE_SWAP, SIZE_MAX); +const TRACKING_TEXT = fillTracking({ + 6: 41, + 8: 26, + 9: 19, + 10: 12, + 11: 6, + 12: 0, + 13: -6, + 14: -11, + 15: -16, + 16: -20, + 17: -24, + 18: -25, + 19: -26 +}, SIZE_MIN, SIZE_SWAP); +function traverse() { + return __awaiter(this, void 0, void 0, function* () { + const nodes = figma.currentPage.selection; + let modifiedCount = 0; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (node.type !== "TEXT") + continue; + let isModified = false; + let fontFamily = node.fontName.family; + const fontSize = node.fontSize; + const letterSpacing = node.letterSpacing; + if (fontFamily !== FONT_DISPLAY && fontFamily !== FONT_TEXT) + continue; + if (fontFamily === FONT_DISPLAY && fontSize < SIZE_SWAP) { + fontFamily = FONT_TEXT; + isModified = true; + } + if (fontFamily === FONT_TEXT && fontSize >= SIZE_SWAP) { + fontFamily = FONT_DISPLAY; + isModified = true; + } + // Load and assign font family + const fontName = { + family: fontFamily, + style: node.fontName.style + }; + yield figma.loadFontAsync(fontName); + node.fontName = fontName; + // Set tracking + switch (fontFamily) { + case FONT_DISPLAY: + if (fontSize >= SIZE_MAX) { + node.letterSpacing = { + value: 0, + unit: "PIXELS" + }; + break; + } + node.letterSpacing = { + value: (fontSize * TRACKING_DISPLAY[fontSize]) / TRACKING_UNIT, + unit: "PIXELS" + }; + break; + case FONT_TEXT: + node.letterSpacing = { + value: (fontSize * TRACKING_TEXT[Math.max(SIZE_MIN, fontSize)]) / + TRACKING_UNIT, + unit: "PIXELS" + }; + break; + } + // Check if text node is modified + if (JSON.stringify(node.letterSpacing) !== JSON.stringify(letterSpacing)) + isModified = true; + if (isModified) + modifiedCount++; + } + figma.closePlugin(modifiedCount + ? `Updated ${modifiedCount} texts with SF typeface.` + : "No texts were updated."); + }); +} +traverse(); diff --git a/code.ts b/code.ts new file mode 100644 index 0000000..f2d2d4d --- /dev/null +++ b/code.ts @@ -0,0 +1,143 @@ +/** + * Fix San Francisco + * Fixes texts with SF typeface according to Apple's official tracking table: + * https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#font-usage-and-tracking + */ + +const FONT_DISPLAY = "SF Pro Display" +const FONT_TEXT = "SF Pro Text" + +const SIZE_MIN = 6 +const SIZE_MAX = 79 +const SIZE_SWAP = 20 +const TRACKING_UNIT = 1000 + +// Fills missing font size values inbetween defined values +function fillTracking(arr, min, max) { + let value = 0 + for (let i = min; i < max; i++) { + if (i in arr) { + value = arr[i] + continue + } + arr[i] = value + } + return arr +} + +const TRACKING_DISPLAY = fillTracking( + { + 20: 19, + 21: 17, + 22: 16, + 24: 15, + 25: 14, + 27: 13, + 30: 12, + 33: 11, + 40: 10, + 44: 9, + 48: 8, + 50: 7, + 53: 6, + 56: 5, + 60: 4, + 65: 3, + 69: 2 + }, + SIZE_SWAP, + SIZE_MAX +) + +const TRACKING_TEXT = fillTracking( + { + 6: 41, + 8: 26, + 9: 19, + 10: 12, + 11: 6, + 12: 0, + 13: -6, + 14: -11, + 15: -16, + 16: -20, + 17: -24, + 18: -25, + 19: -26 + }, + SIZE_MIN, + SIZE_SWAP +) + +async function traverse() { + const nodes = figma.currentPage.selection + let modifiedCount = 0 + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i] + if (node.type !== "TEXT") continue + + let isModified = false + let fontFamily = (node.fontName as FontName).family + const fontSize = node.fontSize as number + const letterSpacing = node.letterSpacing + + if (fontFamily !== FONT_DISPLAY && fontFamily !== FONT_TEXT) continue + + if (fontFamily === FONT_DISPLAY && fontSize < SIZE_SWAP) { + fontFamily = FONT_TEXT + isModified = true + } + + if (fontFamily === FONT_TEXT && fontSize >= SIZE_SWAP) { + fontFamily = FONT_DISPLAY + isModified = true + } + + // Load and assign font family + const fontName = { + family: fontFamily, + style: (node.fontName as FontName).style + } + await figma.loadFontAsync(fontName) + node.fontName = fontName + + // Set tracking + switch (fontFamily) { + case FONT_DISPLAY: + if (fontSize >= SIZE_MAX) { + node.letterSpacing = { + value: 0, + unit: "PIXELS" + } + break + } + node.letterSpacing = { + value: (fontSize * TRACKING_DISPLAY[fontSize]) / TRACKING_UNIT, + unit: "PIXELS" + } + break + case FONT_TEXT: + node.letterSpacing = { + value: + (fontSize * TRACKING_TEXT[Math.max(SIZE_MIN, fontSize)]) / + TRACKING_UNIT, + unit: "PIXELS" + } + break + } + + // Check if text node is modified + if (JSON.stringify(node.letterSpacing) !== JSON.stringify(letterSpacing)) + isModified = true + if (isModified) modifiedCount++ + } + + figma.closePlugin( + modifiedCount + ? `Updated ${modifiedCount} texts with SF typeface.` + : "No texts were updated." + ) +} + +traverse() diff --git a/figma.d.ts b/figma.d.ts new file mode 100644 index 0000000..db897ee --- /dev/null +++ b/figma.d.ts @@ -0,0 +1,655 @@ +// Global variable with Figma's plugin API. +declare const figma: PluginAPI +declare const __html__: string + +interface PluginAPI { + readonly apiVersion: "1.0.0" + readonly command: string + readonly root: DocumentNode + readonly viewport: ViewportAPI + closePlugin(message?: string): void + + showUI(html: string, options?: ShowUIOptions): void + readonly ui: UIAPI + + readonly clientStorage: ClientStorageAPI + + getNodeById(id: string): BaseNode | null + getStyleById(id: string): BaseStyle | null + + currentPage: PageNode + + readonly mixed: symbol + + createRectangle(): RectangleNode + createLine(): LineNode + createEllipse(): EllipseNode + createPolygon(): PolygonNode + createStar(): StarNode + createVector(): VectorNode + createText(): TextNode + createBooleanOperation(): BooleanOperationNode + createFrame(): FrameNode + createComponent(): ComponentNode + createPage(): PageNode + createSlice(): SliceNode + + createPaintStyle(): PaintStyle + createTextStyle(): TextStyle + createEffectStyle(): EffectStyle + createGridStyle(): GridStyle + + importComponentByKeyAsync(key: string): Promise + importStyleByKeyAsync(key: string): Promise + + listAvailableFontsAsync(): Promise + loadFontAsync(fontName: FontName): Promise + readonly hasMissingFont: boolean + + createNodeFromSvg(svg: string): FrameNode + + createImage(data: Uint8Array): Image + getImageByHash(hash: string): Image + + group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode + flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode +} + +interface ClientStorageAPI { + getAsync(key: string): Promise + setAsync(key: string, value: any): Promise +} + +type ShowUIOptions = { + visible?: boolean, + width?: number, + height?: number, +} + +type UIPostMessageOptions = { + targetOrigin?: string, +} + +type OnMessageProperties = { + sourceOrigin: string, +} + +interface UIAPI { + show(): void + hide(): void + resize(width: number, height: number): void + close(): void + + postMessage(pluginMessage: any, options?: UIPostMessageOptions): void + onmessage: ((pluginMessage: any, props: OnMessageProperties) => void) | undefined +} + +interface ViewportAPI { + center: { x: number, y: number } + zoom: number + scrollAndZoomIntoView(nodes: ReadonlyArray) +} + +//////////////////////////////////////////////////////////////////////////////// +// Datatypes + +type Transform = [ + [number, number, number], + [number, number, number] +] + +interface Vector { + readonly x: number + readonly y: number +} + +interface RGB { + readonly r: number + readonly g: number + readonly b: number +} + +interface RGBA { + readonly r: number + readonly g: number + readonly b: number + readonly a: number +} + +interface FontName { + readonly family: string + readonly style: string +} + +type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" + +type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" + +interface ArcData { + readonly startingAngle: number + readonly endingAngle: number + readonly innerRadius: number +} + +interface ShadowEffect { + readonly type: "DROP_SHADOW" | "INNER_SHADOW" + readonly color: RGBA + readonly offset: Vector + readonly radius: number + readonly visible: boolean + readonly blendMode: BlendMode +} + +interface BlurEffect { + readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" + readonly radius: number + readonly visible: boolean +} + +type Effect = ShadowEffect | BlurEffect + +type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" + +interface Constraints { + readonly horizontal: ConstraintType + readonly vertical: ConstraintType +} + +interface ColorStop { + readonly position: number + readonly color: RGBA +} + +interface ImageFilters { + exposure?: number + contrast?: number + saturation?: number + temperature?: number + tint?: number + highlights?: number + shadows?: number +} + +interface SolidPaint { + readonly type: "SOLID" + readonly color: RGB + + readonly visible?: boolean + readonly opacity?: number + readonly blendMode?: BlendMode +} + +interface GradientPaint { + readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" + readonly gradientTransform: Transform + readonly gradientStops: ReadonlyArray + + readonly visible?: boolean + readonly opacity?: number + readonly blendMode?: BlendMode +} + +interface ImagePaint { + readonly type: "IMAGE" + readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" + readonly imageHash: string | null + readonly imageTransform?: Transform // setting for "CROP" + readonly scalingFactor?: number // setting for "TILE" + readonly filters?: ImageFilters + + readonly visible?: boolean + readonly opacity?: number + readonly blendMode?: BlendMode +} + +type Paint = SolidPaint | GradientPaint | ImagePaint + +interface Guide { + readonly axis: "X" | "Y" + readonly offset: number +} + +interface RowsColsLayoutGrid { + readonly pattern: "ROWS" | "COLUMNS" + readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" + readonly gutterSize: number + + readonly count: number // Infinity when "Auto" is set in the UI + readonly sectionSize?: number // Not set for alignment: "STRETCH" + readonly offset?: number // Not set for alignment: "CENTER" + + readonly visible?: boolean + readonly color?: RGBA +} + +interface GridLayoutGrid { + readonly pattern: "GRID" + readonly sectionSize: number + + readonly visible?: boolean + readonly color?: RGBA +} + +type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid + +interface ExportSettingsConstraints { + type: "SCALE" | "WIDTH" | "HEIGHT" + value: number +} + +interface ExportSettingsImage { + format: "JPG" | "PNG" + contentsOnly?: boolean // defaults to true + suffix?: string + constraint?: ExportSettingsConstraints +} + +interface ExportSettingsSVG { + format: "SVG" + contentsOnly?: boolean // defaults to true + suffix?: string + svgOutlineText?: boolean // defaults to true + svgIdAttribute?: boolean // defaults to false + svgSimplifyStroke?: boolean // defaults to true +} + +interface ExportSettingsPDF { + format: "PDF" + contentsOnly?: boolean // defaults to true + suffix?: string +} + +type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF + +type WindingRule = "NONZERO" | "EVENODD" + +interface VectorVertex { + readonly x: number + readonly y: number + readonly strokeCap?: StrokeCap + readonly strokeJoin?: StrokeJoin + readonly cornerRadius?: number + readonly handleMirroring?: HandleMirroring +} + +interface VectorSegment { + readonly start: number + readonly end: number + readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } + readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } +} + +interface VectorRegion { + readonly windingRule: WindingRule + readonly loops: ReadonlyArray> +} + +interface VectorNetwork { + readonly vertices: ReadonlyArray + readonly segments: ReadonlyArray + readonly regions?: ReadonlyArray // Defaults to [] +} + +interface VectorPath { + readonly windingRule: WindingRule | "NONE" + readonly data: string +} + +type VectorPaths = ReadonlyArray + +type LetterSpacing = { + readonly value: number + readonly unit: "PIXELS" | "PERCENT" +} + +type LineHeight = { + readonly value: number + readonly unit: "PIXELS" | "PERCENT" +} | { + readonly unit: "AUTO" +} + +type BlendMode = + "PASS_THROUGH" | + "NORMAL" | + "DARKEN" | + "MULTIPLY" | + "LINEAR_BURN" | + "COLOR_BURN" | + "LIGHTEN" | + "SCREEN" | + "LINEAR_DODGE" | + "COLOR_DODGE" | + "OVERLAY" | + "SOFT_LIGHT" | + "HARD_LIGHT" | + "DIFFERENCE" | + "EXCLUSION" | + "HUE" | + "SATURATION" | + "COLOR" | + "LUMINOSITY" + +interface Font { + fontName: FontName +} + +//////////////////////////////////////////////////////////////////////////////// +// Mixins + +interface BaseNodeMixin { + readonly id: string + readonly parent: (BaseNode & ChildrenMixin) | null + name: string // Note: setting this also sets `autoRename` to false on TextNodes + readonly removed: boolean + toString(): string + remove(): void + + getPluginData(key: string): string + setPluginData(key: string, value: string): void + + // Namespace is a string that must be at least 3 alphanumeric characters, and should + // be a name related to your plugin. Other plugins will be able to read this data. + getSharedPluginData(namespace: string, key: string): string + setSharedPluginData(namespace: string, key: string, value: string): void +} + +interface SceneNodeMixin { + visible: boolean + locked: boolean +} + +interface ChildrenMixin { + readonly children: ReadonlyArray + + appendChild(child: BaseNode): void + insertChild(index: number, child: BaseNode): void + + findAll(callback?: (node: BaseNode) => boolean): ReadonlyArray + findOne(callback: (node: BaseNode) => boolean): BaseNode | null +} + +interface ConstraintMixin { + constraints: Constraints +} + +interface LayoutMixin { + readonly absoluteTransform: Transform + relativeTransform: Transform + x: number + y: number + rotation: number // In degrees + + readonly width: number + readonly height: number + + resize(width: number, height: number): void + resizeWithoutConstraints(width: number, height: number): void +} + +interface BlendMixin { + opacity: number + blendMode: BlendMode + isMask: boolean + effects: ReadonlyArray + effectStyleId: string +} + +interface FrameMixin { + backgrounds: ReadonlyArray + layoutGrids: ReadonlyArray + clipsContent: boolean + guides: ReadonlyArray + gridStyleId: string + backgroundStyleId: string +} + +type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" +type StrokeJoin = "MITER" | "BEVEL" | "ROUND" +type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" + +interface GeometryMixin { + fills: ReadonlyArray | symbol + strokes: ReadonlyArray + strokeWeight: number + strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" + strokeCap: StrokeCap | symbol + strokeJoin: StrokeJoin | symbol + dashPattern: ReadonlyArray + fillStyleId: string | symbol + strokeStyleId: string +} + +interface CornerMixin { + cornerRadius: number | symbol + cornerSmoothing: number +} + +interface ExportMixin { + exportSettings: ExportSettings[] + exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format +} + +interface DefaultShapeMixin extends + BaseNodeMixin, SceneNodeMixin, + BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { +} + +interface DefaultContainerMixin extends + BaseNodeMixin, SceneNodeMixin, + ChildrenMixin, FrameMixin, + BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { +} + +//////////////////////////////////////////////////////////////////////////////// +// Nodes + +interface DocumentNode extends BaseNodeMixin, ChildrenMixin { + readonly type: "DOCUMENT" +} + +interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { + readonly type: "PAGE" + clone(): PageNode + + guides: ReadonlyArray + selection: ReadonlyArray +} + +interface FrameNode extends DefaultContainerMixin { + readonly type: "FRAME" | "GROUP" + clone(): FrameNode +} + +interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { + readonly type: "SLICE" + clone(): SliceNode +} + +interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { + readonly type: "RECTANGLE" + clone(): RectangleNode + topLeftRadius: number + topRightRadius: number + bottomLeftRadius: number + bottomRightRadius: number +} + +interface LineNode extends DefaultShapeMixin, ConstraintMixin { + readonly type: "LINE" + clone(): LineNode +} + +interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { + readonly type: "ELLIPSE" + clone(): EllipseNode + arcData: ArcData +} + +interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { + readonly type: "POLYGON" + clone(): PolygonNode + pointCount: number +} + +interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { + readonly type: "STAR" + clone(): StarNode + pointCount: number + innerRadius: number +} + +interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { + readonly type: "VECTOR" + clone(): VectorNode + vectorNetwork: VectorNetwork + vectorPaths: VectorPaths + handleMirroring: HandleMirroring | symbol +} + +interface TextNode extends DefaultShapeMixin, ConstraintMixin { + readonly type: "TEXT" + clone(): TextNode + characters: string + readonly hasMissingFont: boolean + textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" + textAlignVertical: "TOP" | "CENTER" | "BOTTOM" + textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" + paragraphIndent: number + paragraphSpacing: number + autoRename: boolean + + textStyleId: string | symbol + fontSize: number | symbol + fontName: FontName | symbol + textCase: TextCase | symbol + textDecoration: TextDecoration | symbol + letterSpacing: LetterSpacing | symbol + lineHeight: LineHeight | symbol + + getRangeFontSize(start: number, end: number): number | symbol + setRangeFontSize(start: number, end: number, value: number): void + getRangeFontName(start: number, end: number): FontName | symbol + setRangeFontName(start: number, end: number, value: FontName): void + getRangeTextCase(start: number, end: number): TextCase | symbol + setRangeTextCase(start: number, end: number, value: TextCase): void + getRangeTextDecoration(start: number, end: number): TextDecoration | symbol + setRangeTextDecoration(start: number, end: number, value: TextDecoration): void + getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol + setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void + getRangeLineHeight(start: number, end: number): LineHeight | symbol + setRangeLineHeight(start: number, end: number, value: LineHeight): void + getRangeFills(start: number, end: number): Paint[] | symbol + setRangeFills(start: number, end: number, value: Paint[]): void + getRangeTextStyleId(start: number, end: number): string | symbol + setRangeTextStyleId(start: number, end: number, value: string): void + getRangeFillStyleId(start: number, end: number): string | symbol + setRangeFillStyleId(start: number, end: number, value: string): void +} + +interface ComponentNode extends DefaultContainerMixin { + readonly type: "COMPONENT" + clone(): ComponentNode + + createInstance(): InstanceNode + description: string + readonly remote: boolean + readonly key: string // The key to use with "importComponentByKeyAsync" +} + +interface InstanceNode extends DefaultContainerMixin { + readonly type: "INSTANCE" + clone(): InstanceNode + masterComponent: ComponentNode +} + +interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { + readonly type: "BOOLEAN_OPERATION" + clone(): BooleanOperationNode + booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" +} + +type BaseNode = + DocumentNode | + PageNode | + SceneNode + +type SceneNode = + SliceNode | + FrameNode | + ComponentNode | + InstanceNode | + BooleanOperationNode | + VectorNode | + StarNode | + LineNode | + EllipseNode | + PolygonNode | + RectangleNode | + TextNode + +type NodeType = + "DOCUMENT" | + "PAGE" | + "SLICE" | + "FRAME" | + "GROUP" | + "COMPONENT" | + "INSTANCE" | + "BOOLEAN_OPERATION" | + "VECTOR" | + "STAR" | + "LINE" | + "ELLIPSE" | + "POLYGON" | + "RECTANGLE" | + "TEXT" + +//////////////////////////////////////////////////////////////////////////////// +// Styles +type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" + +interface BaseStyle { + readonly id: string + readonly type: StyleType + name: string + description: string + remote: boolean + readonly key: string // The key to use with "importStyleByKeyAsync" + remove(): void +} + +interface PaintStyle extends BaseStyle { + type: "PAINT" + paints: ReadonlyArray +} + +interface TextStyle extends BaseStyle { + type: "TEXT" + fontSize: number + textDecoration: TextDecoration + fontName: FontName + letterSpacing: LetterSpacing + lineHeight: LineHeight + paragraphIndent: number + paragraphSpacing: number + textCase: TextCase +} + +interface EffectStyle extends BaseStyle { + type: "EFFECT" + effects: ReadonlyArray +} + +interface GridStyle extends BaseStyle { + type: "GRID" + layoutGrids: ReadonlyArray +} + +//////////////////////////////////////////////////////////////////////////////// +// Other + +interface Image { + readonly hash: string + getBytesAsync(): Promise +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..2109f00 --- /dev/null +++ b/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Fix San Francisco", + "id": "742063658553085504", + "api": "1.0.0", + "main": "code.js" +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6cb6c8a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "es6" + } +}