Full Code of jan-dh/figma-tailwindcss for AI

master 2ddf895f842c cached
45 files
72.4 KB
22.2k tokens
52 symbols
1 requests
Download .txt
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,
      },
    },
  },
})
Download .txt
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
Download .txt
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.

Copied to clipboard!