Repository: jan-dh/figma-tailwindcss Branch: master Commit: 2ddf895f842c Files: 45 Total size: 72.4 KB Directory structure: gitextract_7qffhun8/ ├── .editorconfig ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── Plan.md ├── README.md ├── manifest.json ├── package.json ├── src/ │ ├── code/ │ │ ├── figma/ │ │ │ ├── effectStyles.ts │ │ │ ├── helpers.ts │ │ │ ├── nodeStyles.ts │ │ │ ├── paintStyles.ts │ │ │ └── textStyles.ts │ │ └── index.ts │ ├── shared/ │ │ └── types.ts │ └── ui/ │ ├── App.tsx │ ├── components/ │ │ ├── Colors/ │ │ │ ├── Color.tsx │ │ │ ├── Colors.tsx │ │ │ ├── Gradient.tsx │ │ │ └── NewColor.tsx │ │ ├── Effects/ │ │ │ ├── BorderRadius.tsx │ │ │ ├── Effects.tsx │ │ │ └── Shadow.tsx │ │ ├── Export/ │ │ │ ├── Export.tsx │ │ │ └── Info.tsx │ │ ├── Footer/ │ │ │ └── Footer.tsx │ │ ├── Preferences/ │ │ │ └── Preferences.tsx │ │ └── Type/ │ │ ├── FontFamilies.tsx │ │ ├── FontFamily.tsx │ │ ├── FontSize.tsx │ │ ├── FontSizes.tsx │ │ └── Type.tsx │ ├── helpers/ │ │ ├── colorFormatter.ts │ │ ├── customHooks.ts │ │ ├── helpers.ts │ │ └── randomMessages.ts │ ├── icons/ │ │ ├── Github.tsx │ │ └── Tailwind.tsx │ ├── main.tsx │ ├── store/ │ │ └── themeStore.ts │ └── styles/ │ └── app.css ├── tsconfig.json ├── ui.html ├── vite.config.code.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.js] indent_size = 2 [*.md] trim_trailing_whitespace = false [*.yml] indent_size = 2 [{package.json,.babelrc}] indent_size = 2 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: '' assignees: '' --- --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: jan-dh --- ### Description ### Steps to reproduce 1. 2. ### Additional info - Figma version: - OS version: ### Extra Sharing the design file where you encountered the issue makes it easier to debug ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "[Feature]" labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ # build artifacts dist # dependencies node_modules .npm npm-debug.log bun.lockb # editor .DS_Store .idea .vscode *.swp *.swo # environment .env .env.local ================================================ FILE: Plan.md ================================================ # Figma TailwindCSS Plugin Modernization ## Current Task: UI Visual Refresh (shadcn-style Design) ### Status: Complete ## Phases ### Phase 1: Project Setup ✅ - [x] Create Plan.md - [x] Initialize with Bun - [x] Install dependencies (React 18, Zustand, Vite, Tailwind 4, etc.) - [x] Create new project structure ### Phase 2: Vite Configuration ✅ - [x] Create `vite.config.ts` for UI build (with vite-plugin-singlefile) - [x] Create `vite.config.code.ts` for Figma sandbox code (IIFE output) - [x] Update package.json scripts ### Phase 3: React 18 Migration ✅ - [x] Create new entry point (`src/ui/main.tsx`) with createRoot - [x] Update to React 18 createRoot API - [x] Migrate to React Router v6 (Routes/Route pattern) ### Phase 4: State Management (reactn → Zustand) ✅ - [x] Create Zustand store (`src/ui/store/themeStore.ts`) - [x] Define types in `src/shared/types.ts` ### Phase 5: Component Migration ✅ - [x] Migrate shared types - [x] Migrate helper functions (colorFormatter, helpers, customHooks) - [x] Migrate leaf components (Footer, Color, Shadow, FontSize, etc.) - [x] Migrate feature components (Colors, Type, Effects, Export, Preferences) - [x] Migrate App with React Router v6 ### Phase 6: Tailwind 4 for Plugin UI ✅ - [x] Create new CSS setup with @theme directive - [x] Removed old postcss.config.cjs and tailwind.config.cjs ### Phase 7: Export Logic ✅ - [x] TW4 output: `@theme {}` CSS format - [x] TW3 output: `module.exports = { theme: { extend: {} } }` JS format - [x] Updated prism-react-renderer to v2 ### Phase 8: Figma Code Migration ✅ - [x] Migrate `src/js/code.js` → `src/code/index.ts` - [x] Migrate Figma helpers to TypeScript (paintStyles, textStyles, effectStyles, nodeStyles) ### Phase 9: Verification ✅ - [x] Build completes without errors - [x] dist/code.js is valid IIFE (5KB) - [x] dist/ui.html has inlined CSS/JS (292KB - well under 5MB limit) - [ ] Test in Figma (manual verification needed) ## Build Output ``` dist/ ├── code.js (5.15 KB) - Figma sandbox code └── ui.html (292.81 KB) - UI with inlined CSS/JS ``` ## New Project Structure ``` src/ ├── code/ # Figma sandbox (no DOM) │ ├── index.ts │ └── figma/ │ ├── helpers.ts │ ├── paintStyles.ts │ ├── textStyles.ts │ ├── effectStyles.ts │ └── nodeStyles.ts ├── ui/ # React UI │ ├── main.tsx │ ├── App.tsx │ ├── store/ │ │ └── themeStore.ts │ ├── components/ │ │ ├── Colors/ │ │ ├── Effects/ │ │ ├── Export/ │ │ ├── Footer/ │ │ ├── Preferences/ │ │ └── Type/ │ ├── helpers/ │ │ ├── colorFormatter.ts │ │ ├── customHooks.ts │ │ ├── helpers.ts │ │ └── randomMessages.ts │ ├── icons/ │ └── styles/ │ └── app.css └── shared/ └── types.ts ``` ## Key Changes from Original | Aspect | Before | After | |--------|--------|-------| | Build Tool | Webpack | Vite + Bun | | React | 16.14.0 | 18.3.1 | | Router | react-router-dom 5 | react-router-dom 6 | | State | reactn | Zustand | | Tailwind | 3.x (PostCSS) | 4.x (@tailwindcss/vite) | | Syntax Highlighter | prism-react-renderer 1 | prism-react-renderer 2 | | Language | JavaScript | TypeScript | ## Scripts ```bash bun run dev # Vite dev server for UI bun run dev:code # Watch mode for Figma sandbox code bun run build # Build both UI and code bun run build:ui # Build only UI bun run build:code # Build only Figma code ``` --- ## Phase 10: UI Visual Refresh ✅ ### Goal Transform the spacious, bold UI into a compact, minimal shadcn-inspired design. ### Changes Made #### Typography - [x] `.t-beta`: text-2xl font-bold → text-base font-semibold - [x] `.t-gamma`: text-xl → text-sm font-medium - [x] `.intro`: Added text-sm text-slate-500 - [x] Body: Added text-sm base size #### Spacing - [x] App layout: p-4 → p-3 - [x] Header sections: py-4 → py-3 - [x] Content sections: my-8 → my-4 - [x] Navigation buttons: mt-8 → mt-4 - [x] Grid gaps: gap-4 → gap-3 #### Form Controls - [x] `.form-control`: height 44px → 36px, added text-sm, shadow-sm - [x] `.button`: min-h-10 → h-8, text-sm, rounded-md - [x] Select: height adjusted to h-8 #### Color Palette (Vibrant → Slate) - [x] button--green: teal-500 → slate-900 (primary) - [x] button--blue: blue-400 → slate-100/200 (secondary) - [x] button--grey: gray-100 → transparent with border (ghost) - [x] Text colors: gray-600/900 → slate-500/900 - [x] Borders: gray-200/300 → slate-200 - [x] Focus rings: teal-500 → slate-400 #### Components Updated - [x] `src/ui/styles/app.css` - Core styles - [x] `src/ui/App.tsx` - Layout padding - [x] `src/ui/components/Preferences/Preferences.tsx` - [x] `src/ui/components/Colors/Colors.tsx` - [x] `src/ui/components/Colors/Color.tsx` - [x] `src/ui/components/Colors/NewColor.tsx` - [x] `src/ui/components/Colors/Gradient.tsx` - [x] `src/ui/components/Type/Type.tsx` - [x] `src/ui/components/Effects/Effects.tsx` - [x] `src/ui/components/Effects/Shadow.tsx` - [x] `src/ui/components/Effects/BorderRadius.tsx` - [x] `src/ui/components/Export/Export.tsx` - [x] `src/ui/components/Footer/Footer.tsx` #### Leaf Component Details - Color swatches: 42px → 32px - Shadow/BorderRadius boxes: w-20 h-20 → w-12 h-12 (flex wrap layout) - Footer: py-4 → py-2.5, added border-t ### Phase 10b: UI Refinements ✅ - [x] Color row actions: Replaced thick Update/X buttons with subtle icon buttons - [x] Color inputs: Auto-save on blur instead of manual Update button - [x] NewColor: Removed label, uses dashed border placeholder, plus icon to add - [x] Effects grid: Changed from grid-cols-4 to flex-wrap with smaller items (w-12 h-12) - [x] Dynamic viewport: Added resize messaging between UI and plugin (min: 200px, max: 600px) - [x] UI width: Reduced from 740px to 420px for more compact feel ## Potential Future Improvements ### UI/UX Surfaces 1. **Navigation** - Add a step indicator/breadcrumb showing progress (1/5, 2/5, etc.) 2. **Empty states** - More helpful empty states with illustrations or better copy 3. **Loading states** - Add skeleton loaders while Figma data is being fetched 4. **Toast notifications** - Feedback when colors are updated/removed 5. **Search/filter** - Filter colors by name when there are many 6. **Keyboard shortcuts** - Enter to confirm, Escape to cancel 7. **Dark mode** - Support Figma's dark theme 8. **Undo/redo** - Allow reverting color changes ## Phase 11: GitHub Issue Fixes ✅ ### Issue #53 - Option to Disable REM Conversion ✅ - [x] Added `useRem` preference to types (`src/shared/types.ts`) - [x] Added default value in store (`src/ui/store/themeStore.ts`) - [x] Added radio toggle in Preferences (`src/ui/components/Preferences/Preferences.tsx`) - [x] Updated `cleanupTheme` to conditionally convert to REM or keep as px (`src/ui/helpers/helpers.ts`) - [x] Updated Export component to pass preference ### Issue #64 - Border Radius Default Variant ✅ - [x] Added `defaultBorderRadiusIndex` state to types and store - [x] Added `getBorderRadiusName` helper function for Tailwind naming convention (xs, sm, DEFAULT, lg, xl, 2xl, 3xl) - [x] Updated `cleanupTheme` to generate proper border radius names - [x] Updated BorderRadius component to be clickable for selecting DEFAULT - [x] Updated Effects component with selection UI and instructions - [x] Updated Tailwind 4 formatter to output `--radius` (no suffix) for DEFAULT --- ### Functional Improvements 9. **Color picker** - Inline color picker instead of hex input 10. **Drag & drop** - Reorder colors 11. **Bulk actions** - Select multiple colors for removal 12. **Preview** - Live preview of the generated CSS/config 13. **Import** - Import existing tailwind.config.js to prefill 14. **Variable support** - Support Figma variables (new feature) 15. **Copy individual values** - Click to copy a single color/shadow value ================================================ FILE: README.md ================================================ # Figma Tailwindcss A plugin that tries to bridge the gap between design and code. Figma Tailwindcss lets you export aspects of a design made in Figma to a javascript `theme` file that can easily be used with Tailwindcss. The plugin: [Figma TailwindCSS](https://www.figma.com/community/plugin/785619431629077634/Figma-Tailwindcss) --- ## Table of Contents - [Usage](#usage) - [Roadmap](#roadmap) - [License](#license) ## Usage ### Creating your theme The plugin gets it's info from the Local Styles. At this point it picks up: - colors - font-families - text-sizes - box-shadow - border-radius #### Colors Colors are taken from the Figma Local Paint Styles. Colors can be grouped in the export step. If you want to group codes,prefix them with the same name. #### Font-families The plugin will pick up all font-families used in the Local Text Styles. #### Text-sizes All the different font-sizes used in the Local Text Styles will be picked up by the plugin. Pick a base font-size and the rest of the font-size names are calculated accordingly. The logic used: ```javascript ... '3xs' '2xs' 'xs' 'sm' 'base' 'lg' 'xl' '2xl' '3xl' ... ``` The font-sizes the plugin spits out will also be converted into a rem based scale. #### Box-shadows Taken from the effectStyles from your document. #### Border-radius Taken from the nodeStyles from your document. ### Importing your theme Import the `theme.js` file in to your `tailwind.config.js` configuration file to use it: **Require syntax** `const myTheme = require(./theme);` the require syntax will make sure your custom values get picked up by the [Intelligent Tailwind CSS plugin](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss). If you want to use this syntax, remove the `export default theme` statement from your theme file **Import syntax** `import 'myTheme' from './theme` #### Extending the default theme You can extend the default theme like so: ``` module.exports = { theme: { extend: { colors: myTheme.colors } } ``` More info on extending the default theme: - https://tailwindcss.com/docs/theme#extending-the-default-theme - https://www.youtube.com/watch?v=0l0Gx8gWPHk&ab_channel=TailwindLabs ## Contributing All feedback is welcome. Feel free to submit [issues or suggestions](https://github.com/jan-dh/figma-tailwindcss/issues). The plugin shows you some random messages when you're missing one of the exportable properties. If you want to add your own, feel free to make a Pull Request for [this file](https://github.com/jan-dh/figma-tailwindcss/blob/master/src/js/helpers/randomMessages.js). ## Roadmap - line-height ## License This project is licensed under the terms of the MIT license. ================================================ FILE: manifest.json ================================================ { "name": "Figma Tailwindcss", "id": "785619431629077634", "api": "1.0.0", "main": "dist/code.js", "ui": "dist/ui.html", "documentAccess": "dynamic-page", "networkAccess": { "allowedDomains": [ "none" ] }, "editorType": [ "figma" ] } ================================================ FILE: package.json ================================================ { "name": "figma-tailwindcss", "version": "2.1.0", "type": "module", "description": "Export Figma styles to TailwindCSS theme", "scripts": { "dev": "vite", "dev:code": "vite build --config vite.config.code.ts --watch", "build": "vite build && vite build --config vite.config.code.ts", "build:ui": "vite build", "build:code": "vite build --config vite.config.code.ts", "preview": "vite preview" }, "repository": { "type": "git", "url": "git+https://github.com/jan-dh/figma-tailwindcss.git" }, "author": "Jan D'Hollander", "license": "ISC", "bugs": { "url": "https://github.com/jan-dh/figma-tailwindcss/issues" }, "homepage": "https://github.com/jan-dh/figma-tailwindcss#readme", "dependencies": { "file-saver": "^2.0.5", "prism-react-renderer": "^2.4.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.3", "zustand": "^5.0.10" }, "devDependencies": { "@figma/plugin-typings": "^1.123.0", "@tailwindcss/vite": "^4.1.18", "@types/bun": "^1.3.7", "@types/file-saver": "^2.0.7", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "vite": "^7.3.1", "vite-plugin-singlefile": "^2.3.0" }, "private": true } ================================================ FILE: src/code/figma/effectStyles.ts ================================================ import { makeRgb } from './helpers' interface EffectStylesResult { shadows: { name: string; value: string }[] } export default async function getEffectStyles(): Promise { const effectStyles = await figma.getLocalEffectStylesAsync() const shadows: { name: string; value: string }[] = [] effectStyles.forEach((style) => { const { effects, name } = style const styleString: string[] = [] effects.forEach((effect) => { if (effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') { const { color, offset, radius, spread } = effect const { r, g, b, a } = makeRgb(color) const colorString = `${r},${g},${b},${a}` styleString.push( `${effect.type === 'INNER_SHADOW' ? 'inset ' : ''}${offset.x}px ${offset.y}px ${radius}px ${spread}px rgba(${colorString})` ) } }) if (styleString.length > 0) { shadows.push({ name, value: styleString.join(', '), }) } }) return { shadows } } ================================================ FILE: src/code/figma/helpers.ts ================================================ export function rgbToHex(int: number): string { let hex = Number(int).toString(16) if (hex.length < 2) { hex = `0${hex}` } return hex } export function makeHex(r: number, g: number, b: number): string { const red = rgbToHex(r) const green = rgbToHex(g) const blue = rgbToHex(b) return `#${red}${green}${blue}` } export function makeRgb(color: RGB | RGBA): { r: number; g: number; b: number; a: number } { const r = Math.round(255 * color.r) const g = Math.round(255 * color.g) const b = Math.round(255 * color.b) const a = 'a' in color ? Math.round(100 * color.a) / 100 : 1 return { r, g, b, a } } ================================================ FILE: src/code/figma/nodeStyles.ts ================================================ interface NodeStylesResult { finalRadii: { name: string; value: number }[] } export default async function getNodeStyles(): Promise { await figma.loadAllPagesAsync() const filteredNodes = figma.root.findAll( (n): n is SceneNode & { cornerRadius: number } => 'cornerRadius' in n && typeof (n as any).cornerRadius === 'number' ) const radii = new Set() filteredNodes.forEach((n) => { const cornerRadius = (n as any).cornerRadius as number if (typeof cornerRadius === 'number') { const value = cornerRadius < 99 ? cornerRadius : 999 radii.add(value) } }) const radiiArray = [...radii].sort((a, b) => a - b) const finalRadii: { name: string; value: number }[] = [] radiiArray.forEach((radius) => { const value = Number(radius) const name = '' finalRadii.push({ name, value }) }) // Add default none finalRadii.unshift({ name: 'none', value: 0 }) return { finalRadii } } ================================================ FILE: src/code/figma/paintStyles.ts ================================================ import { makeHex, makeRgb } from './helpers' interface ColorResult { colors: { name: string; value: string }[] gradientColors: string[] } export default async function getPaintStyles(): Promise { const colorStyles = await figma.getLocalPaintStylesAsync() const colors: { name: string; value: string }[] = [] const gradientColors: string[] = [] colorStyles.forEach((style) => { const paint = style.paints[0] || null if (paint) { if (paint.type === 'SOLID') { const { name } = style const { r, g, b } = makeRgb(paint.color) const value = makeHex(r, g, b) colors.push({ name, value }) } else if ( paint.type === 'GRADIENT_LINEAR' || paint.type === 'GRADIENT_RADIAL' || paint.type === 'GRADIENT_ANGULAR' || paint.type === 'GRADIENT_DIAMOND' ) { const gradientStops = paint.gradientStops if (gradientStops && gradientStops.length > 0) { gradientStops.forEach((stop) => { const { r, g, b } = makeRgb(stop.color) const value = makeHex(r, g, b) gradientColors.push(value) }) } } } }) return { colors, gradientColors } } ================================================ FILE: src/code/figma/textStyles.ts ================================================ interface TextStylesResult { finalSizes: { name: string; value: string }[] finalFamilies: { name: string; value: string }[] } export default async function getTextStyles(): Promise { const textStyles = await figma.getLocalTextStylesAsync() const fontSizes: number[] = [] const fontFamilies: string[] = [] const finalSizes: { name: string; value: string }[] = [] const finalFamilies: { name: string; value: string }[] = [] textStyles.forEach((style) => { const { family } = style.fontName const { fontSize } = style fontFamilies.push(family) fontSizes.push(fontSize) }) // Get unique values const singleSizes = Array.from(new Set(fontSizes)).sort((a, b) => a - b) const singleFamilies = Array.from(new Set(fontFamilies)) // Clean sizes singleSizes.forEach((size) => { const name = '' const value = size.toString() finalSizes.push({ name, value }) }) // Clean families singleFamilies.forEach((family) => { const name = family.replace(/\s+/g, '-').toLowerCase() const value = family finalFamilies.push({ name, value }) }) return { finalSizes, finalFamilies } } ================================================ FILE: src/code/index.ts ================================================ import getPaintStyles from './figma/paintStyles' import getTextStyles from './figma/textStyles' import getEffectStyles from './figma/effectStyles' import getNodeStyles from './figma/nodeStyles' interface Theme { colors: { name: string; value: string }[] gradientColors: string[] fontSize: { name: string; value: string }[] fontFamily: { name: string; value: string }[] boxShadow: { name: string; value: string }[] borderRadius: { name: string; value: number }[] baseFontSize: false preferences: { tailwindVersion: '4' colorFormat: 'hex' grouped: false } } const theme: Theme = { colors: [], gradientColors: [], fontSize: [], fontFamily: [], boxShadow: [], borderRadius: [], baseFontSize: false, preferences: { tailwindVersion: '4', colorFormat: 'hex', grouped: false, }, } const UI_WIDTH = 420 const UI_MIN_HEIGHT = 200 const UI_MAX_HEIGHT = 600 // Gather all different properties const paintStyles = getPaintStyles() const textStyles = getTextStyles() const effectStyles = getEffectStyles() const nodeStyles = getNodeStyles() Promise.all([paintStyles, textStyles, effectStyles, nodeStyles]) .then((values) => { theme.colors.push(...values[0].colors) theme.gradientColors.push(...values[0].gradientColors) theme.fontSize.push(...values[1].finalSizes) theme.fontFamily.push(...values[1].finalFamilies) theme.boxShadow.push(...values[2].shadows) theme.borderRadius.push(...values[3].finalRadii) }) .then(() => { // Show UI figma.showUI(__html__, { width: UI_WIDTH, height: UI_MAX_HEIGHT }) // Pass theme to UI figma.ui.postMessage(theme) }) // Handle messages from UI figma.ui.onmessage = (msg: { type: string; height?: number }) => { if (msg.type === 'resize' && typeof msg.height === 'number') { const newHeight = Math.min(Math.max(msg.height, UI_MIN_HEIGHT), UI_MAX_HEIGHT) figma.ui.resize(UI_WIDTH, newHeight) } } ================================================ FILE: src/shared/types.ts ================================================ export interface ColorItem { name: string value: string } export interface FontSizeItem { name: string value: string } export interface FontFamilyItem { name: string value: string } export interface ShadowItem { name: string value: string } export interface BorderRadiusItem { name: string value: number | string } export interface Preferences { tailwindVersion: '3' | '4' colorFormat: 'hex' | 'rgba' | 'hsl' | 'oklch' | 'oklab' grouped: boolean useRem: boolean } export interface ThemeState { colors: ColorItem[] gradientColors: string[] fontSize: FontSizeItem[] fontFamily: FontFamilyItem[] boxShadow: ShadowItem[] borderRadius: BorderRadiusItem[] baseFontSize: number | false defaultBorderRadiusIndex: number | null preferences: Preferences } export type CleanTheme = { colors?: Record> fontFamily?: Record fontSize?: Record boxShadow?: Record borderRadius?: Record } ================================================ FILE: src/ui/App.tsx ================================================ import { MemoryRouter, Routes, Route } from 'react-router-dom' import { ScrollToTop, useAutoResize } from './helpers/customHooks' import Preferences from './components/Preferences/Preferences' import Colors from './components/Colors/Colors' import Type from './components/Type/Type' import Effects from './components/Effects/Effects' import Export from './components/Export/Export' import Info from './components/Export/Info' import Footer from './components/Footer/Footer' const AppContent = () => { useAutoResize() return ( <>
} /> } /> } /> } /> } /> } />
) } const App = () => { return (
) } export default App ================================================ FILE: src/ui/components/Colors/Color.tsx ================================================ import { useInput } from '../../helpers/customHooks' import type { ColorItem } from '../../../shared/types' interface ColorProps extends ColorItem { index: number onChange: (color: ColorItem, index: number) => void removeColor: (index: number) => void colorFormat: string } function Color({ name: initialName, value: initialValue, index, onChange, removeColor }: ColorProps) { const [name, setName] = useInput(initialName) const [value, setValue] = useInput(initialValue) const updateColor = () => { const newColor = { name, value } onChange(newColor, index) } const handleRemove = () => { removeColor(index) } return (
) } export default Color ================================================ FILE: src/ui/components/Colors/Colors.tsx ================================================ import { Link } from 'react-router-dom' import { useThemeStore } from '../../store/themeStore' import Color from './Color' import Gradient from './Gradient' import NewColor from './NewColor' import messages from '../../helpers/randomMessages' import type { ColorItem } from '../../../shared/types' const Colors = () => { const colors = useThemeStore((s) => s.colors) const gradients = useThemeStore((s) => s.gradientColors) const preferences = useThemeStore((s) => s.preferences) const updateColor = useThemeStore((s) => s.updateColor) const removeColor = useThemeStore((s) => s.removeColor) const setColors = useThemeStore((s) => s.setColors) const nextIndex = colors.length const hasColor = colors.length > 0 const hasGradients = gradients.length > 0 const feedbackItem = messages.emptyColors[Math.floor(Math.random() * messages.emptyColors.length)] const { colorFormat } = preferences const handleColorChange = (color: ColorItem, i: number) => { if (i >= colors.length) { // Adding new color setColors([...colors, color]) } else { updateColor(i, color) } } const handleRemoveColor = (i: number) => { removeColor(i) } return ( <>

Colors

Pick and choose the colors you want to use

Colors are taken from the Figma Local Paint Styles. Colors can be grouped in the export step. If you want to group codes, prefix them with the same name (only the last two parts will be used).

{hasColor ? ( colors.map((color, i) => ( )) ) : (

{feedbackItem}

)} {hasGradients ? (

Gradients

We found some gradients in the document. Make sure to add the colors if you want to use them in your theme.

{gradients.map((color, i) => ( ))}
) : null}
Previous Next
) } export default Colors ================================================ FILE: src/ui/components/Colors/Gradient.tsx ================================================ interface GradientProps { hex: string } function Gradient({ hex }: GradientProps) { return (
{hex}
) } export default Gradient ================================================ FILE: src/ui/components/Colors/NewColor.tsx ================================================ import { useInput } from '../../helpers/customHooks' import type { ColorItem } from '../../../shared/types' interface NewColorProps { index: number onChange: (color: ColorItem, index: number) => void } const NewColor = ({ index, onChange }: NewColorProps) => { const [name, setName] = useInput('') const [value, setValue] = useInput('') const addColor = () => { const newColor = { name, value } onChange(newColor, index) } return (
) } export default NewColor ================================================ FILE: src/ui/components/Effects/BorderRadius.tsx ================================================ import { getBorderRadiusName } from '../../helpers/helpers' interface BorderRadiusProps { index: number value: number | string isDefault: boolean defaultIndex: number | null total: number useRem: boolean onSelect: () => void } const BorderRadius = ({ index, value, isDefault, defaultIndex, total, useRem, onSelect, }: BorderRadiusProps) => { const displayName = getBorderRadiusName(index, defaultIndex, total) const numValue = typeof value === 'number' ? value : parseFloat(String(value)) const displayValue = useRem ? `${numValue / 16}rem` : `${numValue}px` return (
{displayName} {displayValue}
) } export default BorderRadius ================================================ FILE: src/ui/components/Effects/Effects.tsx ================================================ import { Link } from 'react-router-dom' import { useThemeStore } from '../../store/themeStore' import Shadow from './Shadow' import BorderRadius from './BorderRadius' import messages from '../../helpers/randomMessages' const Effects = () => { const shadows = useThemeStore((s) => s.boxShadow) const borderRadii = useThemeStore((s) => s.borderRadius) const defaultBorderRadiusIndex = useThemeStore((s) => s.defaultBorderRadiusIndex) const setDefaultBorderRadiusIndex = useThemeStore((s) => s.setDefaultBorderRadiusIndex) const useRem = useThemeStore((s) => s.preferences.useRem) const hasShadows = shadows.length > 0 const hasBorderRadii = borderRadii.length > 0 const handleSelectDefault = (index: number) => { // Toggle off if clicking the same one if (defaultBorderRadiusIndex === index) { setDefaultBorderRadiusIndex(null) } else { setDefaultBorderRadiusIndex(index) } } const feedbackShadows = messages.emptyShadows[Math.floor(Math.random() * messages.emptyShadows.length)] const feedbackBorderRadii = messages.emptyBorderRadii[ Math.floor(Math.random() * messages.emptyBorderRadii.length) ] return ( <>

Effects

Effect Styles we found in your Figma file

Border-radius

{hasBorderRadii && (

Click a radius to set it as DEFAULT (used for rounded class)

)} {hasBorderRadii ? (
{borderRadii.map((radius, i) => ( handleSelectDefault(i)} /> ))}
) : (

{feedbackBorderRadii}

)}

Shadows

{hasShadows ? (
{shadows.map((shadow, i) => ( ))}
) : (

{feedbackShadows}

)}
Previous Next
) } export default Effects ================================================ FILE: src/ui/components/Effects/Shadow.tsx ================================================ interface ShadowProps { name: string value: string } const Shadow = ({ name, value }: ShadowProps) => { return (
{name}
) } export default Shadow ================================================ FILE: src/ui/components/Export/Export.tsx ================================================ import { Link } from 'react-router-dom' import { Highlight, themes } from 'prism-react-renderer' import { saveAs } from 'file-saver' import { useThemeStore } from '../../store/themeStore' import { cleanupTheme, formatTailwind4Theme, formatTailwind3Theme } from '../../helpers/helpers' const Export = () => { const theme = useThemeStore() const preferences = useThemeStore((s) => s.preferences) const setPreferences = useThemeStore((s) => s.setPreferences) const defaultBorderRadiusIndex = useThemeStore((s) => s.defaultBorderRadiusIndex) const { colorFormat, grouped, tailwindVersion, useRem } = preferences const isV4 = tailwindVersion === '4' const cleanTheme = cleanupTheme(theme, colorFormat, grouped, useRem, defaultBorderRadiusIndex) const groupColors = () => { if (isV4) return setPreferences({ grouped: !grouped }) } const exportTheme = () => { if (isV4) { const cssContent = formatTailwind4Theme(cleanTheme) const blob = new Blob([cssContent], { type: 'text/css;charset=utf-8', }) saveAs(blob, 'theme.css') } else { const jsContent = formatTailwind3Theme(cleanTheme) const blob = new Blob([jsContent], { type: 'application/javascript;charset=utf-8', }) saveAs(blob, 'tailwind.config.js') } } const markup = isV4 ? formatTailwind4Theme(cleanTheme) : formatTailwind3Theme(cleanTheme) const buttonText = grouped ? 'Ungroup colors' : 'Group colors' const switchClass = grouped ? 'bg-green-400 active' : 'bg-gray-400' return ( <>

Export theme

Almost there...

{({ className, style, tokens, getLineProps, getTokenProps }) => (
              {tokens.map((line, i) => (
                
{line.map((token, key) => ( ))}
))}
)}
{!isV4 && (
{buttonText}
)}
Previous
) } export default Export ================================================ FILE: src/ui/components/Export/Info.tsx ================================================ import { Link } from 'react-router-dom' const Info = () => { const theme = `'./theme'` const overWrite = `module.exports = { theme: { colors: { theme.colors }` const extend = `module.exports = { theme: { colors: { gray: { 100: #f7fafc', 200: '#edf2f7', }, ...theme.colors } ` return ( <>

Usage

Everything you need to know to get you started

Import the theme.js file in to your{' '} tailwind.config.js configuration file to use it:

            import theme from {theme};
          

Overriding the default theme

To override an option in the default theme, create a theme section in your config and add the key you would like to override.

            {overWrite}
          

Add to current values

Using the spread operator at the end of each property you can add your theme values to an existing config or to the default tailwind config.

            {extend}
          
Previous
) } export default Info ================================================ FILE: src/ui/components/Footer/Footer.tsx ================================================ import Tailwind from '../../icons/Tailwind' import Github from '../../icons/Github' const Footer = () => { return ( ) } export default Footer ================================================ FILE: src/ui/components/Preferences/Preferences.tsx ================================================ import type { ChangeEvent } from 'react' import { Link } from 'react-router-dom' import { useThemeStore } from '../../store/themeStore' const Preferences = () => { const preferences = useThemeStore((s) => s.preferences) const setPreferences = useThemeStore((s) => s.setPreferences) const updatePreference = (key: string, value: string | boolean) => { setPreferences({ [key]: value }) } return ( <>

Preferences

Configure how Tailwind CSS should be generated

Tailwind version

Choose which version of Tailwind to target.

Color format

Select how colors should be exported.

Font size unit

Choose whether font sizes should be converted to REM or kept as pixels.

Next
) } export default Preferences ================================================ FILE: src/ui/components/Type/FontFamilies.tsx ================================================ import { useThemeStore } from '../../store/themeStore' import FontFamily from './FontFamily' const FontFamilies = () => { const fontFamilies = useThemeStore((s) => s.fontFamily) return (
{fontFamilies.map((family, i) => ( ))}
) } export default FontFamilies ================================================ FILE: src/ui/components/Type/FontFamily.tsx ================================================ interface FontFamilyProps { name: string value: string } const FontFamily = ({ name, value }: FontFamilyProps) => { return (
{name} {value}
) } export default FontFamily ================================================ FILE: src/ui/components/Type/FontSize.tsx ================================================ interface FontSizeProps { name: string value: string } const FontSize = ({ name, value }: FontSizeProps) => { const remSize = Number(value) / 16 const fontSize = { fontSize: `${remSize}rem`, } return (

{value}px- {remSize}rem

{name}
) } export default FontSize ================================================ FILE: src/ui/components/Type/FontSizes.tsx ================================================ import type { ChangeEvent } from 'react' import { useThemeStore } from '../../store/themeStore' import FontSize from './FontSize' import { calculatePosition } from '../../helpers/helpers' const FontSizes = () => { const fontSizes = useThemeStore((s) => s.fontSize) const baseFontSize = useThemeStore((s) => s.baseFontSize) const setFontSize = useThemeStore((s) => s.setFontSize) const setBaseFontSize = useThemeStore((s) => s.setBaseFontSize) const updateFontSizes = (e: ChangeEvent) => { const basePosition = fontSizes.findIndex((x) => x.value === e.target.value) const size = fontSizes.length const newFontSizes = fontSizes.map((item, i) => ({ ...item, name: calculatePosition(i, basePosition, size), })) setBaseFontSize(Number(e.target.value)) setFontSize(newFontSizes) } return ( <>
{fontSizes.map((size, i) => ( ))}
) } export default FontSizes ================================================ FILE: src/ui/components/Type/Type.tsx ================================================ import type { MouseEvent } from 'react' import { Link } from 'react-router-dom' import { useThemeStore } from '../../store/themeStore' import FontFamilies from './FontFamilies' import FontSizes from './FontSizes' import messages from '../../helpers/randomMessages' const Type = () => { const fontSizes = useThemeStore((s) => s.fontSize) const fontFamilies = useThemeStore((s) => s.fontFamily) const baseFontSize = useThemeStore((s) => s.baseFontSize) const hasFamilies = fontFamilies.length > 0 const hasSizes = fontSizes.length > 0 const emptyFamily = messages.emptyFamilies[Math.floor(Math.random() * messages.emptyFamilies.length)] const emptySize = messages.emptySizes[Math.floor(Math.random() * messages.emptySizes.length)] const validateFontSize = (e: MouseEvent) => { if (!baseFontSize && fontSizes.length > 0) { e.preventDefault() } } const buttonClass = baseFontSize || !hasSizes ? 'button button--green' : 'button button--green opacity-50' return ( <>

Typography

Font families and font sizes

Font families

{hasFamilies ? ( ) : (

{emptyFamily}

)}

Font sizes

{hasSizes ? ( ) : (

{emptySize}

)}
Previous Next
) } export default Type ================================================ FILE: src/ui/helpers/colorFormatter.ts ================================================ // Converts sRGB (0-255) to linear RGB (0-1) function sRGBToLinear(c: number): number { c /= 255 return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) } // RGB string to array function parseRGB(input: string | number[]): [number, number, number, number] { if (typeof input === 'string' && input.startsWith('#')) { let hex = input.slice(1) if (hex.length === 3) hex = hex .split('') .map((h) => h + h) .join('') const r = parseInt(hex.slice(0, 2), 16) const g = parseInt(hex.slice(2, 4), 16) const b = parseInt(hex.slice(4, 6), 16) return [r, g, b, 1] } if (typeof input === 'string' && input.startsWith('rgb')) { const nums = input.match(/[\d.]+/g)?.map(Number) || [0, 0, 0, 1] return nums.length === 3 ? [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0, 1] : [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0, nums[3] ?? 1] } if (Array.isArray(input)) { return input.length === 3 ? [input[0] ?? 0, input[1] ?? 0, input[2] ?? 0, 1] : [input[0] ?? 0, input[1] ?? 0, input[2] ?? 0, input[3] ?? 1] } return [0, 0, 0, 1] } // RGB -> HSL function rgbToHsl([r, g, b]: [number, number, number]): [number, number, number] { r /= 255 g /= 255 b /= 255 const max = Math.max(r, g, b), min = Math.min(r, g, b) let h = 0, s = 0 const l = (max + min) / 2 if (max !== min) { const d = max - min s = l > 0.5 ? d / (2 - max - min) : d / (max + min) switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0) break case g: h = (b - r) / d + 2 break case b: h = (r - g) / d + 4 break } h /= 6 } return [h * 360, s * 100, l * 100] } // RGB -> OKLab function rgbToOklab([r, g, b]: [number, number, number]): [number, number, number] { // convert sRGB -> linear const R = sRGBToLinear(r), G = sRGBToLinear(g), B = sRGBToLinear(b) // linear RGB -> LMS const L = 0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B const M = 0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B const S = 0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B // LMS -> OKLab const l_ = Math.cbrt(L * 0.8189330101 + M * 0.3618667424 - S * 0.1288597137) const m_ = Math.cbrt(L * 0.0329845436 + M * 0.9293118715 + S * 0.0361456387) const s_ = Math.cbrt(L * 0.0482003018 + M * 0.2643662691 + S * 0.633851707) const L_ok = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_ const a_ok = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_ const b_ok = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_ return [L_ok, a_ok, b_ok] } // OKLab -> OKLCH function oklabToOklch([L, a, b]: [number, number, number]): [number, number, number] { const C = Math.sqrt(a * a + b * b) let H = Math.atan2(b, a) * (180 / Math.PI) if (H < 0) H += 360 return [L, C, H] } // Main format function export function formatColor(input: string | number[], format: string = 'hex'): string { const [r, g, b, alpha] = parseRGB(input) switch (format) { case 'rgba': return `rgba(${r}, ${g}, ${b}, ${alpha})` case 'hsl': { const [h, s, l] = rgbToHsl([r, g, b]) return `hsl(${h.toFixed(0)}, ${s.toFixed(0)}%, ${l.toFixed(0)}%)` } case 'oklab': { const [L, a, b_] = rgbToOklab([r, g, b]) return `oklab(${L.toFixed(3)}, ${a.toFixed(3)}, ${b_.toFixed(3)})` } case 'oklch': { const oklch = oklabToOklch(rgbToOklab([r, g, b])) return `oklch(${oklch[0].toFixed(3)}, ${oklch[1].toFixed(3)}, ${oklch[2].toFixed(0)})` } case 'hex': default: return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` } } ================================================ FILE: src/ui/helpers/customHooks.ts ================================================ import { useState, useEffect, useCallback } from 'react' import type { ChangeEvent } from 'react' import { useLocation } from 'react-router-dom' export function useInput(initialValue: string): [string, (e: ChangeEvent) => void] { const [value, setValue] = useState(initialValue) function handleChange(e: ChangeEvent) { setValue(e.target.value) } return [value, handleChange] } export function ScrollToTop(): null { const { pathname } = useLocation() useEffect(() => { window.scrollTo(0, 0) }, [pathname]) return null } export function useAutoResize(): void { const { pathname } = useLocation() const updateHeight = useCallback(() => { // Small delay to let content render requestAnimationFrame(() => { const height = document.body.scrollHeight parent.postMessage({ pluginMessage: { type: 'resize', height } }, '*') }) }, []) useEffect(() => { updateHeight() }, [pathname, updateHeight]) useEffect(() => { // Also resize on window resize and DOM changes const observer = new ResizeObserver(updateHeight) observer.observe(document.body) return () => observer.disconnect() }, [updateHeight]) } ================================================ FILE: src/ui/helpers/helpers.ts ================================================ import { formatColor } from './colorFormatter' import type { ThemeState, CleanTheme, ColorItem } from '../../shared/types' export function calculatePosition(index: number, basePosition: number, length: number): string { let value = '' if (index === basePosition) { value = 'base' } if (index === basePosition - 1) { value = 'sm' } else if (index === basePosition - 2) { value = 'xs' } else if (index === basePosition + 1) { value = 'lg' } else if (index === basePosition + 2) { value = 'xl' } else if (index < basePosition - 2) { const numberOfSmaller = length - (length - (basePosition - 2)) const position = -(index - (numberOfSmaller + 1)) value = `${position}xs` } else if (index > basePosition + 2) { const numberOfBigger = length - (basePosition + 3) const position = numberOfBigger - (length - (index + 2)) value = `${position}xl` } return value } export function getBorderRadiusName( index: number, defaultIndex: number | null, total: number ): string { // If no default selected, use numeric naming if (defaultIndex === null) { return String(index) } const offset = index - defaultIndex if (offset === 0) return 'DEFAULT' if (offset === -1) return 'sm' if (offset === -2) return 'xs' if (offset === 1) return 'lg' if (offset === 2) return 'xl' if (offset === 3) return '2xl' if (offset === 4) return '3xl' // For values smaller than xs if (offset < -2) { const xsCount = Math.abs(offset) - 1 return `${xsCount}xs` } // For values larger than 3xl if (offset > 4) { return `${offset - 1}xl` } return String(index) } export function getPart(name: string, i: number): string { let cleanName = name const parts = name.split('/') if (name.indexOf('/') !== -1) { cleanName = parts[parts.length - i] ?? name } return cleanName } export function groupColors(colors: ColorItem[]): Record> { const groupedColors: Record> = {} colors.forEach((color) => { const { name, value } = color const key = getPart(name, 2) if (!groupedColors[key]) { groupedColors[key] = {} } const cleanName = getPart(name, 1) if (cleanName === name) { groupedColors[key] = value } else { const current = groupedColors[key] if (typeof current === 'object') { current[cleanName] = value } } }) return groupedColors } export function cleanupTheme( theme: ThemeState, colorFormat: string, grouped: boolean, useRem: boolean = true, defaultBorderRadiusIndex: number | null = null ): CleanTheme { const cleanTheme: CleanTheme = {} // Handle colors if (theme.colors.length > 0) { const formattedColors = theme.colors.map(({ name, value }) => ({ name, value: formatColor(value, colorFormat), })) cleanTheme.colors = grouped ? groupColors(formattedColors) : Object.assign({}, ...formattedColors.map((c) => ({ [c.name]: c.value }))) } // Handle fontFamily if (theme.fontFamily.length > 0) { cleanTheme.fontFamily = Object.assign( {}, ...theme.fontFamily.map(({ name, value }) => ({ [name]: value })) ) } // Handle fontSize if (theme.fontSize.length > 0) { cleanTheme.fontSize = Object.assign( {}, ...theme.fontSize.map(({ name, value }) => ({ [name]: useRem ? `${Number(value) / 16}rem` : `${value}px`, })) ) } // Handle boxShadow if (theme.boxShadow.length > 0) { cleanTheme.boxShadow = Object.assign( {}, ...theme.boxShadow.map(({ name, value }) => ({ [name]: value })) ) } // Handle borderRadius if (theme.borderRadius.length > 0) { cleanTheme.borderRadius = Object.assign( {}, ...theme.borderRadius.map(({ name, value }, index) => { const radiusName = name || getBorderRadiusName(index, defaultBorderRadiusIndex, theme.borderRadius.length) // Convert to rem or keep as px based on preference const numValue = typeof value === 'number' ? value : parseFloat(String(value)) const radiusValue = useRem ? `${numValue / 16}rem` : `${numValue}px` return { [radiusName]: radiusValue } }) ) } return cleanTheme } export function rgbToHex(int: number): string { let hex = Number(int).toString(16) if (hex.length < 2) { hex = `0${hex}` } return hex } export function makeHex(r: number, g: number, b: number): string { const red = rgbToHex(r) const green = rgbToHex(g) const blue = rgbToHex(b) return `#${red}${green}${blue}` } export function makeRgb(color: { r: number; g: number; b: number; a?: number }): { r: number g: number b: number a: number } { const r = Math.round(255 * color.r) const g = Math.round(255 * color.g) const b = Math.round(255 * color.b) const a = Math.round(100 * (color.a ?? 1)) / 100 return { r, g, b, a } } export function formatTailwind4Theme(cleanTheme: CleanTheme): string { const prefixMap: Record = { colors: 'color', fontFamily: 'font', fontSize: 'text', fontWeight: 'font-weight', letterSpacing: 'tracking', lineHeight: 'leading', borderRadius: 'radius', boxShadow: 'shadow', } const lines: string[] = [] const flattenColors = (obj: Record>) => { for (const [colorName, value] of Object.entries(obj)) { if (typeof value === 'string') { lines.push(` --color-${colorName}: ${value};`) } else if (typeof value === 'object' && value !== null) { for (const [variant, val] of Object.entries(value)) { const safeVariant = variant.replace('/', '-') lines.push(` --color-${colorName}-${safeVariant}: ${val};`) } } } } for (const [section, values] of Object.entries(cleanTheme)) { const ns = prefixMap[section] if (!ns || typeof values !== 'object') continue if (section === 'colors') { flattenColors(values as Record>) continue } if (section === 'boxShadow') { for (const [key, val] of Object.entries(values)) { if (!val) continue const cleanKey = key.replace(/^shadow-/, '') lines.push(` --shadow-${cleanKey}: ${val};`) } continue } if (section === 'borderRadius') { for (const [key, val] of Object.entries(values)) { // DEFAULT becomes --radius (no suffix), others get the key as suffix const varName = key === 'DEFAULT' ? '--radius' : `--radius-${key}` lines.push(` ${varName}: ${val};`) } continue } for (const [key, val] of Object.entries(values)) { lines.push(` --${ns}-${key}: ${val};`) } } return `@theme {\n${lines.join('\n')}\n}` } export function formatTailwind3Theme(cleanTheme: CleanTheme): string { const themeObj: Record = {} if (cleanTheme.colors) { themeObj.colors = cleanTheme.colors } if (cleanTheme.fontFamily) { themeObj.fontFamily = cleanTheme.fontFamily } if (cleanTheme.fontSize) { themeObj.fontSize = cleanTheme.fontSize } if (cleanTheme.boxShadow) { themeObj.boxShadow = cleanTheme.boxShadow } if (cleanTheme.borderRadius) { themeObj.borderRadius = cleanTheme.borderRadius } const formatValue = (val: unknown, indent = 2): string => { if (typeof val === 'string') { return `'${val}'` } if (typeof val === 'number') { return String(val) } if (Array.isArray(val)) { return `[${val.map((v) => formatValue(v)).join(', ')}]` } if (typeof val === 'object' && val !== null) { const entries = Object.entries(val) if (entries.length === 0) return '{}' const spaces = ' '.repeat(indent) const innerSpaces = ' '.repeat(indent + 2) const lines = entries.map(([k, v]) => { const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `'${k}'` return `${innerSpaces}${key}: ${formatValue(v, indent + 2)}` }) return `{\n${lines.join(',\n')}\n${spaces}}` } return String(val) } return `module.exports = { theme: { extend: ${formatValue(themeObj, 4)} } }` } ================================================ FILE: src/ui/helpers/randomMessages.ts ================================================ const messages = { emptyColors: [ 'Ooow, no colors?', 'hmmm, I see you have no colors', 'Care to add some colors?', 'Add some colors to brighten your day', ], emptyFamilies: [ "I guess not every project needs a font family", 'Default font stacks can do just fine', 'Always time to add them later', ], emptySizes: [ 'No sizes? no problem!', "Ah, you're going with the default font sizes, neat!", 'No font sizes detected', ], emptyShadows: [ 'No shadows? Smoooth', 'As he faced the sun he cast no shadow', 'Who needs shadows anyway', ], emptyBorderRadii: ['Pretty square, right?', 'No curves? No problemo'], } export default messages ================================================ FILE: src/ui/icons/Github.tsx ================================================ const Github = () => { return ( ) } export default Github ================================================ FILE: src/ui/icons/Tailwind.tsx ================================================ const Tailwind = () => { return ( ) } export default Tailwind ================================================ FILE: src/ui/main.tsx ================================================ import { createRoot } from 'react-dom/client' import { useThemeStore } from './store/themeStore' import App from './App' import './styles/app.css' import type { ThemeState } from '../shared/types' // Listen for the initial message from the Figma plugin window.onmessage = (event: MessageEvent) => { const theme = event.data.pluginMessage as Partial useThemeStore.getState().initializeTheme(theme) const container = document.getElementById('app') if (container) { const root = createRoot(container) root.render() } // Only process the first message window.onmessage = null } ================================================ FILE: src/ui/store/themeStore.ts ================================================ import { create } from 'zustand' import type { ThemeState, ColorItem, FontSizeItem, FontFamilyItem, ShadowItem, BorderRadiusItem, Preferences, } from '../../shared/types' interface ThemeActions { initializeTheme: (theme: Partial) => void setColors: (colors: ColorItem[]) => void setGradientColors: (gradientColors: string[]) => void setFontSize: (fontSize: FontSizeItem[]) => void setFontFamily: (fontFamily: FontFamilyItem[]) => void setBoxShadow: (boxShadow: ShadowItem[]) => void setBorderRadius: (borderRadius: BorderRadiusItem[]) => void setBaseFontSize: (baseFontSize: number | false) => void setDefaultBorderRadiusIndex: (index: number | null) => void setPreferences: (preferences: Partial) => void updateColor: (index: number, color: ColorItem) => void removeColor: (index: number) => void } const defaultPreferences: Preferences = { tailwindVersion: '4', colorFormat: 'hex', grouped: false, useRem: true, } export const useThemeStore = create((set) => ({ // State colors: [], gradientColors: [], fontSize: [], fontFamily: [], boxShadow: [], borderRadius: [], baseFontSize: false, defaultBorderRadiusIndex: null, preferences: defaultPreferences, // Actions initializeTheme: (theme) => set((state) => ({ ...state, ...theme, preferences: { ...defaultPreferences, ...theme.preferences, }, })), setColors: (colors) => set({ colors }), setGradientColors: (gradientColors) => set({ gradientColors }), setFontSize: (fontSize) => set({ fontSize }), setFontFamily: (fontFamily) => set({ fontFamily }), setBoxShadow: (boxShadow) => set({ boxShadow }), setBorderRadius: (borderRadius) => set({ borderRadius }), setBaseFontSize: (baseFontSize) => set({ baseFontSize }), setDefaultBorderRadiusIndex: (index) => set({ defaultBorderRadiusIndex: index }), setPreferences: (prefs) => set((state) => ({ preferences: { ...state.preferences, ...prefs }, })), updateColor: (index, color) => set((state) => { const newColors = [...state.colors] newColors[index] = color return { colors: newColors } }), removeColor: (index) => set((state) => ({ colors: state.colors.filter((_, i) => i !== index), })), })) ================================================ FILE: src/ui/styles/app.css ================================================ @import "tailwindcss"; @theme { --color-code-black: #2a2734; --color-code-green: #b5f4a5; --color-code-yellow: #ffe484; --color-code-purple: #d9a9ff; --color-code-red: #ff8383; --color-code-blue: #93ddfd; --color-code-white: #fff; --color-tailwind: #2e3748; } /* Base styles */ html { font-size: 100%; } body { @apply bg-white p-0 m-0 font-sans leading-normal text-slate-900 text-sm; } /* Typography utilities */ @layer base { .t-alpha { @apply text-3xl; } .t-beta { @apply text-base font-semibold text-slate-900; } .t-gamma { @apply text-sm text-slate-900 font-medium leading-tight; } .intro { @apply text-sm text-slate-500; } } /* Color swatch */ .color { height: 32px; width: 32px; min-width: 32px; box-sizing: border-box; @apply rounded mr-3 flex-shrink-0; } /* Code block */ .code-block { @apply bg-code-black rounded-lg p-4 text-sm; } /* Switch toggle */ .switch > div { @apply transition duration-100 ease-out; transform: translateX(0%); } .switch.active > div { transform: translateX(100%); } /* Form styles */ .form-field { @apply flex-1; } .form-label-wrapper { @apply mb-2 flex flex-col items-start; } .form-label { @apply leading-tight; } .form-instructions { @apply text-sm; } .form-control { @apply block w-full py-0 px-2 border border-slate-200 leading-normal rounded bg-white text-slate-900 text-sm shadow-sm; height: 36px; } .form-control:focus { @apply outline-none border-slate-400 ring-1 ring-slate-400; } .form-control--textarea { height: auto; } /* Button styles */ .button { @apply inline-flex justify-center items-center rounded-md px-3 font-medium leading-tight focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-1 text-sm h-8; transition: 0.15s ease-in; } .button--white { @apply bg-white text-slate-700 font-medium hover:bg-slate-50 border border-slate-200; } .button--grey { @apply bg-transparent text-slate-700 font-medium hover:bg-slate-100 border border-slate-200; } .button--blue { @apply bg-slate-100 text-slate-900 hover:bg-slate-200; } .button--green { @apply bg-slate-900 text-white hover:bg-slate-800; } .button--disabled { @apply bg-slate-200 text-slate-400; } .text-link { @apply text-slate-600 hover:text-slate-900 underline; } /* Rich text styles */ .richtext { @apply text-slate-600 text-sm; } .richtext h2:first-child, .richtext h3:first-child, .richtext p:first-child { @apply mt-0; } .richtext p { @apply mt-2; } .richtext h2 { @apply text-base font-semibold mt-3 text-slate-900; } .richtext h3 { @apply text-sm text-slate-900 font-medium leading-tight mt-3; } .richtext ul, .richtext ol { @apply mt-2; } .richtext ol { @apply list-decimal list-inside; } .richtext ul > li, .richtext ol > li { @apply relative mt-1; } .richtext ul > li { @apply pl-4; } .richtext ul > li:before { content: ''; top: 7px; left: 0; border-radius: 50%; width: 4px; height: 4px; @apply absolute bg-slate-400; } .richtext a { @apply text-slate-700 underline; } .richtext a:hover { @apply text-slate-900; } .richtext pre code { @apply p-3 my-3 block overflow-auto text-sm leading-normal bg-slate-100 rounded; } .richtext code { padding: 0.15em 0.35em; margin: 0; font-size: 85%; background-color: rgba(0, 0, 0, 0.04); border-radius: 3px; } /* Prism/Code highlighting - override prism-react-renderer theme */ pre[class*='language-'] { @apply p-4 rounded-lg text-sm font-mono; background: #1d1f21 !important; overflow: auto; line-height: 1.5; } code[class*='language-'] { @apply font-mono; background: transparent !important; } /* Select styling */ select { @apply border border-slate-200 rounded px-2 py-1.5 bg-white text-slate-900 text-sm shadow-sm h-8; } select:focus { @apply outline-none ring-1 ring-slate-400 border-slate-400; } /* Radio button styling */ input[type="radio"] { @apply mr-1; } /* Grid gaps */ .grid { @apply gap-3; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ================================================ FILE: ui.html ================================================ Figma TailwindCSS
================================================ FILE: vite.config.code.ts ================================================ import { defineConfig } from 'vite' import { resolve } from 'path' // Figma sandbox code build - outputs IIFE for code.js export default defineConfig({ build: { outDir: 'dist', emptyOutDir: false, lib: { entry: resolve(__dirname, 'src/code/index.ts'), name: 'code', fileName: () => 'code.js', formats: ['iife'], }, rollupOptions: { output: { inlineDynamicImports: true, }, }, target: 'es2020', minify: false, }, }) ================================================ FILE: vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import { viteSingleFile } from 'vite-plugin-singlefile' import { resolve } from 'path' // UI build config - outputs a single HTML file with inlined CSS/JS export default defineConfig({ plugins: [react(), tailwindcss(), viteSingleFile({ removeViteModuleLoader: true })], build: { outDir: 'dist', emptyOutDir: true, assetsInlineLimit: 100000000, chunkSizeWarningLimit: 100000000, cssCodeSplit: false, rollupOptions: { input: resolve(__dirname, 'ui.html'), output: { inlineDynamicImports: true, }, }, }, })