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<EffectStylesResult> {
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<NodeStylesResult> {
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<number>()
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<ColorResult> {
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<TextStylesResult> {
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<string, string | Record<string, string>>
fontFamily?: Record<string, string>
fontSize?: Record<string, string>
boxShadow?: Record<string, string>
borderRadius?: Record<string, string>
}
================================================
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 (
<>
<ScrollToTop />
<div className="p-3 flex-grow">
<Routes>
<Route path="/" element={<Preferences />} />
<Route path="/colors" element={<Colors />} />
<Route path="/typography" element={<Type />} />
<Route path="/effects" element={<Effects />} />
<Route path="/export" element={<Export />} />
<Route path="/info" element={<Info />} />
</Routes>
</div>
</>
)
}
const App = () => {
return (
<div className="min-h-full flex flex-col">
<MemoryRouter initialEntries={['/']}>
<AppContent />
</MemoryRouter>
<Footer />
</div>
)
}
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 (
<div className="flex mt-2 justify-between items-center gap-1.5">
<div style={{ backgroundColor: value }} className="color shadow-sm"></div>
<div className="form-field">
<input
type="text"
name="name"
placeholder={name}
defaultValue={name}
className="form-control"
onChange={setName}
onBlur={updateColor}
/>
</div>
<div className="form-field">
<input
type="text"
name="hex"
placeholder={value}
defaultValue={value}
className="form-control"
onChange={setValue}
onBlur={updateColor}
/>
</div>
<button
className="text-slate-400 hover:text-slate-600 p-1 flex-shrink-0"
onClick={handleRemove}
title="Remove color"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)
}
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 (
<>
<div className="w-full border-b border-slate-200 py-3">
<h2 className="t-beta">Colors</h2>
<p className="intro">Pick and choose the colors you want to use</p>
</div>
<div className="my-4 richtext">
<p>
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).
</p>
</div>
{hasColor ? (
colors.map((color, i) => (
<Color
key={i}
index={i}
onChange={handleColorChange}
removeColor={handleRemoveColor}
colorFormat={colorFormat}
{...color}
/>
))
) : (
<div className="richtext">
<p>{feedbackItem}</p>
</div>
)}
{hasGradients ? (
<div className="my-4">
<div className="richtext">
<h3 className="t-gamma">Gradients</h3>
<p>
We found some gradients in the document. Make sure to add the colors if
you want to use them in your theme.
</p>
</div>
{gradients.map((color, i) => (
<Gradient key={i} hex={color} />
))}
</div>
) : null}
<NewColor key={nextIndex} index={nextIndex} onChange={handleColorChange} />
<div className="flex justify-between mt-4">
<Link to="/" className="button button--green">
Previous
</Link>
<Link to="/typography" className="button button--green">
Next
</Link>
</div>
</>
)
}
export default Colors
================================================
FILE: src/ui/components/Colors/Gradient.tsx
================================================
interface GradientProps {
hex: string
}
function Gradient({ hex }: GradientProps) {
return (
<div className="flex mt-2 justify-start items-center">
<div style={{ backgroundColor: hex }} className="color shadow-sm"></div>
<div className="text-sm text-slate-600">{hex}</div>
</div>
)
}
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 (
<div className="mt-4 pt-3 border-t border-slate-100">
<div className="flex items-center gap-1.5">
<div
style={{ backgroundColor: value || '#e2e8f0', borderColor: value || '#e2e8f0' }}
className="color border border-dashed border-slate-300"
></div>
<div className="form-field">
<input
type="text"
className="form-control"
placeholder="Color name"
onChange={setName}
/>
</div>
<div className="form-field">
<input
type="text"
className="form-control"
placeholder="#hex"
onChange={setValue}
/>
</div>
<button
className="text-slate-400 hover:text-slate-600 p-1 flex-shrink-0"
onClick={addColor}
title="Add color"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
</div>
)
}
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 (
<div
className={`flex flex-col items-center text-center cursor-pointer group ${
isDefault ? 'ring-2 ring-slate-900 ring-offset-2 rounded' : ''
}`}
onClick={onSelect}
title={`Click to set as DEFAULT (${displayValue})`}
>
<div
className={`w-12 h-12 bg-slate-200 transition-colors ${
isDefault ? '' : 'group-hover:bg-slate-300'
}`}
style={{ borderRadius: numValue }}
></div>
<span
className={`block text-[10px] mt-1.5 truncate max-w-full ${
isDefault ? 'text-slate-900 font-medium' : 'text-slate-500'
}`}
>
{displayName}
</span>
<span className="block text-slate-400 text-[9px]">{displayValue}</span>
</div>
)
}
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 (
<>
<div className="w-full border-b border-slate-200 py-3">
<h2 className="t-beta">Effects</h2>
<p className="intro">Effect Styles we found in your Figma file</p>
</div>
<h3 className="t-gamma my-3">Border-radius</h3>
{hasBorderRadii && (
<p className="text-xs text-slate-500 mb-2">
Click a radius to set it as DEFAULT (used for <code className="bg-slate-100 px-1 rounded">rounded</code> class)
</p>
)}
{hasBorderRadii ? (
<div className="flex flex-wrap gap-4">
{borderRadii.map((radius, i) => (
<BorderRadius
key={i}
index={i}
value={radius.value}
isDefault={defaultBorderRadiusIndex === i}
defaultIndex={defaultBorderRadiusIndex}
total={borderRadii.length}
useRem={useRem}
onSelect={() => handleSelectDefault(i)}
/>
))}
</div>
) : (
<div className="mt-2">
<div className="richtext">
<p>{feedbackBorderRadii}</p>
</div>
</div>
)}
<h3 className="t-gamma my-3">Shadows</h3>
{hasShadows ? (
<div className="flex flex-wrap gap-4">
{shadows.map((shadow, i) => (
<Shadow key={i} {...shadow} />
))}
</div>
) : (
<div className="mt-2">
<div className="richtext">
<p>{feedbackShadows}</p>
</div>
</div>
)}
<div className="flex justify-between mt-4">
<Link to="/typography" className="button button--green">
Previous
</Link>
<Link to="/export" className="button button--green">
Next
</Link>
</div>
</>
)
}
export default Effects
================================================
FILE: src/ui/components/Effects/Shadow.tsx
================================================
interface ShadowProps {
name: string
value: string
}
const Shadow = ({ name, value }: ShadowProps) => {
return (
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 rounded bg-white" style={{ boxShadow: value }}></div>
<span className="block text-slate-500 text-[10px] mt-1.5 truncate max-w-full">{name}</span>
</div>
)
}
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 (
<>
<div className="flex">
<div className="w-full border-b border-slate-200 py-3">
<h2 className="t-beta">Export theme</h2>
<p className="intro">Almost there...</p>
</div>
</div>
<div className="mt-4 relative">
<Highlight
theme={themes.dracula}
code={markup}
language={isV4 ? 'css' : 'javascript'}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
{!isV4 && (
<div
className="absolute top-0 right-0 mr-3 mt-3 flex items-center cursor-pointer"
onClick={groupColors}
>
<div
className={`switch w-10 h-5 rounded-full cursor-pointer mr-3 ${switchClass}`}
>
<div className="w-1/2 h-full rounded-full shadow bg-white"></div>
</div>
<span className="text-slate-300 text-xs">{buttonText}</span>
</div>
)}
</div>
<div className="mt-4 flex items-center">
<button className="button button--grey mr-3" onClick={exportTheme}>
<svg
className="fill-current w-3.5 h-3.5 mr-1.5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z" />
</svg>
Create file
</button>
<a
className="button button--blue"
href="https://github.com/jan-dh/figma-tailwindcss/blob/master/README.md"
target="_blank"
rel="noreferrer"
>
How does it work?
</a>
</div>
<div className="flex justify-between mt-4">
<Link to="/effects" className="button button--green">
Previous
</Link>
</div>
</>
)
}
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 (
<>
<div className="flex">
<div className="w-full border-b border-gray-200 py-4">
<h2 className="t-beta">Usage</h2>
<p className="intro">Everything you need to know to get you started</p>
</div>
</div>
<div className="mt-8">
<div className="richtext">
<p>
Import the <code>theme.js</code> file in to your{' '}
<code>tailwind.config.js</code>
configuration file to use it:
</p>
<pre>
<code>import theme from {theme};</code>
</pre>
<h3 className="t-gamma">Overriding the default theme</h3>
<p>
To override an option in the default theme, create a theme section in your
config and add the key you would like to override.
</p>
<pre>
<code>{overWrite}</code>
</pre>
<h3 className="t-gamma">Add to current values</h3>
<p>
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.
</p>
<pre>
<code>{extend}</code>
</pre>
</div>
</div>
<div className="flex justify-between mt-8">
<Link to="/export" className="button button--green">
Previous
</Link>
</div>
</>
)
}
export default Info
================================================
FILE: src/ui/components/Footer/Footer.tsx
================================================
import Tailwind from '../../icons/Tailwind'
import Github from '../../icons/Github'
const Footer = () => {
return (
<div className="w-full bg-slate-50 py-2.5 border-t border-slate-100">
<div className="px-3 flex text-xs text-slate-500 font-medium">
<div className="w-1/2">
<a
href="https://tailwindcss.com/"
rel="noopener noreferrer"
target="_blank"
className="inline-flex items-center hover:text-slate-700"
>
<Tailwind />
<span className="ml-1.5">tailwindcss</span>
</a>
</div>
<div className="w-1/2 text-right">
<a
href="https://github.com/jan-dh/figma-tailwindcss/"
rel="noopener noreferrer"
target="_blank"
className="inline-flex items-center hover:text-slate-700"
>
<Github />
<span className="ml-1.5">Github</span>
</a>
</div>
</div>
</div>
)
}
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 (
<>
<div className="w-full border-b border-slate-200 py-3">
<h2 className="t-beta">Preferences</h2>
<p className="intro">Configure how Tailwind CSS should be generated</p>
</div>
<div className="my-4 richtext">
<h3 className="t-gamma">Tailwind version</h3>
<p>Choose which version of Tailwind to target.</p>
<div className="mt-2 space-x-4">
<label>
<input
type="radio"
name="tailwindVersion"
value="3"
checked={preferences.tailwindVersion === '3'}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
updatePreference('tailwindVersion', e.target.value as '3' | '4')
}
/>{' '}
Tailwind 3
</label>
<label>
<input
type="radio"
name="tailwindVersion"
value="4"
checked={preferences.tailwindVersion === '4'}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
updatePreference('tailwindVersion', e.target.value as '3' | '4')
}
/>{' '}
Tailwind 4
</label>
</div>
</div>
<div className="my-4 richtext">
<h3 className="t-gamma">Color format</h3>
<p>Select how colors should be exported.</p>
<select
className="mt-2"
value={preferences.colorFormat}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
updatePreference('colorFormat', e.target.value)
}
>
<option value="hex">Hex (#rrggbb)</option>
<option value="rgba">RGBA</option>
<option value="hsl">HSL</option>
<option value="oklch">OKLCH</option>
</select>
</div>
<div className="my-4 richtext">
<h3 className="t-gamma">Font size unit</h3>
<p>Choose whether font sizes should be converted to REM or kept as pixels.</p>
<div className="mt-2 space-x-4">
<label>
<input
type="radio"
name="useRem"
value="true"
checked={preferences.useRem === true}
onChange={() => updatePreference('useRem', true)}
/>{' '}
REM (relative)
</label>
<label>
<input
type="radio"
name="useRem"
value="false"
checked={preferences.useRem === false}
onChange={() => updatePreference('useRem', false)}
/>{' '}
Pixels (absolute)
</label>
</div>
</div>
<div className="flex justify-end mt-4">
<Link to="/colors" className="button button--green">
Next
</Link>
</div>
</>
)
}
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 (
<div className="code-block text-white mt-4">
{fontFamilies.map((family, i) => (
<FontFamily key={i} {...family} />
))}
</div>
)
}
export default FontFamilies
================================================
FILE: src/ui/components/Type/FontFamily.tsx
================================================
interface FontFamilyProps {
name: string
value: string
}
const FontFamily = ({ name, value }: FontFamilyProps) => {
return (
<div className="flex justify-between">
<span>{name}</span>
<span className="text-code-green">{value}</span>
</div>
)
}
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 (
<div className="flex justify-between items-center leading-none">
<p style={fontSize} className="text-code-white">
{value}px<span className="ml-4 text-code-red">- {remSize}rem</span>
</p>
<span className="text-code-green">{name}</span>
</div>
)
}
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<HTMLSelectElement>) => {
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 (
<>
<label htmlFor="sizeSelect" className="block mt-4">
Pick a base font-size
</label>
<select
className="mt-2 border rounded p-2 focus-visible:outline-none focus:ring-1 focus:ring-teal-500"
value={baseFontSize || ''}
onChange={updateFontSizes}
>
<option value="">-</option>
{fontSizes.map((size, i) => (
<option value={size.value} key={i}>
{size.value}px
</option>
))}
</select>
<div className="mt-4 code-block">
{fontSizes.map((size, i) => (
<FontSize key={i} {...size} />
))}
</div>
</>
)
}
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<HTMLAnchorElement>) => {
if (!baseFontSize && fontSizes.length > 0) {
e.preventDefault()
}
}
const buttonClass =
baseFontSize || !hasSizes
? 'button button--green'
: 'button button--green opacity-50'
return (
<>
<div className="w-full border-b border-slate-200 py-3">
<h2 className="t-beta">Typography</h2>
<p className="intro">Font families and font sizes</p>
</div>
<h3 className="t-gamma mt-3">Font families</h3>
{hasFamilies ? (
<FontFamilies />
) : (
<div className="richtext mt-2">
<p>{emptyFamily}</p>
</div>
)}
<div className="mt-4">
<h3 className="t-gamma">Font sizes</h3>
{hasSizes ? (
<FontSizes />
) : (
<div className="richtext mt-2">
<p>{emptySize}</p>
</div>
)}
</div>
<div className="flex justify-between mt-4">
<Link to="/colors" className="button button--green">
Previous
</Link>
<Link to="/effects" className={buttonClass} onClick={validateFontSize}>
Next
</Link>
</div>
</>
)
}
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<HTMLInputElement>) => void] {
const [value, setValue] = useState(initialValue)
function handleChange(e: ChangeEvent<HTMLInputElement>) {
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<string, string | Record<string, string>> {
const groupedColors: Record<string, string | Record<string, string>> = {}
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<string, string> = {
colors: 'color',
fontFamily: 'font',
fontSize: 'text',
fontWeight: 'font-weight',
letterSpacing: 'tracking',
lineHeight: 'leading',
borderRadius: 'radius',
boxShadow: 'shadow',
}
const lines: string[] = []
const flattenColors = (obj: Record<string, string | Record<string, string>>) => {
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<string, string | Record<string, string>>)
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<string, unknown> = {}
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 (
<svg
height="18"
width="18"
viewBox="0 0 16 16"
version="1.1"
aria-hidden="true"
>
<path
fillRule="evenodd"
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
></path>
</svg>
)
}
export default Github
================================================
FILE: src/ui/icons/Tailwind.tsx
================================================
const Tailwind = () => {
return (
<svg
width="22"
height="18"
viewBox="0 0 18 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.5 4.2C5.09991 1.80009 6.60009 0.599998 9 0.599998C12.6 0.599998 13.05 3.3 14.85 3.75C16.0501 4.05009 17.1 3.60009 18 2.4C17.4001 4.79991 15.8999 6 13.5 6C9.9 6 9.45 3.3 7.65 2.85C6.44991 2.5499 5.4 2.9999 4.5 4.2ZM0 9.6C0.599906 7.20009 2.10009 6 4.5 6C8.1 6 8.55 8.7 10.35 9.15C11.5501 9.45009 12.6 9.00009 13.5 7.8C12.9001 10.1999 11.3999 11.4 9 11.4C5.4 11.4 4.95 8.7 3.15 8.25C1.94991 7.94991 0.9 8.39991 0 9.6Z"
fill="url(#paint0_linear)"
/>
<defs>
<linearGradient
id="paint0_linear"
x1="4.47292e-07"
y1="-8.99991"
x2="18"
y2="20.9999"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2383AE" />
<stop offset="1" stopColor="#6DD7B9" />
</linearGradient>
</defs>
</svg>
)
}
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<ThemeState>
useThemeStore.getState().initializeTheme(theme)
const container = document.getElementById('app')
if (container) {
const root = createRoot(container)
root.render(<App />)
}
// 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<ThemeState>) => 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<Preferences>) => 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<ThemeState & ThemeActions>((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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Figma TailwindCSS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/ui/main.tsx"></script>
</body>
</html>
================================================
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,
},
},
},
})
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
SYMBOL INDEX (52 symbols across 18 files)
FILE: src/code/figma/effectStyles.ts
type EffectStylesResult (line 3) | interface EffectStylesResult {
function getEffectStyles (line 7) | async function getEffectStyles(): Promise<EffectStylesResult> {
FILE: src/code/figma/helpers.ts
function rgbToHex (line 1) | function rgbToHex(int: number): string {
function makeHex (line 9) | function makeHex(r: number, g: number, b: number): string {
function makeRgb (line 16) | function makeRgb(color: RGB | RGBA): { r: number; g: number; b: number; ...
FILE: src/code/figma/nodeStyles.ts
type NodeStylesResult (line 1) | interface NodeStylesResult {
function getNodeStyles (line 5) | async function getNodeStyles(): Promise<NodeStylesResult> {
FILE: src/code/figma/paintStyles.ts
type ColorResult (line 3) | interface ColorResult {
function getPaintStyles (line 8) | async function getPaintStyles(): Promise<ColorResult> {
FILE: src/code/figma/textStyles.ts
type TextStylesResult (line 1) | interface TextStylesResult {
function getTextStyles (line 6) | async function getTextStyles(): Promise<TextStylesResult> {
FILE: src/code/index.ts
type Theme (line 6) | interface Theme {
constant UI_WIDTH (line 36) | const UI_WIDTH = 420
constant UI_MIN_HEIGHT (line 37) | const UI_MIN_HEIGHT = 200
constant UI_MAX_HEIGHT (line 38) | const UI_MAX_HEIGHT = 600
FILE: src/shared/types.ts
type ColorItem (line 1) | interface ColorItem {
type FontSizeItem (line 6) | interface FontSizeItem {
type FontFamilyItem (line 11) | interface FontFamilyItem {
type ShadowItem (line 16) | interface ShadowItem {
type BorderRadiusItem (line 21) | interface BorderRadiusItem {
type Preferences (line 26) | interface Preferences {
type ThemeState (line 33) | interface ThemeState {
type CleanTheme (line 45) | type CleanTheme = {
FILE: src/ui/components/Colors/Color.tsx
type ColorProps (line 4) | interface ColorProps extends ColorItem {
function Color (line 11) | function Color({ name: initialName, value: initialValue, index, onChange...
FILE: src/ui/components/Colors/Gradient.tsx
type GradientProps (line 1) | interface GradientProps {
function Gradient (line 5) | function Gradient({ hex }: GradientProps) {
FILE: src/ui/components/Colors/NewColor.tsx
type NewColorProps (line 4) | interface NewColorProps {
FILE: src/ui/components/Effects/BorderRadius.tsx
type BorderRadiusProps (line 3) | interface BorderRadiusProps {
FILE: src/ui/components/Effects/Shadow.tsx
type ShadowProps (line 1) | interface ShadowProps {
FILE: src/ui/components/Type/FontFamily.tsx
type FontFamilyProps (line 1) | interface FontFamilyProps {
FILE: src/ui/components/Type/FontSize.tsx
type FontSizeProps (line 1) | interface FontSizeProps {
FILE: src/ui/helpers/colorFormatter.ts
function sRGBToLinear (line 2) | function sRGBToLinear(c: number): number {
function parseRGB (line 8) | function parseRGB(input: string | number[]): [number, number, number, nu...
function rgbToHsl (line 36) | function rgbToHsl([r, g, b]: [number, number, number]): [number, number,...
function rgbToOklab (line 65) | function rgbToOklab([r, g, b]: [number, number, number]): [number, numbe...
function oklabToOklch (line 85) | function oklabToOklch([L, a, b]: [number, number, number]): [number, num...
function formatColor (line 93) | function formatColor(input: string | number[], format: string = 'hex'): ...
FILE: src/ui/helpers/customHooks.ts
function useInput (line 5) | function useInput(initialValue: string): [string, (e: ChangeEvent<HTMLIn...
function ScrollToTop (line 13) | function ScrollToTop(): null {
function useAutoResize (line 23) | function useAutoResize(): void {
FILE: src/ui/helpers/helpers.ts
function calculatePosition (line 4) | function calculatePosition(index: number, basePosition: number, length: ...
function getBorderRadiusName (line 29) | function getBorderRadiusName(
function getPart (line 63) | function getPart(name: string, i: number): string {
function groupColors (line 72) | function groupColors(colors: ColorItem[]): Record<string, string | Recor...
function cleanupTheme (line 93) | function cleanupTheme(
function rgbToHex (line 157) | function rgbToHex(int: number): string {
function makeHex (line 165) | function makeHex(r: number, g: number, b: number): string {
function makeRgb (line 172) | function makeRgb(color: { r: number; g: number; b: number; a?: number }): {
function formatTailwind4Theme (line 185) | function formatTailwind4Theme(cleanTheme: CleanTheme): string {
function formatTailwind3Theme (line 247) | function formatTailwind3Theme(cleanTheme: CleanTheme): string {
FILE: src/ui/store/themeStore.ts
type ThemeActions (line 12) | interface ThemeActions {
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
{
"path": ".editorconfig",
"chars": 280,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 408,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG]\"\nlabels: ''\nassignees: ''\n\n---\n\n---\nname: B"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 604,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[Feature]\"\nlabels: ''\nassignees: ''\n\n---\n\n**Is"
},
{
"path": ".gitignore",
"chars": 158,
"preview": "# build artifacts\ndist\n\n# dependencies\nnode_modules\n.npm\nnpm-debug.log\nbun.lockb\n\n# editor\n.DS_Store\n.idea\n.vscode\n*.swp"
},
{
"path": "Plan.md",
"chars": 7832,
"preview": "# Figma TailwindCSS Plugin Modernization\n\n## Current Task: UI Visual Refresh (shadcn-style Design)\n\n### Status: Complete"
},
{
"path": "README.md",
"chars": 2771,
"preview": "# Figma Tailwindcss\n\nA plugin that tries to bridge the gap between design and code. Figma Tailwindcss lets you export as"
},
{
"path": "manifest.json",
"chars": 310,
"preview": "{\n \"name\": \"Figma Tailwindcss\",\n \"id\": \"785619431629077634\",\n \"api\": \"1.0.0\",\n \"main\": \"dist/code.js\",\n \""
},
{
"path": "package.json",
"chars": 1355,
"preview": "{\n \"name\": \"figma-tailwindcss\",\n \"version\": \"2.1.0\",\n \"type\": \"module\",\n \"description\": \"Export Figma styles to Tail"
},
{
"path": "src/code/figma/effectStyles.ts",
"chars": 1027,
"preview": "import { makeRgb } from './helpers'\n\ninterface EffectStylesResult {\n shadows: { name: string; value: string }[]\n}\n\nexpo"
},
{
"path": "src/code/figma/helpers.ts",
"chars": 630,
"preview": "export function rgbToHex(int: number): string {\n let hex = Number(int).toString(16)\n if (hex.length < 2) {\n hex = `"
},
{
"path": "src/code/figma/nodeStyles.ts",
"chars": 974,
"preview": "interface NodeStylesResult {\n finalRadii: { name: string; value: number }[]\n}\n\nexport default async function getNodeSty"
},
{
"path": "src/code/figma/paintStyles.ts",
"chars": 1225,
"preview": "import { makeHex, makeRgb } from './helpers'\n\ninterface ColorResult {\n colors: { name: string; value: string }[]\n grad"
},
{
"path": "src/code/figma/textStyles.ts",
"chars": 1161,
"preview": "interface TextStylesResult {\n finalSizes: { name: string; value: string }[]\n finalFamilies: { name: string; value: str"
},
{
"path": "src/code/index.ts",
"chars": 1942,
"preview": "import getPaintStyles from './figma/paintStyles'\nimport getTextStyles from './figma/textStyles'\nimport getEffectStyles f"
},
{
"path": "src/shared/types.ts",
"chars": 1038,
"preview": "export interface ColorItem {\n name: string\n value: string\n}\n\nexport interface FontSizeItem {\n name: string\n value: s"
},
{
"path": "src/ui/App.tsx",
"chars": 1217,
"preview": "import { MemoryRouter, Routes, Route } from 'react-router-dom'\nimport { ScrollToTop, useAutoResize } from './helpers/cus"
},
{
"path": "src/ui/components/Colors/Color.tsx",
"chars": 1776,
"preview": "import { useInput } from '../../helpers/customHooks'\nimport type { ColorItem } from '../../../shared/types'\n\ninterface C"
},
{
"path": "src/ui/components/Colors/Colors.tsx",
"chars": 2920,
"preview": "import { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../store/themeStore'\nimport Color from './Colo"
},
{
"path": "src/ui/components/Colors/Gradient.tsx",
"chars": 336,
"preview": "interface GradientProps {\n hex: string\n}\n\nfunction Gradient({ hex }: GradientProps) {\n return (\n <div className=\"fl"
},
{
"path": "src/ui/components/Colors/NewColor.tsx",
"chars": 1611,
"preview": "import { useInput } from '../../helpers/customHooks'\nimport type { ColorItem } from '../../../shared/types'\n\ninterface N"
},
{
"path": "src/ui/components/Effects/BorderRadius.tsx",
"chars": 1395,
"preview": "import { getBorderRadiusName } from '../../helpers/helpers'\n\ninterface BorderRadiusProps {\n index: number\n value: numb"
},
{
"path": "src/ui/components/Effects/Effects.tsx",
"chars": 3004,
"preview": "import { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../store/themeStore'\nimport Shadow from './Sha"
},
{
"path": "src/ui/components/Effects/Shadow.tsx",
"chars": 405,
"preview": "interface ShadowProps {\n name: string\n value: string\n}\n\nconst Shadow = ({ name, value }: ShadowProps) => {\n return (\n"
},
{
"path": "src/ui/components/Export/Export.tsx",
"chars": 3798,
"preview": "import { Link } from 'react-router-dom'\nimport { Highlight, themes } from 'prism-react-renderer'\nimport { saveAs } from "
},
{
"path": "src/ui/components/Export/Info.tsx",
"chars": 1801,
"preview": "import { Link } from 'react-router-dom'\n\nconst Info = () => {\n const theme = `'./theme'`\n const overWrite = `module.ex"
},
{
"path": "src/ui/components/Footer/Footer.tsx",
"chars": 1035,
"preview": "import Tailwind from '../../icons/Tailwind'\nimport Github from '../../icons/Github'\n\nconst Footer = () => {\n return (\n "
},
{
"path": "src/ui/components/Preferences/Preferences.tsx",
"chars": 3287,
"preview": "import type { ChangeEvent } from 'react'\nimport { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../st"
},
{
"path": "src/ui/components/Type/FontFamilies.tsx",
"chars": 382,
"preview": "import { useThemeStore } from '../../store/themeStore'\nimport FontFamily from './FontFamily'\n\nconst FontFamilies = () =>"
},
{
"path": "src/ui/components/Type/FontFamily.tsx",
"chars": 300,
"preview": "interface FontFamilyProps {\n name: string\n value: string\n}\n\nconst FontFamily = ({ name, value }: FontFamilyProps) => {"
},
{
"path": "src/ui/components/Type/FontSize.tsx",
"chars": 527,
"preview": "interface FontSizeProps {\n name: string\n value: string\n}\n\nconst FontSize = ({ name, value }: FontSizeProps) => {\n con"
},
{
"path": "src/ui/components/Type/FontSizes.tsx",
"chars": 1543,
"preview": "import type { ChangeEvent } from 'react'\nimport { useThemeStore } from '../../store/themeStore'\nimport FontSize from './"
},
{
"path": "src/ui/components/Type/Type.tsx",
"chars": 2001,
"preview": "import type { MouseEvent } from 'react'\nimport { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../sto"
},
{
"path": "src/ui/helpers/colorFormatter.ts",
"chars": 3752,
"preview": "// Converts sRGB (0-255) to linear RGB (0-1)\nfunction sRGBToLinear(c: number): number {\n c /= 255\n return c <= 0.04045"
},
{
"path": "src/ui/helpers/customHooks.ts",
"chars": 1217,
"preview": "import { useState, useEffect, useCallback } from 'react'\nimport type { ChangeEvent } from 'react'\nimport { useLocation }"
},
{
"path": "src/ui/helpers/helpers.ts",
"chars": 8284,
"preview": "import { formatColor } from './colorFormatter'\nimport type { ThemeState, CleanTheme, ColorItem } from '../../shared/type"
},
{
"path": "src/ui/helpers/randomMessages.ts",
"chars": 703,
"preview": "const messages = {\n emptyColors: [\n 'Ooow, no colors?',\n 'hmmm, I see you have no colors',\n 'Care to add some "
},
{
"path": "src/ui/icons/Github.tsx",
"chars": 858,
"preview": "const Github = () => {\n return (\n <svg\n height=\"18\"\n width=\"18\"\n viewBox=\"0 0 16 16\"\n version=\"1"
},
{
"path": "src/ui/icons/Tailwind.tsx",
"chars": 1038,
"preview": "const Tailwind = () => {\n return (\n <svg\n width=\"22\"\n height=\"18\"\n viewBox=\"0 0 18 12\"\n fill=\"no"
},
{
"path": "src/ui/main.tsx",
"chars": 618,
"preview": "import { createRoot } from 'react-dom/client'\nimport { useThemeStore } from './store/themeStore'\nimport App from './App'"
},
{
"path": "src/ui/store/themeStore.ts",
"chars": 2353,
"preview": "import { create } from 'zustand'\nimport type {\n ThemeState,\n ColorItem,\n FontSizeItem,\n FontFamilyItem,\n ShadowItem"
},
{
"path": "src/ui/styles/app.css",
"chars": 3992,
"preview": "@import \"tailwindcss\";\n\n@theme {\n --color-code-black: #2a2734;\n --color-code-green: #b5f4a5;\n --color-code-yellow: #f"
},
{
"path": "tsconfig.json",
"chars": 736,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n "
},
{
"path": "ui.html",
"chars": 306,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "vite.config.code.ts",
"chars": 496,
"preview": "import { defineConfig } from 'vite'\nimport { resolve } from 'path'\n\n// Figma sandbox code build - outputs IIFE for code."
},
{
"path": "vite.config.ts",
"chars": 686,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tailwindcss from '@tailwindcss/vite'"
}
]
About this extraction
This page contains the full source code of the jan-dh/figma-tailwindcss GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (72.4 KB), approximately 22.2k tokens, and a symbol index with 52 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.