diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f1d00af --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "image": "node:14", + "containerUser": "node", + "extensions": [ + "editorconfig.editorconfig", + "me-dutour-mathieu.vscode-github-actions" + ], + "mounts": [ + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.npmrc,target=/home/node/.npmrc,type=bind,consistency=cached" + ] +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3f999da --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_size = 2 +indent_style = space \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index cac0e10..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.formatOnSave": true -} \ No newline at end of file diff --git a/custom.d.ts b/custom.d.ts index 8155c1b..afe6050 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -4,7 +4,12 @@ declare module '*.svg' { } declare module '*.css' { - const content: any; + const content: string; + export default content; +} + +declare module '*.scss' { + const content: Record; export default content; } diff --git a/figma.d.ts b/figma.d.ts deleted file mode 100644 index e55f9bd..0000000 --- a/figma.d.ts +++ /dev/null @@ -1,857 +0,0 @@ -// https://www.figma.com/plugin-docs/api/typings/ -// Figma Plugin API version 1, update 14 - -declare global { - // Global variable with Figma's plugin API. - const figma: PluginAPI; - const __html__: string; - - interface PluginAPI { - readonly apiVersion: '1.0.0'; - readonly command: string; - readonly viewport: ViewportAPI; - closePlugin(message?: string): void; - - notify(message: string, options?: NotificationOptions): NotificationHandler; - - showUI(html: string, options?: ShowUIOptions): void; - readonly ui: UIAPI; - - readonly clientStorage: ClientStorageAPI; - - getNodeById(id: string): BaseNode | null; - getStyleById(id: string): BaseStyle | null; - - readonly root: DocumentNode; - currentPage: PageNode; - - on(type: 'selectionchange' | 'currentpagechange' | 'close', callback: () => void): void; - once(type: 'selectionchange' | 'currentpagechange' | 'close', callback: () => void): void; - off(type: 'selectionchange' | 'currentpagechange' | 'close', callback: () => void): void; - - readonly mixed: unique symbol; - - createRectangle(): RectangleNode; - createLine(): LineNode; - createEllipse(): EllipseNode; - createPolygon(): PolygonNode; - createStar(): StarNode; - createVector(): VectorNode; - createText(): TextNode; - createFrame(): FrameNode; - createComponent(): ComponentNode; - createPage(): PageNode; - createSlice(): SliceNode; - /** - * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. - */ - createBooleanOperation(): BooleanOperationNode; - - createPaintStyle(): PaintStyle; - createTextStyle(): TextStyle; - createEffectStyle(): EffectStyle; - createGridStyle(): GridStyle; - - // The styles are returned in the same order as displayed in the UI. Only - // local styles are returned. Never styles from team library. - getLocalPaintStyles(): PaintStyle[]; - getLocalTextStyles(): TextStyle[]; - getLocalEffectStyles(): EffectStyle[]; - getLocalGridStyles(): 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): GroupNode; - flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode; - - union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode; - subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode; - intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode; - exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode; - } - - interface ClientStorageAPI { - getAsync(key: string): Promise; - setAsync(key: string, value: any): Promise; - } - - interface NotificationOptions { - timeout?: number; - } - - interface NotificationHandler { - cancel: () => void; - } - - interface ShowUIOptions { - visible?: boolean; - width?: number; - height?: number; - } - - interface UIPostMessageOptions { - origin?: string; - } - - interface OnMessageProperties { - origin: string; - } - - type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void; - - interface UIAPI { - show(): void; - hide(): void; - resize(width: number, height: number): void; - close(): void; - - postMessage(pluginMessage: any, options?: UIPostMessageOptions): void; - onmessage: MessageEventHandler | undefined; - on(type: 'message', callback: MessageEventHandler): void; - once(type: 'message', callback: MessageEventHandler): void; - off(type: 'message', callback: MessageEventHandler): void; - } - - interface ViewportAPI { - center: Vector; - zoom: number; - scrollAndZoomIntoView(nodes: ReadonlyArray): void; - readonly bounds: Rect; - } - - //////////////////////////////////////////////////////////////////////////////// - // Datatypes - - type Transform = [[number, number, number], [number, number, number]]; - - interface Vector { - readonly x: number; - readonly y: number; - } - - interface Rect { - readonly x: number; - readonly y: number; - readonly width: number; - readonly height: 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 { - readonly exposure?: number; - readonly contrast?: number; - readonly saturation?: number; - readonly temperature?: number; - readonly tint?: number; - readonly highlights?: number; - readonly 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 { - readonly type: 'SCALE' | 'WIDTH' | 'HEIGHT'; - readonly value: number; - } - - interface ExportSettingsImage { - readonly format: 'JPG' | 'PNG'; - readonly contentsOnly?: boolean; // defaults to true - readonly suffix?: string; - readonly constraint?: ExportSettingsConstraints; - } - - interface ExportSettingsSVG { - readonly format: 'SVG'; - readonly contentsOnly?: boolean; // defaults to true - readonly suffix?: string; - readonly svgOutlineText?: boolean; // defaults to true - readonly svgIdAttribute?: boolean; // defaults to false - readonly svgSimplifyStroke?: boolean; // defaults to true - } - - interface ExportSettingsPDF { - readonly format: 'PDF'; - readonly contentsOnly?: boolean; // defaults to true - readonly 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; - - interface 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; - } - - type Reaction = {action: Action; trigger: Trigger}; - - type Action = - | {readonly type: 'BACK' | 'CLOSE'} - | {readonly type: 'URL'; url: string} - | { - readonly type: 'NODE'; - readonly destinationId: string | null; - readonly navigation: Navigation; - readonly transition: Transition | null; - readonly preserveScrollPosition: boolean; - - // Only present if navigation == "OVERLAY" and the destination uses - // overlay position type "RELATIVE" - readonly overlayRelativePosition?: Vector; - }; - - interface SimpleTransition { - readonly type: 'DISSOLVE' | 'SMART_ANIMATE'; - readonly easing: Easing; - readonly duration: number; - } - - interface DirectionalTransition { - readonly type: 'MOVE_IN' | 'MOVE_OUT' | 'PUSH' | 'SLIDE_IN' | 'SLIDE_OUT'; - readonly direction: 'LEFT' | 'RIGHT' | 'TOP' | 'BOTTOM'; - readonly matchLayers: boolean; - - readonly easing: Easing; - readonly duration: number; - } - - type Transition = SimpleTransition | DirectionalTransition; - - type Trigger = - | {readonly type: 'ON_CLICK' | 'ON_HOVER' | 'ON_PRESS' | 'ON_DRAG'} - | {readonly type: 'AFTER_TIMEOUT'; readonly timeout: number} - | {readonly type: 'MOUSE_ENTER' | 'MOUSE_LEAVE' | 'MOUSE_UP' | 'MOUSE_DOWN'; readonly delay: number}; - - type Navigation = 'NAVIGATE' | 'SWAP' | 'OVERLAY'; - - interface Easing { - readonly type: 'EASE_IN' | 'EASE_OUT' | 'EASE_IN_AND_OUT' | 'LINEAR'; - } - - type OverflowDirection = 'NONE' | 'HORIZONTAL' | 'VERTICAL' | 'BOTH'; - - type OverlayPositionType = - | 'CENTER' - | 'TOP_LEFT' - | 'TOP_CENTER' - | 'TOP_RIGHT' - | 'BOTTOM_LEFT' - | 'BOTTOM_CENTER' - | 'BOTTOM_RIGHT' - | 'MANUAL'; - - type OverlayBackground = {readonly type: 'NONE'} | {readonly type: 'SOLID_COLOR'; readonly color: RGBA}; - - type OverlayBackgroundInteraction = 'NONE' | 'CLOSE_ON_CLICK_OUTSIDE'; - - //////////////////////////////////////////////////////////////////////////////// - // 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; - setRelaunchData(data: {[command: string]: /* description */ string}): void; - } - - interface SceneNodeMixin { - visible: boolean; - locked: boolean; - } - - interface ChildrenMixin { - readonly children: ReadonlyArray; - - appendChild(child: SceneNode): void; - insertChild(index: number, child: SceneNode): void; - - findChildren(callback?: (node: SceneNode) => boolean): SceneNode[]; - findChild(callback: (node: SceneNode) => boolean): SceneNode | null; - - /** - * If you only need to search immediate children, it is much faster - * to call node.children.filter(callback) or node.findChildren(callback) - */ - findAll(callback?: (node: SceneNode) => boolean): SceneNode[]; - - /** - * If you only need to search immediate children, it is much faster - * to call node.children.find(callback) or node.findChild(callback) - */ - findOne(callback: (node: SceneNode) => boolean): SceneNode | 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; - constrainProportions: boolean; - - layoutAlign: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH'; // applicable only inside auto-layout frames - - 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 ContainerMixin { - expanded: boolean; - backgrounds: ReadonlyArray; // DEPRECATED: use 'fills' instead - backgroundStyleId: string; // DEPRECATED: use 'fillStyleId' instead - } - - 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 | PluginAPI['mixed']; - strokes: ReadonlyArray; - strokeWeight: number; - strokeMiterLimit: number; - strokeAlign: 'CENTER' | 'INSIDE' | 'OUTSIDE'; - strokeCap: StrokeCap | PluginAPI['mixed']; - strokeJoin: StrokeJoin | PluginAPI['mixed']; - dashPattern: ReadonlyArray; - fillStyleId: string | PluginAPI['mixed']; - strokeStyleId: string; - outlineStroke(): VectorNode | null; - } - - interface CornerMixin { - cornerRadius: number | PluginAPI['mixed']; - cornerSmoothing: number; - } - - interface RectangleCornerMixin { - topLeftRadius: number; - topRightRadius: number; - bottomLeftRadius: number; - bottomRightRadius: number; - } - - interface ExportMixin { - exportSettings: ReadonlyArray; - exportAsync(settings?: ExportSettings): Promise; // Defaults to PNG format - } - - interface ReactionMixin { - readonly reactions: ReadonlyArray; - } - - interface DefaultShapeMixin - extends BaseNodeMixin, - SceneNodeMixin, - ReactionMixin, - BlendMixin, - GeometryMixin, - LayoutMixin, - ExportMixin {} - - interface DefaultFrameMixin - extends BaseNodeMixin, - SceneNodeMixin, - ReactionMixin, - ChildrenMixin, - ContainerMixin, - GeometryMixin, - CornerMixin, - RectangleCornerMixin, - BlendMixin, - ConstraintMixin, - LayoutMixin, - ExportMixin { - layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL'; - counterAxisSizingMode: 'FIXED' | 'AUTO'; // applicable only if layoutMode != "NONE" - horizontalPadding: number; // applicable only if layoutMode != "NONE" - verticalPadding: number; // applicable only if layoutMode != "NONE" - itemSpacing: number; // applicable only if layoutMode != "NONE" - - layoutGrids: ReadonlyArray; - gridStyleId: string; - clipsContent: boolean; - guides: ReadonlyArray; - - overflowDirection: OverflowDirection; - numberOfFixedChildren: number; - - readonly overlayPositionType: OverlayPositionType; - readonly overlayBackground: OverlayBackground; - readonly overlayBackgroundInteraction: OverlayBackgroundInteraction; - } - - //////////////////////////////////////////////////////////////////////////////// - // Nodes - - interface DocumentNode extends BaseNodeMixin { - readonly type: 'DOCUMENT'; - - readonly children: ReadonlyArray; - - appendChild(child: PageNode): void; - insertChild(index: number, child: PageNode): void; - findChildren(callback?: (node: PageNode) => boolean): Array; - findChild(callback: (node: PageNode) => boolean): PageNode | null; - - /** - * If you only need to search immediate children, it is much faster - * to call node.children.filter(callback) or node.findChildren(callback) - */ - findAll(callback?: (node: PageNode | SceneNode) => boolean): Array; - - /** - * If you only need to search immediate children, it is much faster - * to call node.children.find(callback) or node.findChild(callback) - */ - findOne(callback: (node: PageNode | SceneNode) => boolean): PageNode | SceneNode | null; - } - - interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { - readonly type: 'PAGE'; - clone(): PageNode; - - guides: ReadonlyArray; - selection: ReadonlyArray; - selectedTextRange: {node: TextNode; start: number; end: number} | null; - - backgrounds: ReadonlyArray; - - readonly prototypeStartNode: FrameNode | GroupNode | ComponentNode | InstanceNode | null; - } - - interface FrameNode extends DefaultFrameMixin { - readonly type: 'FRAME'; - clone(): FrameNode; - } - - interface GroupNode - extends BaseNodeMixin, - SceneNodeMixin, - ReactionMixin, - ChildrenMixin, - ContainerMixin, - BlendMixin, - LayoutMixin, - ExportMixin { - readonly type: 'GROUP'; - clone(): GroupNode; - } - - interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { - readonly type: 'SLICE'; - clone(): SliceNode; - } - - interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin, RectangleCornerMixin { - readonly type: 'RECTANGLE'; - clone(): RectangleNode; - } - - 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 | PluginAPI['mixed']; - } - - interface TextNode extends DefaultShapeMixin, ConstraintMixin { - readonly type: 'TEXT'; - clone(): TextNode; - 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 | PluginAPI['mixed']; - fontSize: number | PluginAPI['mixed']; - fontName: FontName | PluginAPI['mixed']; - textCase: TextCase | PluginAPI['mixed']; - textDecoration: TextDecoration | PluginAPI['mixed']; - letterSpacing: LetterSpacing | PluginAPI['mixed']; - lineHeight: LineHeight | PluginAPI['mixed']; - - characters: string; - insertCharacters(start: number, characters: string, useStyle?: 'BEFORE' | 'AFTER'): void; - deleteCharacters(start: number, end: number): void; - - getRangeFontSize(start: number, end: number): number | PluginAPI['mixed']; - setRangeFontSize(start: number, end: number, value: number): void; - getRangeFontName(start: number, end: number): FontName | PluginAPI['mixed']; - setRangeFontName(start: number, end: number, value: FontName): void; - getRangeTextCase(start: number, end: number): TextCase | PluginAPI['mixed']; - setRangeTextCase(start: number, end: number, value: TextCase): void; - getRangeTextDecoration(start: number, end: number): TextDecoration | PluginAPI['mixed']; - setRangeTextDecoration(start: number, end: number, value: TextDecoration): void; - getRangeLetterSpacing(start: number, end: number): LetterSpacing | PluginAPI['mixed']; - setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void; - getRangeLineHeight(start: number, end: number): LineHeight | PluginAPI['mixed']; - setRangeLineHeight(start: number, end: number, value: LineHeight): void; - getRangeFills(start: number, end: number): Paint[] | PluginAPI['mixed']; - setRangeFills(start: number, end: number, value: Paint[]): void; - getRangeTextStyleId(start: number, end: number): string | PluginAPI['mixed']; - setRangeTextStyleId(start: number, end: number, value: string): void; - getRangeFillStyleId(start: number, end: number): string | PluginAPI['mixed']; - setRangeFillStyleId(start: number, end: number, value: string): void; - } - - interface ComponentNode extends DefaultFrameMixin { - readonly type: 'COMPONENT'; - clone(): ComponentNode; - - createInstance(): InstanceNode; - description: string; - readonly remote: boolean; - readonly key: string; // The key to use with "importComponentByKeyAsync" - } - - interface InstanceNode extends DefaultFrameMixin { - readonly type: 'INSTANCE'; - clone(): InstanceNode; - masterComponent: ComponentNode; - scaleFactor: number; - } - - interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { - readonly type: 'BOOLEAN_OPERATION'; - clone(): BooleanOperationNode; - booleanOperation: 'UNION' | 'INTERSECT' | 'SUBTRACT' | 'EXCLUDE'; - - expanded: boolean; - } - - type BaseNode = DocumentNode | PageNode | SceneNode; - - type SceneNode = - | SliceNode - | FrameNode - | GroupNode - | 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; - } -} // declare global - -export {}; diff --git a/package.json b/package.json index 0ee0b7e..488ca78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pexels-figma", - "version": "11.0.1", + "version": "2.0.0", "description": "A Figma plugin to insert photos from Pexels.", "homepage": "https://www.figma.com/community/plugin/829802086526281657/Pexels", "license": "ISC", @@ -12,17 +12,26 @@ "author": "Craig Dennis (https://craigmdennis.com)", "dependencies": { "@closeio/use-infinite-scroll": "^1.0.0", + "@pexels/figma": "^2.7.9", + "@pexels/types": "^2.7.9", + "@pexels/utils": "^2.7.9", + "@pexels/icons": "^2.7.9", "babel-plugin-styled-components": "^1.10.7", - "figma-plugin-ds": "^0.1.8", - "react": "^16.8.6", - "react-dom": "^16.8.6", + "framer-motion": "^4.1.17", + "pexels": "^1.2.1", + "qs": "^6.10.1", + "react": "^17.0.0", + "react-dom": "^17.0.0", "react-intersection-observer": "^8.26.1", - "react-loading-skeleton": "^2.0.1" + "react-loading-skeleton": "^2.0.1", + "swr": "^0.5.6" }, "devDependencies": { "@babel/preset-env": "^7.9.5", + "@figma/plugin-typings": "^1.26.0", "@svgr/webpack": "^5.3.1", "@types/node": "^13.13.0", + "@types/qs": "^6.9.6", "@types/react": "^16.8.24", "@types/react-dom": "^16.8.5", "copy-webpack-plugin": "^5.1.1", @@ -33,6 +42,8 @@ "husky": "^3.0.2", "lint-staged": "^9.2.1", "prettier": "^1.18.2", + "sass": "^1.35.1", + "sass-loader": "^10.1.0", "serialize-javascript": "^3.1.0", "style-loader": "^1.2.0", "ts-loader": "^6.0.4", diff --git a/src/api/fetchers.ts b/src/api/fetchers.ts new file mode 100644 index 0000000..5da2233 --- /dev/null +++ b/src/api/fetchers.ts @@ -0,0 +1,92 @@ +import * as qs from 'qs'; +import { KeyLoader, useSWRInfinite } from 'swr'; +import { createClient, ErrorResponse } from 'pexels'; +import { ImageColor, ImageSize, Orientation } from '../constants'; + +type TRawPexelsClient = ReturnType; +type TSearchFn = TRawPexelsClient['photos']['search']; +type TCuratedFn = TRawPexelsClient['photos']['curated']; + +type TPexelsClient = { + photos: { + search: (params: Parameters[0] & { + orientation?: Orientation; + color?: ImageColor; + size?: ImageSize; + }) => ReturnType; + curated: TCuratedFn; + }; +} + +const pexelsClient: TPexelsClient = { + photos: { + search: async (params) => { + const baseUrl = 'https://api.pexels.com/v1/search'; + const apiUrl = `${baseUrl}?${qs.stringify(params)}`; + const response = await fetch(apiUrl, { + headers: { + authorization: `Bearer ${process.env.API_KEY}` + } + }); + return await response.json(); + }, + curated: async (params) => { + const baseUrl = 'https://api.pexels.com/v1/curated'; + const apiUrl = `${baseUrl}?${qs.stringify(params)}`; + const response = await fetch(apiUrl, { + headers: { + authorization: `Bearer ${process.env.API_KEY}` + } + }); + return await response.json(); + } + } +}; + +const isErrorResponse = (input: O | ErrorResponse): input is ErrorResponse => { + if ('error' in input) return true; + return false; +} + +const getKeyFactory = (name: string, listKey: K, transform?: (...args: any[]) => any[]): KeyLoader => { + return (pageIndex: number, previousPageData?: O | null) => { + if (previousPageData && !previousPageData[listKey].length) return null; + const keys = [name, pageIndex]; + return transform ? transform(...keys) : keys; + }; +} + +export const usePhotosSearch = (opts: Parameters[0]) => { + type TPhotosSearchType = ReturnType extends Promise ? V : any; + type FetcherDataType = Exclude; + + const { query, orientation, color, size } = opts; + const keyLoader = getKeyFactory<'photos', FetcherDataType>('photosSearch', 'photos', (...args: any[]) => ([...args, query.replace(/[^a-zA-Z_-]/g, '-'), orientation, color, size])); + const fetcher = async (_: string, pageIndex: number): Promise => { + const res = await pexelsClient.photos.search({ + page: pageIndex + 1, + query: encodeURIComponent(query), + orientation: orientation !== Orientation.ALL ? orientation : undefined, + size: size !== ImageSize.ALL ? size : undefined, + color: color !== ImageColor.ALL ? color : undefined, + }); + if (isErrorResponse(res)) throw new Error(res.error); + return res; + }; + + return useSWRInfinite(keyLoader, fetcher); +} + +export const usePhotosCurated = () => { + type TPhotosCuratedType = ReturnType extends Promise ? V : any; + type FetcherDataType = Exclude; + + const keyLoader = getKeyFactory<'photos', FetcherDataType>('photosSearch', 'photos', (...args: any[]) => ([...args])); + const fetcher = async (_: string, pageIndex: number): Promise => { + const res = await pexelsClient.photos.curated({ page: pageIndex + 1 }); + if (isErrorResponse(res)) throw new Error(res.error); + return res; + }; + + return useSWRInfinite(keyLoader, fetcher); +} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..f1fedd9 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1 @@ +export * from './fetchers'; \ No newline at end of file diff --git a/src/app/components/App.tsx b/src/app/components/App.tsx deleted file mode 100644 index c85fb89..0000000 --- a/src/app/components/App.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import '../styles/ui.css'; -import 'figma-plugin-ds/dist/figma-plugin-ds.css'; -import Footer from './Footer'; -import Gallery from './Gallery'; - -const App = ({}) => { - return ( - - -