diff --git a/package-lock.json b/package-lock.json index 294d73a..befb293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3088,6 +3088,10 @@ "resolved": "packages/copy-manager", "link": true }, + "node_modules/figma-export-salt-theme": { + "resolved": "packages/export-salt-theme", + "link": true + }, "node_modules/figma-export-styles": { "resolved": "packages/export-styles", "link": true @@ -4940,10 +4944,9 @@ } }, "node_modules/lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", - "dev": true, + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "bin": { "lz-string": "bin/bin.js" } @@ -6730,6 +6733,20 @@ "@types/papaparse": "^5.3.7" } }, + "packages/export-salt-theme": { + "name": "figma-export-salt-theme", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@salt-ds/core": "^1.7.1", + "@salt-ds/icons": "^1.3.1", + "@salt-ds/lab": "^1.0.0-alpha.9", + "@salt-ds/theme": "^1.5.0", + "lz-string": "^1.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "packages/export-styles": { "name": "figma-export-styles", "version": "0.0.1", @@ -8951,6 +8968,18 @@ "react-dom": "^18.2.0" } }, + "figma-export-salt-theme": { + "version": "file:packages/export-salt-theme", + "requires": { + "@salt-ds/core": "^1.7.1", + "@salt-ds/icons": "^1.3.1", + "@salt-ds/lab": "^1.0.0-alpha.9", + "@salt-ds/theme": "^1.5.0", + "lz-string": "^1.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "figma-export-styles": { "version": "file:packages/export-styles", "requires": { @@ -10347,10 +10376,9 @@ } }, "lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" }, "make-dir": { "version": "3.1.0", diff --git a/packages/export-salt-theme/README.md b/packages/export-salt-theme/README.md new file mode 100644 index 0000000..6a0ccc3 --- /dev/null +++ b/packages/export-salt-theme/README.md @@ -0,0 +1,3 @@ +![Plugin screenshot](docs/export-salt-theme-plugin-screenshot.png) + +Figma plugin to export Salt DS library to JSON object, which can be previewed using code. diff --git a/packages/export-salt-theme/docs/export-salt-theme-plugin-screenshot.png b/packages/export-salt-theme/docs/export-salt-theme-plugin-screenshot.png new file mode 100644 index 0000000..47526fd Binary files /dev/null and b/packages/export-salt-theme/docs/export-salt-theme-plugin-screenshot.png differ diff --git a/packages/export-salt-theme/manifest.json b/packages/export-salt-theme/manifest.json new file mode 100644 index 0000000..03fd09d --- /dev/null +++ b/packages/export-salt-theme/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "Export Salt Theme", + "id": "export-salt-theme", + "api": "1.0.0", + "editorType": ["figma"], + "permissions": [], + "main": "dist/code.js", + "ui": "dist/index.html" +} diff --git a/packages/export-salt-theme/package.json b/packages/export-salt-theme/package.json new file mode 100644 index 0000000..7d60874 --- /dev/null +++ b/packages/export-salt-theme/package.json @@ -0,0 +1,39 @@ +{ + "name": "figma-export-salt-theme", + "version": "0.0.1", + "description": "Figma plugin exports salt theme", + "main": "dist/code.js", + "scripts": { + "tsc": "npm run tsc:main && npm run tsc:ui", + "tsc:main": "tsc --noEmit -p plugin-src", + "tsc:ui": "tsc --noEmit -p ui-src", + "tsc:watch": "concurrently -n widget,iframe,tests \"npm run tsc:main -- --watch --preserveWatchOutput\" \"npm run tsc:ui -- --watch --preserveWatchOutput\" \"npm run tsc:tests -- --watch --preserveWatchOutput\"", + "build": "npm run build:ui && npm run build:main -- --minify", + "build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es6", + "build:ui": "npx vite build --minify esbuild --emptyOutDir=false", + "build:watch": "concurrently -n widget,iframe \"npm run build:main -- --watch\" \"npm run build:ui -- --watch\"", + "dev": "concurrently -n tsc,build,vite 'npm:tsc:watch' 'npm:build:watch' 'vite'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jpmorganchase/Figma-Plugins-and-Widgets.git" + }, + "keywords": [ + "figma-plugin" + ], + "author": "JPMC", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/jpmorganchase/Figma-Plugins-and-Widgets/issues" + }, + "homepage": "https://github.com/jpmorganchase/Figma-Plugins-and-Widgets#readme", + "dependencies": { + "@salt-ds/core": "^1.7.1", + "@salt-ds/icons": "^1.3.1", + "@salt-ds/lab": "^1.0.0-alpha.9", + "@salt-ds/theme": "^1.5.0", + "lz-string": "^1.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/packages/export-salt-theme/plugin-src/code.ts b/packages/export-salt-theme/plugin-src/code.ts new file mode 100644 index 0000000..8432eec --- /dev/null +++ b/packages/export-salt-theme/plugin-src/code.ts @@ -0,0 +1,27 @@ +import { PostToFigmaMessage, PostToUIMessage } from "../shared-src/messages"; +import { generateThemeJson } from "./utils"; + +const WINDOW_MIN_WIDTH = 400; +const WINDOW_MIN_HEIGHT = 500; + +figma.showUI(__html__, { + themeColors: true, + width: WINDOW_MIN_WIDTH, + height: WINDOW_MIN_HEIGHT, +}); + +figma.ui.onmessage = (msg: PostToFigmaMessage) => { + if (msg.type === "ui-ready") { + } else if (msg.type === "generate-json") { + figma.ui.postMessage({ + type: "generate-json-result", + data: generateThemeJson(), + } satisfies PostToUIMessage); + } else if (msg.type === "resize-window") { + const { width, height } = msg; + figma.ui.resize( + Math.max(width, WINDOW_MIN_WIDTH), + Math.max(height, WINDOW_MIN_HEIGHT) + ); + } +}; diff --git a/packages/export-salt-theme/plugin-src/jest.config.js b/packages/export-salt-theme/plugin-src/jest.config.js new file mode 100644 index 0000000..524183f --- /dev/null +++ b/packages/export-salt-theme/plugin-src/jest.config.js @@ -0,0 +1,16 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + transform: { + // Below matches default jest transform (from --debug) + // or a second transform would be applied and tsconfig would be overridden + "^.+\\.tsx?$": [ + "ts-jest", + { + tsconfig: "plugin-src/__tests__/tsconfig.json", + }, + ], + }, + setupFiles: ["/../../../jest/globals.js"], +}; diff --git a/packages/export-salt-theme/plugin-src/tsconfig.json b/packages/export-salt-theme/plugin-src/tsconfig.json new file mode 100644 index 0000000..afac995 --- /dev/null +++ b/packages/export-salt-theme/plugin-src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["es6"], + "strict": true, + "typeRoots": ["../../../node_modules/@figma"] + }, + "include": ["./", "../shared-src"], + "exclude": ["__tests__"] +} diff --git a/packages/export-salt-theme/plugin-src/utils.ts b/packages/export-salt-theme/plugin-src/utils.ts new file mode 100644 index 0000000..32af53e --- /dev/null +++ b/packages/export-salt-theme/plugin-src/utils.ts @@ -0,0 +1,167 @@ +type ThemeObject = any; +type TokenType = any; + +export function setNestedKey(obj: any, path: string[], value: any): any { + if (path.length === 0) { + return obj; + } else if (path.length === 1) { + return { + ...obj, + [path[0]]: value, + }; + } else { + if (obj[path[0]]) { + return { + ...obj, + [path[0]]: setNestedKey(obj[path[0]], path.slice(1), value), + }; + } else { + return { + ...obj, + [path[0]]: setNestedKey({}, path.slice(1), value), + }; + } + } +} + +const KEY_MAP = new Map([["border", "borderColor"]]); + +/** + * https://stackoverflow.com/a/2970667 + **/ +function camelize(str: string) { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { + if (+match === 0) return ""; // or if (/\s+/.test(match)) for white spaces + return index === 0 ? match.toLowerCase() : match.toUpperCase(); + }); +} + +/** Design side has different naming convention than code, need to modify the path to fit. */ +export const fixPathForSalt = (path: string[]) => { + return path.reduce( + (prev, current, index, fullArray) => { + const key = current.toLowerCase(); + + const mappedKeys = key.split(" ").map((x) => KEY_MAP.get(x) || x); + + // Ignore default when at last position + if ( + mappedKeys.length === 1 && + mappedKeys[0] === "default" && + fullArray.length - 1 === index + ) { + return prev; + } else { + return [...prev, ...mappedKeys]; + } + }, + // prefix salt as top level namespace if not existed + path[0] === "salt" ? [] : ["salt"] + ); +}; + +export const updateTheme = ( + theme: ThemeObject, + newToken: TokenType, + path: string[] +): ThemeObject => { + let newTheme = { ...theme }; + + const newPath = fixPathForSalt(path); + + console.log({ newPath }); + + newTheme = setNestedKey(newTheme, newPath, newToken); + + return newTheme; +}; + +// Refer to MDN font-weight page for more information. +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping +export const FONT_WEIGHT_MAPPING: { [key: string]: number } = { + Thin: 100, + "Extra Light": 200, + ExtraLight: 200, + Light: 300, + Regular: 400, + Medium: 500, + "Semi Bold": 600, + SemiBold: 600, + Semibold: 600, + Bold: 700, + "Extra Bold": 800, + ExtraBold: 800, + Extrabold: 800, + Black: 900, +}; + +// Try convert to valid CSS line height, see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height +function extractLineHeight(lineHeight: LineHeight): string { + switch (lineHeight.unit) { + case "AUTO": { + return "normal"; + } + case "PERCENT": { + return Math.round(lineHeight.value) + "%"; + } + case "PIXELS": { + return Math.round(lineHeight.value) + "px"; + } + } + return "normal"; +} + +export function generateThemeJson(): ThemeObject { + // Just support raw color without reference for now + + let newTheme: ThemeObject = {}; + + const localPaintStyles = figma.getLocalPaintStyles(); + for (let index = 0; index < localPaintStyles.length; index++) { + const paintStyle = localPaintStyles[index]; + if ( + paintStyle.paints.length === 1 && + paintStyle.paints[0].type === "SOLID" + ) { + const objPaths = paintStyle.name.split("/"); + + const { color, opacity } = paintStyle.paints[0]; + + const newColorToken = { + $type: "color", + $value: { + r: Math.round(color.r * 255), + g: Math.round(color.g * 255), + b: Math.round(color.b * 255), + a: opacity ? Math.round(opacity * 100) / 100 : undefined, + }, + }; + + newTheme = updateTheme(newTheme, newColorToken as any, objPaths); + } + } + + const localTextStyles = figma.getLocalTextStyles(); + for (let index = 0; index < localTextStyles.length; index++) { + const textStyle = localTextStyles[index]; + + // NOTE: not all information is extracted + const { fontName, fontSize, lineHeight } = textStyle; + const { family, style } = fontName; + + const newTypographyToken = { + $type: "typography", + $value: { + fontFamily: family, + fontWeight: FONT_WEIGHT_MAPPING[style] || 400, // default to 400 / regular + fontSize: fontSize + "px", // Figma fontSize is unit-less + lineHeight: extractLineHeight(lineHeight), + }, + }; + + const objPaths = textStyle.name.split("/"); + newTheme = updateTheme(newTheme, newTypographyToken as any, objPaths); + } + + return newTheme; +} diff --git a/packages/export-salt-theme/shared-src/index.ts b/packages/export-salt-theme/shared-src/index.ts new file mode 100644 index 0000000..c2e7ea9 --- /dev/null +++ b/packages/export-salt-theme/shared-src/index.ts @@ -0,0 +1 @@ +export * from "./messages"; diff --git a/packages/export-salt-theme/shared-src/messages.ts b/packages/export-salt-theme/shared-src/messages.ts new file mode 100644 index 0000000..1ce95d2 --- /dev/null +++ b/packages/export-salt-theme/shared-src/messages.ts @@ -0,0 +1,25 @@ +export type GenerateJsonResultToUIMessage = { + type: "generate-json-result"; + data: any; +}; + +export type PostToUIMessage = GenerateJsonResultToUIMessage; + +export type GenerateJsonToFigmaMessage = { + type: "generate-json"; +}; + +export type UIRedayToFigmaMessage = { + type: "ui-ready"; +}; + +export type ResizeWindowToFigmaMessage = { + type: "resize-window"; + width: number; + height: number; +}; + +export type PostToFigmaMessage = + | GenerateJsonToFigmaMessage + | UIRedayToFigmaMessage + | ResizeWindowToFigmaMessage; diff --git a/packages/export-salt-theme/ui-src/App.css b/packages/export-salt-theme/ui-src/App.css new file mode 100644 index 0000000..3f7d9bd --- /dev/null +++ b/packages/export-salt-theme/ui-src/App.css @@ -0,0 +1,23 @@ +#root, +.appRoot, +.saltFlexLayout { + height: 100%; +} + +body { + background: var(--figma-color-bg); + font-family: "Open Sans"; +} + +.figma-light { + color-scheme: light; +} + +.figma-dark { + color-scheme: dark; +} + +.appRoot textarea { + resize: none; + flex: 1; +} diff --git a/packages/export-salt-theme/ui-src/App.tsx b/packages/export-salt-theme/ui-src/App.tsx new file mode 100644 index 0000000..a9e9cd4 --- /dev/null +++ b/packages/export-salt-theme/ui-src/App.tsx @@ -0,0 +1,34 @@ +import { SaltProvider } from "@salt-ds/core"; +import React, { useEffect } from "react"; +import { PostToFigmaMessage } from "../shared-src"; +import { CornerResizer } from "./components/CornerResizer"; +import { useFigmaPluginTheme } from "./components/hooks"; +import { ExportJsonView } from "./views/ExportJsonView"; + +import "./App.css"; + +function App() { + const [theme] = useFigmaPluginTheme(); + + useEffect(() => { + parent.postMessage( + { + pluginMessage: { + type: "ui-ready", + } as PostToFigmaMessage, + }, + "*" + ); + }, []); + + return ( + +
+ + +
+
+ ); +} + +export default App; diff --git a/packages/export-salt-theme/ui-src/components/CornerResizer.css b/packages/export-salt-theme/ui-src/components/CornerResizer.css new file mode 100644 index 0000000..dc8532c --- /dev/null +++ b/packages/export-salt-theme/ui-src/components/CornerResizer.css @@ -0,0 +1,6 @@ +#corner { + position: absolute; + right: 1px; + bottom: 2px; + cursor: nwse-resize; +} diff --git a/packages/export-salt-theme/ui-src/components/CornerResizer.tsx b/packages/export-salt-theme/ui-src/components/CornerResizer.tsx new file mode 100644 index 0000000..97cc5a4 --- /dev/null +++ b/packages/export-salt-theme/ui-src/components/CornerResizer.tsx @@ -0,0 +1,85 @@ +import React, { PointerEventHandler, useState } from "react"; + +import "./CornerResizer.css"; + +/** + * Helper to resize the Plugin window. + * + * Use it by adding to UI: + * ``` + * import { CornerResizer } from "@shared-util/ui"; + * ... + * + * ``` + * + * And to figma message listener: + * ``` + * const MIN_WIDTH = 455; + * const MIN_HEIGHT = 300; + * ... + * case "resize-window": { + * const { width, height } = msg; + * figma.ui.resize(Math.max(width, MIN_WIDTH), Math.max(height, MIN_HEIGHT)); + * break; + * } + * ``` + * + * Shared message type if needed: + * ``` + * export type ResizeWindowToFigmaMessage = { + * type: "resize-window"; + * width: number; + * height: number; + * }; + * ``` + */ +export const CornerResizer = () => { + const [isDragging, setIsDragging] = useState(false); + const handlePointerDown: PointerEventHandler = (e) => { + setIsDragging(true); + (e.target as SVGSVGElement).setPointerCapture(e.pointerId); + }; + const handlePointerUp: PointerEventHandler = (e) => { + setIsDragging(false); + (e.target as SVGSVGElement).releasePointerCapture(e.pointerId); + }; + const resizeWindow: PointerEventHandler = (e) => { + if (!isDragging) return; + + const size = { + width: Math.max(50, Math.floor(e.clientX + 5)), + height: Math.max(50, Math.floor(e.clientY + 5)), + }; + parent.postMessage( + { + pluginMessage: { + type: "resize-window", + ...size, + }, + }, + "*" + ); + }; + + return ( + + + + + ); +}; diff --git a/packages/export-salt-theme/ui-src/components/hooks.ts b/packages/export-salt-theme/ui-src/components/hooks.ts new file mode 100644 index 0000000..9bbb758 --- /dev/null +++ b/packages/export-salt-theme/ui-src/components/hooks.ts @@ -0,0 +1,71 @@ +import { useEffect, useCallback, useState } from "react"; + +export const useFigmaPluginTheme = ( + defaultTheme: "light" | "dark" = "light" +) => { + const [theme, setTheme] = useState(defaultTheme); + + useEffect(() => { + // Support Figma dark theme - https://www.figma.com/plugin-docs/css-variables/ + if (document.querySelector("html")?.classList.contains("figma-dark")) { + setTheme("dark"); + } else { + setTheme("light"); + } + + setTimeout(() => { + // When using plugin on LVDI, it's slow to get the correct class name detected + if (document.querySelector("html")?.classList.contains("figma-dark")) { + setTheme("dark"); + } else { + setTheme("light"); + } + }, 100); + }, []); + + const onMutation = useCallback((mutationList) => { + console.log( + "onMutation", + mutationList, + (mutationList[0].target as any).classList.value + ); + const theme = (mutationList[0].target as any).classList.contains( + "figma-dark" + ) + ? "dark" + : "light"; + setTheme(theme); + }, []); + useMutationObservable(document.querySelector("html")!, onMutation); + + console.log("useFigmaPluginTheme", { theme }); + + return [theme, setTheme] as const; +}; + +const DEFAULT_OPTIONS = { + config: { attributes: true, childList: false, subtree: false }, +}; +function useMutationObservable( + targetEl: Node, + cb: MutationCallback, + options = DEFAULT_OPTIONS +) { + const [observer, setObserver] = useState(null); + + useEffect(() => { + const obs = new MutationObserver(cb); + setObserver(obs); + }, [cb, options, setObserver]); + + useEffect(() => { + if (!observer) return; + const { config } = options; + observer.observe(targetEl, config); + return () => { + if (observer) { + observer.disconnect(); + } + }; + }, [observer, targetEl, options]); +} diff --git a/packages/export-salt-theme/ui-src/components/icons/AvocadoIcon.tsx b/packages/export-salt-theme/ui-src/components/icons/AvocadoIcon.tsx new file mode 100644 index 0000000..6f7816f --- /dev/null +++ b/packages/export-salt-theme/ui-src/components/icons/AvocadoIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Icon, IconProps } from "@salt-ds/icons"; + +export const AvocadoIcon = (props: IconProps) => { + return ( + + + + + + ); +}; diff --git a/packages/export-salt-theme/ui-src/index.html b/packages/export-salt-theme/ui-src/index.html new file mode 100644 index 0000000..e080aa7 --- /dev/null +++ b/packages/export-salt-theme/ui-src/index.html @@ -0,0 +1,19 @@ + + + + + + Export to CSS Variable + + + + + + +
+ + + diff --git a/packages/export-salt-theme/ui-src/jest-setup.ts b/packages/export-salt-theme/ui-src/jest-setup.ts new file mode 100644 index 0000000..cb6e63f --- /dev/null +++ b/packages/export-salt-theme/ui-src/jest-setup.ts @@ -0,0 +1,14 @@ +import "@testing-library/jest-dom"; + +class ResizeObserver { + observe() { + // do nothing + } + unobserve() { + // do nothing + } + disconnect() { + // do nothing + } +} +global.ResizeObserver = ResizeObserver; diff --git a/packages/export-salt-theme/ui-src/jest.config.js b/packages/export-salt-theme/ui-src/jest.config.js new file mode 100644 index 0000000..e33c026 --- /dev/null +++ b/packages/export-salt-theme/ui-src/jest.config.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "jsdom", + setupFilesAfterEnv: ["/jest-setup.ts"], + moduleNameMapper: { + ".+\\.(css|ttf|woff|woff2)$": "identity-obj-proxy", + ".+\\.(svg|png|jpg)(\\?raw)?$": "/../jest/imageMock.js", + }, +}; diff --git a/packages/export-salt-theme/ui-src/main.tsx b/packages/export-salt-theme/ui-src/main.tsx new file mode 100644 index 0000000..7f7c691 --- /dev/null +++ b/packages/export-salt-theme/ui-src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import "@salt-ds/theme/css/global.css"; +import "@salt-ds/theme/css/theme.css"; + +import App from "./App"; + +const container = document.getElementById("root"); +const root = createRoot(container!); +root.render(); diff --git a/packages/export-salt-theme/ui-src/tsconfig.json b/packages/export-salt-theme/ui-src/tsconfig.json new file mode 100644 index 0000000..0f2586b --- /dev/null +++ b/packages/export-salt-theme/ui-src/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["./", "../shared-src"] +} diff --git a/packages/export-salt-theme/ui-src/types.ts b/packages/export-salt-theme/ui-src/types.ts new file mode 100644 index 0000000..f0f8f36 --- /dev/null +++ b/packages/export-salt-theme/ui-src/types.ts @@ -0,0 +1,5 @@ +import { PostToUIMessage } from "../shared-src"; + +export type FigmaToUIMessageEvent = MessageEvent<{ + pluginMessage: PostToUIMessage; +}>; diff --git a/packages/export-salt-theme/ui-src/views/ExportJsonView.tsx b/packages/export-salt-theme/ui-src/views/ExportJsonView.tsx new file mode 100644 index 0000000..fdc3002 --- /dev/null +++ b/packages/export-salt-theme/ui-src/views/ExportJsonView.tsx @@ -0,0 +1,90 @@ +import { Button, StackLayout } from "@salt-ds/core"; +import { compressToEncodedURIComponent } from "lz-string"; +import React, { useEffect, useRef, useState } from "react"; +import { PostToFigmaMessage } from "../../shared-src"; +import { AvocadoIcon } from "../components/icons/AvocadoIcon"; +import { FigmaToUIMessageEvent } from "../types"; + +function compressObject(object: object): string { + const compressed = compressToEncodedURIComponent(JSON.stringify(object)); + return compressed; +} + +export const ExportJsonView = () => { + const textareaRef = useRef(null); + const copyButtonRef = useRef(null); + + const [text, setText] = useState(""); + + const onGenerateJson = () => { + parent.postMessage( + { + pluginMessage: { + type: "generate-json", + } satisfies PostToFigmaMessage, + }, + "*" + ); + }; + + // Generate JSON on load + useEffect(() => { + onGenerateJson(); + }, []); + + const onCopy = () => { + textareaRef.current?.select(); + document.execCommand("copy"); + copyButtonRef.current?.focus(); + }; + + const onGoSandpack = () => { + const newTheme = JSON.parse(text); + + console.log("generateSandpackLink", { newTheme }); + + const compressed = compressObject(newTheme); + + const PREFIX = "https://origami-z.github.io/stunning-guacamole/#theme/"; + + const url = PREFIX + compressed; + window.open(url, "_blank"); + }; + + const handleMessage = (event: FigmaToUIMessageEvent) => { + const pluginMessage = event.data.pluginMessage; + switch (pluginMessage.type) { + case "generate-json-result": + setText(JSON.stringify(pluginMessage.data, null, 2)); + break; + default: + break; + } + }; + + useEffect(() => { + window.addEventListener("message", handleMessage); + + return () => { + window.removeEventListener("message", handleMessage); + }; + }, []); + + return ( + + + + + + + ); +}; diff --git a/packages/export-salt-theme/ui-src/vite-env.d.ts b/packages/export-salt-theme/ui-src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/export-salt-theme/ui-src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/export-salt-theme/vite.config.ts b/packages/export-salt-theme/vite.config.ts new file mode 100644 index 0000000..f640362 --- /dev/null +++ b/packages/export-salt-theme/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: "./ui-src", + plugins: [react(), viteSingleFile()], + build: { + target: "esnext", + assetsInlineLimit: 100000000, + chunkSizeWarningLimit: 100000000, + cssCodeSplit: false, + outDir: "../dist", + }, +});