[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\n\n[*.js]\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.yml]\nindent_size = 2\n\n[{package.json,.babelrc}]\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG]\"\nlabels: ''\nassignees: ''\n\n---\n\n---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: jan-dh\n\n---\n\n### Description\n\n\n### Steps to reproduce\n1.\n2.\n\n### Additional info\n- Figma version:\n- OS version:\n\n### Extra\nSharing the design file where you encountered the issue makes it easier to debug\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[Feature]\"\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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\n*.swo\n\n# environment\n.env\n.env.local\n"
  },
  {
    "path": "Plan.md",
    "content": "# Figma TailwindCSS Plugin Modernization\n\n## Current Task: UI Visual Refresh (shadcn-style Design)\n\n### Status: Complete\n\n## Phases\n\n### Phase 1: Project Setup ✅\n- [x] Create Plan.md\n- [x] Initialize with Bun\n- [x] Install dependencies (React 18, Zustand, Vite, Tailwind 4, etc.)\n- [x] Create new project structure\n\n### Phase 2: Vite Configuration ✅\n- [x] Create `vite.config.ts` for UI build (with vite-plugin-singlefile)\n- [x] Create `vite.config.code.ts` for Figma sandbox code (IIFE output)\n- [x] Update package.json scripts\n\n### Phase 3: React 18 Migration ✅\n- [x] Create new entry point (`src/ui/main.tsx`) with createRoot\n- [x] Update to React 18 createRoot API\n- [x] Migrate to React Router v6 (Routes/Route pattern)\n\n### Phase 4: State Management (reactn → Zustand) ✅\n- [x] Create Zustand store (`src/ui/store/themeStore.ts`)\n- [x] Define types in `src/shared/types.ts`\n\n### Phase 5: Component Migration ✅\n- [x] Migrate shared types\n- [x] Migrate helper functions (colorFormatter, helpers, customHooks)\n- [x] Migrate leaf components (Footer, Color, Shadow, FontSize, etc.)\n- [x] Migrate feature components (Colors, Type, Effects, Export, Preferences)\n- [x] Migrate App with React Router v6\n\n### Phase 6: Tailwind 4 for Plugin UI ✅\n- [x] Create new CSS setup with @theme directive\n- [x] Removed old postcss.config.cjs and tailwind.config.cjs\n\n### Phase 7: Export Logic ✅\n- [x] TW4 output: `@theme {}` CSS format\n- [x] TW3 output: `module.exports = { theme: { extend: {} } }` JS format\n- [x] Updated prism-react-renderer to v2\n\n### Phase 8: Figma Code Migration ✅\n- [x] Migrate `src/js/code.js` → `src/code/index.ts`\n- [x] Migrate Figma helpers to TypeScript (paintStyles, textStyles, effectStyles, nodeStyles)\n\n### Phase 9: Verification ✅\n- [x] Build completes without errors\n- [x] dist/code.js is valid IIFE (5KB)\n- [x] dist/ui.html has inlined CSS/JS (292KB - well under 5MB limit)\n- [ ] Test in Figma (manual verification needed)\n\n## Build Output\n\n```\ndist/\n├── code.js    (5.15 KB)  - Figma sandbox code\n└── ui.html    (292.81 KB) - UI with inlined CSS/JS\n```\n\n## New Project Structure\n\n```\nsrc/\n├── code/                    # Figma sandbox (no DOM)\n│   ├── index.ts\n│   └── figma/\n│       ├── helpers.ts\n│       ├── paintStyles.ts\n│       ├── textStyles.ts\n│       ├── effectStyles.ts\n│       └── nodeStyles.ts\n├── ui/                      # React UI\n│   ├── main.tsx\n│   ├── App.tsx\n│   ├── store/\n│   │   └── themeStore.ts\n│   ├── components/\n│   │   ├── Colors/\n│   │   ├── Effects/\n│   │   ├── Export/\n│   │   ├── Footer/\n│   │   ├── Preferences/\n│   │   └── Type/\n│   ├── helpers/\n│   │   ├── colorFormatter.ts\n│   │   ├── customHooks.ts\n│   │   ├── helpers.ts\n│   │   └── randomMessages.ts\n│   ├── icons/\n│   └── styles/\n│       └── app.css\n└── shared/\n    └── types.ts\n```\n\n## Key Changes from Original\n\n| Aspect | Before | After |\n|--------|--------|-------|\n| Build Tool | Webpack | Vite + Bun |\n| React | 16.14.0 | 18.3.1 |\n| Router | react-router-dom 5 | react-router-dom 6 |\n| State | reactn | Zustand |\n| Tailwind | 3.x (PostCSS) | 4.x (@tailwindcss/vite) |\n| Syntax Highlighter | prism-react-renderer 1 | prism-react-renderer 2 |\n| Language | JavaScript | TypeScript |\n\n## Scripts\n\n```bash\nbun run dev          # Vite dev server for UI\nbun run dev:code     # Watch mode for Figma sandbox code\nbun run build        # Build both UI and code\nbun run build:ui     # Build only UI\nbun run build:code   # Build only Figma code\n```\n\n---\n\n## Phase 10: UI Visual Refresh ✅\n\n### Goal\nTransform the spacious, bold UI into a compact, minimal shadcn-inspired design.\n\n### Changes Made\n\n#### Typography\n- [x] `.t-beta`: text-2xl font-bold → text-base font-semibold\n- [x] `.t-gamma`: text-xl → text-sm font-medium\n- [x] `.intro`: Added text-sm text-slate-500\n- [x] Body: Added text-sm base size\n\n#### Spacing\n- [x] App layout: p-4 → p-3\n- [x] Header sections: py-4 → py-3\n- [x] Content sections: my-8 → my-4\n- [x] Navigation buttons: mt-8 → mt-4\n- [x] Grid gaps: gap-4 → gap-3\n\n#### Form Controls\n- [x] `.form-control`: height 44px → 36px, added text-sm, shadow-sm\n- [x] `.button`: min-h-10 → h-8, text-sm, rounded-md\n- [x] Select: height adjusted to h-8\n\n#### Color Palette (Vibrant → Slate)\n- [x] button--green: teal-500 → slate-900 (primary)\n- [x] button--blue: blue-400 → slate-100/200 (secondary)\n- [x] button--grey: gray-100 → transparent with border (ghost)\n- [x] Text colors: gray-600/900 → slate-500/900\n- [x] Borders: gray-200/300 → slate-200\n- [x] Focus rings: teal-500 → slate-400\n\n#### Components Updated\n- [x] `src/ui/styles/app.css` - Core styles\n- [x] `src/ui/App.tsx` - Layout padding\n- [x] `src/ui/components/Preferences/Preferences.tsx`\n- [x] `src/ui/components/Colors/Colors.tsx`\n- [x] `src/ui/components/Colors/Color.tsx`\n- [x] `src/ui/components/Colors/NewColor.tsx`\n- [x] `src/ui/components/Colors/Gradient.tsx`\n- [x] `src/ui/components/Type/Type.tsx`\n- [x] `src/ui/components/Effects/Effects.tsx`\n- [x] `src/ui/components/Effects/Shadow.tsx`\n- [x] `src/ui/components/Effects/BorderRadius.tsx`\n- [x] `src/ui/components/Export/Export.tsx`\n- [x] `src/ui/components/Footer/Footer.tsx`\n\n#### Leaf Component Details\n- Color swatches: 42px → 32px\n- Shadow/BorderRadius boxes: w-20 h-20 → w-12 h-12 (flex wrap layout)\n- Footer: py-4 → py-2.5, added border-t\n\n### Phase 10b: UI Refinements ✅\n\n- [x] Color row actions: Replaced thick Update/X buttons with subtle icon buttons\n- [x] Color inputs: Auto-save on blur instead of manual Update button\n- [x] NewColor: Removed label, uses dashed border placeholder, plus icon to add\n- [x] Effects grid: Changed from grid-cols-4 to flex-wrap with smaller items (w-12 h-12)\n- [x] Dynamic viewport: Added resize messaging between UI and plugin (min: 200px, max: 600px)\n- [x] UI width: Reduced from 740px to 420px for more compact feel\n\n## Potential Future Improvements\n\n### UI/UX Surfaces\n1. **Navigation** - Add a step indicator/breadcrumb showing progress (1/5, 2/5, etc.)\n2. **Empty states** - More helpful empty states with illustrations or better copy\n3. **Loading states** - Add skeleton loaders while Figma data is being fetched\n4. **Toast notifications** - Feedback when colors are updated/removed\n5. **Search/filter** - Filter colors by name when there are many\n6. **Keyboard shortcuts** - Enter to confirm, Escape to cancel\n7. **Dark mode** - Support Figma's dark theme\n8. **Undo/redo** - Allow reverting color changes\n\n## Phase 11: GitHub Issue Fixes ✅\n\n### Issue #53 - Option to Disable REM Conversion ✅\n- [x] Added `useRem` preference to types (`src/shared/types.ts`)\n- [x] Added default value in store (`src/ui/store/themeStore.ts`)\n- [x] Added radio toggle in Preferences (`src/ui/components/Preferences/Preferences.tsx`)\n- [x] Updated `cleanupTheme` to conditionally convert to REM or keep as px (`src/ui/helpers/helpers.ts`)\n- [x] Updated Export component to pass preference\n\n### Issue #64 - Border Radius Default Variant ✅\n- [x] Added `defaultBorderRadiusIndex` state to types and store\n- [x] Added `getBorderRadiusName` helper function for Tailwind naming convention (xs, sm, DEFAULT, lg, xl, 2xl, 3xl)\n- [x] Updated `cleanupTheme` to generate proper border radius names\n- [x] Updated BorderRadius component to be clickable for selecting DEFAULT\n- [x] Updated Effects component with selection UI and instructions\n- [x] Updated Tailwind 4 formatter to output `--radius` (no suffix) for DEFAULT\n\n---\n\n### Functional Improvements\n9. **Color picker** - Inline color picker instead of hex input\n10. **Drag & drop** - Reorder colors\n11. **Bulk actions** - Select multiple colors for removal\n12. **Preview** - Live preview of the generated CSS/config\n13. **Import** - Import existing tailwind.config.js to prefill\n14. **Variable support** - Support Figma variables (new feature)\n15. **Copy individual values** - Click to copy a single color/shadow value\n"
  },
  {
    "path": "README.md",
    "content": "# Figma Tailwindcss\n\nA 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.\n\nThe plugin: [Figma TailwindCSS](https://www.figma.com/community/plugin/785619431629077634/Figma-Tailwindcss)\n\n---\n\n## Table of Contents\n\n-   [Usage](#usage)\n-   [Roadmap](#roadmap)\n-   [License](#license)\n\n## Usage\n\n### Creating your theme\n\nThe plugin gets it's info from the Local Styles. At this point it picks up:\n\n-   colors\n-   font-families\n-   text-sizes\n-   box-shadow\n-   border-radius\n\n#### Colors\n\nColors 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.\n\n#### Font-families\n\nThe plugin will pick up all font-families used in the Local Text Styles.\n\n#### Text-sizes\n\nAll 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:\n\n```javascript\n...\n'3xs'\n'2xs'\n'xs'\n'sm'\n'base'\n'lg'\n'xl'\n'2xl'\n'3xl'\n...\n```\n\nThe font-sizes the plugin spits out will also be converted into a rem based scale.\n\n#### Box-shadows\nTaken from the effectStyles from your document.\n\n#### Border-radius\nTaken from the nodeStyles from your document.\n\n### Importing your theme\n\nImport the `theme.js` file in to your `tailwind.config.js` configuration file to use it:\n\n**Require syntax**\n\n`const myTheme = require(./theme);`\n\nthe 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\n\n**Import syntax**\n\n`import 'myTheme' from './theme`\n\n#### Extending the default theme\n\nYou can extend the default theme like so:\n\n```\nmodule.exports = {\n    theme: {\n        extend: {\n            colors: myTheme.colors\n        }\n    }\n```\n\nMore info on extending the default theme:\n- https://tailwindcss.com/docs/theme#extending-the-default-theme\n- https://www.youtube.com/watch?v=0l0Gx8gWPHk&ab_channel=TailwindLabs\n\n## Contributing\n\nAll feedback is welcome. Feel free to submit [issues or suggestions](https://github.com/jan-dh/figma-tailwindcss/issues).\n\nThe 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).\n\n## Roadmap\n\n-  line-height\n\n## License\n\nThis project is licensed under the terms of the MIT license.\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n    \"name\": \"Figma Tailwindcss\",\n    \"id\": \"785619431629077634\",\n    \"api\": \"1.0.0\",\n    \"main\": \"dist/code.js\",\n    \"ui\": \"dist/ui.html\",\n    \"documentAccess\": \"dynamic-page\",\n    \"networkAccess\": {\n        \"allowedDomains\": [\n            \"none\"\n        ]\n    },\n    \"editorType\": [\n        \"figma\"\n    ]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"figma-tailwindcss\",\n  \"version\": \"2.1.0\",\n  \"type\": \"module\",\n  \"description\": \"Export Figma styles to TailwindCSS theme\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"dev:code\": \"vite build --config vite.config.code.ts --watch\",\n    \"build\": \"vite build && vite build --config vite.config.code.ts\",\n    \"build:ui\": \"vite build\",\n    \"build:code\": \"vite build --config vite.config.code.ts\",\n    \"preview\": \"vite preview\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/jan-dh/figma-tailwindcss.git\"\n  },\n  \"author\": \"Jan D'Hollander\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/jan-dh/figma-tailwindcss/issues\"\n  },\n  \"homepage\": \"https://github.com/jan-dh/figma-tailwindcss#readme\",\n  \"dependencies\": {\n    \"file-saver\": \"^2.0.5\",\n    \"prism-react-renderer\": \"^2.4.1\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-router-dom\": \"^6.30.3\",\n    \"zustand\": \"^5.0.10\"\n  },\n  \"devDependencies\": {\n    \"@figma/plugin-typings\": \"^1.123.0\",\n    \"@tailwindcss/vite\": \"^4.1.18\",\n    \"@types/bun\": \"^1.3.7\",\n    \"@types/file-saver\": \"^2.0.7\",\n    \"@types/react\": \"^19.2.10\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^5.1.2\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.1\",\n    \"vite-plugin-singlefile\": \"^2.3.0\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "src/code/figma/effectStyles.ts",
    "content": "import { makeRgb } from './helpers'\n\ninterface EffectStylesResult {\n  shadows: { name: string; value: string }[]\n}\n\nexport default async function getEffectStyles(): Promise<EffectStylesResult> {\n  const effectStyles = await figma.getLocalEffectStylesAsync()\n  const shadows: { name: string; value: string }[] = []\n\n  effectStyles.forEach((style) => {\n    const { effects, name } = style\n    const styleString: string[] = []\n\n    effects.forEach((effect) => {\n      if (effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') {\n        const { color, offset, radius, spread } = effect\n        const { r, g, b, a } = makeRgb(color)\n        const colorString = `${r},${g},${b},${a}`\n        styleString.push(\n          `${effect.type === 'INNER_SHADOW' ? 'inset ' : ''}${offset.x}px ${offset.y}px ${radius}px ${spread}px rgba(${colorString})`\n        )\n      }\n    })\n\n    if (styleString.length > 0) {\n      shadows.push({\n        name,\n        value: styleString.join(', '),\n      })\n    }\n  })\n\n  return { shadows }\n}\n"
  },
  {
    "path": "src/code/figma/helpers.ts",
    "content": "export function rgbToHex(int: number): string {\n  let hex = Number(int).toString(16)\n  if (hex.length < 2) {\n    hex = `0${hex}`\n  }\n  return hex\n}\n\nexport function makeHex(r: number, g: number, b: number): string {\n  const red = rgbToHex(r)\n  const green = rgbToHex(g)\n  const blue = rgbToHex(b)\n  return `#${red}${green}${blue}`\n}\n\nexport function makeRgb(color: RGB | RGBA): { r: number; g: number; b: number; a: number } {\n  const r = Math.round(255 * color.r)\n  const g = Math.round(255 * color.g)\n  const b = Math.round(255 * color.b)\n  const a = 'a' in color ? Math.round(100 * color.a) / 100 : 1\n  return { r, g, b, a }\n}\n"
  },
  {
    "path": "src/code/figma/nodeStyles.ts",
    "content": "interface NodeStylesResult {\n  finalRadii: { name: string; value: number }[]\n}\n\nexport default async function getNodeStyles(): Promise<NodeStylesResult> {\n  await figma.loadAllPagesAsync()\n\n  const filteredNodes = figma.root.findAll(\n    (n): n is SceneNode & { cornerRadius: number } =>\n      'cornerRadius' in n && typeof (n as any).cornerRadius === 'number'\n  )\n\n  const radii = new Set<number>()\n\n  filteredNodes.forEach((n) => {\n    const cornerRadius = (n as any).cornerRadius as number\n    if (typeof cornerRadius === 'number') {\n      const value = cornerRadius < 99 ? cornerRadius : 999\n      radii.add(value)\n    }\n  })\n\n  const radiiArray = [...radii].sort((a, b) => a - b)\n  const finalRadii: { name: string; value: number }[] = []\n\n  radiiArray.forEach((radius) => {\n    const value = Number(radius)\n    const name = ''\n    finalRadii.push({ name, value })\n  })\n\n  // Add default none\n  finalRadii.unshift({ name: 'none', value: 0 })\n\n  return { finalRadii }\n}\n"
  },
  {
    "path": "src/code/figma/paintStyles.ts",
    "content": "import { makeHex, makeRgb } from './helpers'\n\ninterface ColorResult {\n  colors: { name: string; value: string }[]\n  gradientColors: string[]\n}\n\nexport default async function getPaintStyles(): Promise<ColorResult> {\n  const colorStyles = await figma.getLocalPaintStylesAsync()\n  const colors: { name: string; value: string }[] = []\n  const gradientColors: string[] = []\n\n  colorStyles.forEach((style) => {\n    const paint = style.paints[0] || null\n    if (paint) {\n      if (paint.type === 'SOLID') {\n        const { name } = style\n        const { r, g, b } = makeRgb(paint.color)\n        const value = makeHex(r, g, b)\n        colors.push({ name, value })\n      } else if (\n        paint.type === 'GRADIENT_LINEAR' ||\n        paint.type === 'GRADIENT_RADIAL' ||\n        paint.type === 'GRADIENT_ANGULAR' ||\n        paint.type === 'GRADIENT_DIAMOND'\n      ) {\n        const gradientStops = paint.gradientStops\n        if (gradientStops && gradientStops.length > 0) {\n          gradientStops.forEach((stop) => {\n            const { r, g, b } = makeRgb(stop.color)\n            const value = makeHex(r, g, b)\n            gradientColors.push(value)\n          })\n        }\n      }\n    }\n  })\n\n  return { colors, gradientColors }\n}\n"
  },
  {
    "path": "src/code/figma/textStyles.ts",
    "content": "interface TextStylesResult {\n  finalSizes: { name: string; value: string }[]\n  finalFamilies: { name: string; value: string }[]\n}\n\nexport default async function getTextStyles(): Promise<TextStylesResult> {\n  const textStyles = await figma.getLocalTextStylesAsync()\n  const fontSizes: number[] = []\n  const fontFamilies: string[] = []\n  const finalSizes: { name: string; value: string }[] = []\n  const finalFamilies: { name: string; value: string }[] = []\n\n  textStyles.forEach((style) => {\n    const { family } = style.fontName\n    const { fontSize } = style\n\n    fontFamilies.push(family)\n    fontSizes.push(fontSize)\n  })\n\n  // Get unique values\n  const singleSizes = Array.from(new Set(fontSizes)).sort((a, b) => a - b)\n  const singleFamilies = Array.from(new Set(fontFamilies))\n\n  // Clean sizes\n  singleSizes.forEach((size) => {\n    const name = ''\n    const value = size.toString()\n    finalSizes.push({ name, value })\n  })\n\n  // Clean families\n  singleFamilies.forEach((family) => {\n    const name = family.replace(/\\s+/g, '-').toLowerCase()\n    const value = family\n    finalFamilies.push({ name, value })\n  })\n\n  return { finalSizes, finalFamilies }\n}\n"
  },
  {
    "path": "src/code/index.ts",
    "content": "import getPaintStyles from './figma/paintStyles'\nimport getTextStyles from './figma/textStyles'\nimport getEffectStyles from './figma/effectStyles'\nimport getNodeStyles from './figma/nodeStyles'\n\ninterface Theme {\n  colors: { name: string; value: string }[]\n  gradientColors: string[]\n  fontSize: { name: string; value: string }[]\n  fontFamily: { name: string; value: string }[]\n  boxShadow: { name: string; value: string }[]\n  borderRadius: { name: string; value: number }[]\n  baseFontSize: false\n  preferences: {\n    tailwindVersion: '4'\n    colorFormat: 'hex'\n    grouped: false\n  }\n}\n\nconst theme: Theme = {\n  colors: [],\n  gradientColors: [],\n  fontSize: [],\n  fontFamily: [],\n  boxShadow: [],\n  borderRadius: [],\n  baseFontSize: false,\n  preferences: {\n    tailwindVersion: '4',\n    colorFormat: 'hex',\n    grouped: false,\n  },\n}\n\nconst UI_WIDTH = 420\nconst UI_MIN_HEIGHT = 200\nconst UI_MAX_HEIGHT = 600\n\n// Gather all different properties\nconst paintStyles = getPaintStyles()\nconst textStyles = getTextStyles()\nconst effectStyles = getEffectStyles()\nconst nodeStyles = getNodeStyles()\n\nPromise.all([paintStyles, textStyles, effectStyles, nodeStyles])\n  .then((values) => {\n    theme.colors.push(...values[0].colors)\n    theme.gradientColors.push(...values[0].gradientColors)\n    theme.fontSize.push(...values[1].finalSizes)\n    theme.fontFamily.push(...values[1].finalFamilies)\n    theme.boxShadow.push(...values[2].shadows)\n    theme.borderRadius.push(...values[3].finalRadii)\n  })\n  .then(() => {\n    // Show UI\n    figma.showUI(__html__, { width: UI_WIDTH, height: UI_MAX_HEIGHT })\n    // Pass theme to UI\n    figma.ui.postMessage(theme)\n  })\n\n// Handle messages from UI\nfigma.ui.onmessage = (msg: { type: string; height?: number }) => {\n  if (msg.type === 'resize' && typeof msg.height === 'number') {\n    const newHeight = Math.min(Math.max(msg.height, UI_MIN_HEIGHT), UI_MAX_HEIGHT)\n    figma.ui.resize(UI_WIDTH, newHeight)\n  }\n}\n"
  },
  {
    "path": "src/shared/types.ts",
    "content": "export interface ColorItem {\n  name: string\n  value: string\n}\n\nexport interface FontSizeItem {\n  name: string\n  value: string\n}\n\nexport interface FontFamilyItem {\n  name: string\n  value: string\n}\n\nexport interface ShadowItem {\n  name: string\n  value: string\n}\n\nexport interface BorderRadiusItem {\n  name: string\n  value: number | string\n}\n\nexport interface Preferences {\n  tailwindVersion: '3' | '4'\n  colorFormat: 'hex' | 'rgba' | 'hsl' | 'oklch' | 'oklab'\n  grouped: boolean\n  useRem: boolean\n}\n\nexport interface ThemeState {\n  colors: ColorItem[]\n  gradientColors: string[]\n  fontSize: FontSizeItem[]\n  fontFamily: FontFamilyItem[]\n  boxShadow: ShadowItem[]\n  borderRadius: BorderRadiusItem[]\n  baseFontSize: number | false\n  defaultBorderRadiusIndex: number | null\n  preferences: Preferences\n}\n\nexport type CleanTheme = {\n  colors?: Record<string, string | Record<string, string>>\n  fontFamily?: Record<string, string>\n  fontSize?: Record<string, string>\n  boxShadow?: Record<string, string>\n  borderRadius?: Record<string, string>\n}\n"
  },
  {
    "path": "src/ui/App.tsx",
    "content": "import { MemoryRouter, Routes, Route } from 'react-router-dom'\nimport { ScrollToTop, useAutoResize } from './helpers/customHooks'\nimport Preferences from './components/Preferences/Preferences'\nimport Colors from './components/Colors/Colors'\nimport Type from './components/Type/Type'\nimport Effects from './components/Effects/Effects'\nimport Export from './components/Export/Export'\nimport Info from './components/Export/Info'\nimport Footer from './components/Footer/Footer'\n\nconst AppContent = () => {\n  useAutoResize()\n  return (\n    <>\n      <ScrollToTop />\n      <div className=\"p-3 flex-grow\">\n        <Routes>\n          <Route path=\"/\" element={<Preferences />} />\n          <Route path=\"/colors\" element={<Colors />} />\n          <Route path=\"/typography\" element={<Type />} />\n          <Route path=\"/effects\" element={<Effects />} />\n          <Route path=\"/export\" element={<Export />} />\n          <Route path=\"/info\" element={<Info />} />\n        </Routes>\n      </div>\n    </>\n  )\n}\n\nconst App = () => {\n  return (\n    <div className=\"min-h-full flex flex-col\">\n      <MemoryRouter initialEntries={['/']}>\n        <AppContent />\n      </MemoryRouter>\n      <Footer />\n    </div>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "src/ui/components/Colors/Color.tsx",
    "content": "import { useInput } from '../../helpers/customHooks'\nimport type { ColorItem } from '../../../shared/types'\n\ninterface ColorProps extends ColorItem {\n  index: number\n  onChange: (color: ColorItem, index: number) => void\n  removeColor: (index: number) => void\n  colorFormat: string\n}\n\nfunction Color({ name: initialName, value: initialValue, index, onChange, removeColor }: ColorProps) {\n  const [name, setName] = useInput(initialName)\n  const [value, setValue] = useInput(initialValue)\n\n  const updateColor = () => {\n    const newColor = { name, value }\n    onChange(newColor, index)\n  }\n\n  const handleRemove = () => {\n    removeColor(index)\n  }\n\n  return (\n    <div className=\"flex mt-2 justify-between items-center gap-1.5\">\n      <div style={{ backgroundColor: value }} className=\"color shadow-sm\"></div>\n      <div className=\"form-field\">\n        <input\n          type=\"text\"\n          name=\"name\"\n          placeholder={name}\n          defaultValue={name}\n          className=\"form-control\"\n          onChange={setName}\n          onBlur={updateColor}\n        />\n      </div>\n      <div className=\"form-field\">\n        <input\n          type=\"text\"\n          name=\"hex\"\n          placeholder={value}\n          defaultValue={value}\n          className=\"form-control\"\n          onChange={setValue}\n          onBlur={updateColor}\n        />\n      </div>\n      <button\n        className=\"text-slate-400 hover:text-slate-600 p-1 flex-shrink-0\"\n        onClick={handleRemove}\n        title=\"Remove color\"\n      >\n        <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M6 18L18 6M6 6l12 12\" />\n        </svg>\n      </button>\n    </div>\n  )\n}\n\nexport default Color\n"
  },
  {
    "path": "src/ui/components/Colors/Colors.tsx",
    "content": "import { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../store/themeStore'\nimport Color from './Color'\nimport Gradient from './Gradient'\nimport NewColor from './NewColor'\nimport messages from '../../helpers/randomMessages'\nimport type { ColorItem } from '../../../shared/types'\n\nconst Colors = () => {\n  const colors = useThemeStore((s) => s.colors)\n  const gradients = useThemeStore((s) => s.gradientColors)\n  const preferences = useThemeStore((s) => s.preferences)\n  const updateColor = useThemeStore((s) => s.updateColor)\n  const removeColor = useThemeStore((s) => s.removeColor)\n  const setColors = useThemeStore((s) => s.setColors)\n\n  const nextIndex = colors.length\n  const hasColor = colors.length > 0\n  const hasGradients = gradients.length > 0\n  const feedbackItem =\n    messages.emptyColors[Math.floor(Math.random() * messages.emptyColors.length)]\n\n  const { colorFormat } = preferences\n\n  const handleColorChange = (color: ColorItem, i: number) => {\n    if (i >= colors.length) {\n      // Adding new color\n      setColors([...colors, color])\n    } else {\n      updateColor(i, color)\n    }\n  }\n\n  const handleRemoveColor = (i: number) => {\n    removeColor(i)\n  }\n\n  return (\n    <>\n      <div className=\"w-full border-b border-slate-200 py-3\">\n        <h2 className=\"t-beta\">Colors</h2>\n        <p className=\"intro\">Pick and choose the colors you want to use</p>\n      </div>\n      <div className=\"my-4 richtext\">\n        <p>\n          Colors are taken from the Figma Local Paint Styles. Colors can be grouped in\n          the export step. If you want to group codes, prefix them with the same name\n          (only the last two parts will be used).\n        </p>\n      </div>\n      {hasColor ? (\n        colors.map((color, i) => (\n          <Color\n            key={i}\n            index={i}\n            onChange={handleColorChange}\n            removeColor={handleRemoveColor}\n            colorFormat={colorFormat}\n            {...color}\n          />\n        ))\n      ) : (\n        <div className=\"richtext\">\n          <p>{feedbackItem}</p>\n        </div>\n      )}\n      {hasGradients ? (\n        <div className=\"my-4\">\n          <div className=\"richtext\">\n            <h3 className=\"t-gamma\">Gradients</h3>\n            <p>\n              We found some gradients in the document. Make sure to add the colors if\n              you want to use them in your theme.\n            </p>\n          </div>\n          {gradients.map((color, i) => (\n            <Gradient key={i} hex={color} />\n          ))}\n        </div>\n      ) : null}\n      <NewColor key={nextIndex} index={nextIndex} onChange={handleColorChange} />\n      <div className=\"flex justify-between mt-4\">\n        <Link to=\"/\" className=\"button button--green\">\n          Previous\n        </Link>\n        <Link to=\"/typography\" className=\"button button--green\">\n          Next\n        </Link>\n      </div>\n    </>\n  )\n}\n\nexport default Colors\n"
  },
  {
    "path": "src/ui/components/Colors/Gradient.tsx",
    "content": "interface GradientProps {\n  hex: string\n}\n\nfunction Gradient({ hex }: GradientProps) {\n  return (\n    <div className=\"flex mt-2 justify-start items-center\">\n      <div style={{ backgroundColor: hex }} className=\"color shadow-sm\"></div>\n      <div className=\"text-sm text-slate-600\">{hex}</div>\n    </div>\n  )\n}\n\nexport default Gradient\n"
  },
  {
    "path": "src/ui/components/Colors/NewColor.tsx",
    "content": "import { useInput } from '../../helpers/customHooks'\nimport type { ColorItem } from '../../../shared/types'\n\ninterface NewColorProps {\n  index: number\n  onChange: (color: ColorItem, index: number) => void\n}\n\nconst NewColor = ({ index, onChange }: NewColorProps) => {\n  const [name, setName] = useInput('')\n  const [value, setValue] = useInput('')\n\n  const addColor = () => {\n    const newColor = { name, value }\n    onChange(newColor, index)\n  }\n\n  return (\n    <div className=\"mt-4 pt-3 border-t border-slate-100\">\n      <div className=\"flex items-center gap-1.5\">\n        <div\n          style={{ backgroundColor: value || '#e2e8f0', borderColor: value || '#e2e8f0' }}\n          className=\"color border border-dashed border-slate-300\"\n        ></div>\n        <div className=\"form-field\">\n          <input\n            type=\"text\"\n            className=\"form-control\"\n            placeholder=\"Color name\"\n            onChange={setName}\n          />\n        </div>\n        <div className=\"form-field\">\n          <input\n            type=\"text\"\n            className=\"form-control\"\n            placeholder=\"#hex\"\n            onChange={setValue}\n          />\n        </div>\n        <button\n          className=\"text-slate-400 hover:text-slate-600 p-1 flex-shrink-0\"\n          onClick={addColor}\n          title=\"Add color\"\n        >\n          <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M12 4v16m8-8H4\" />\n          </svg>\n        </button>\n      </div>\n    </div>\n  )\n}\n\nexport default NewColor\n"
  },
  {
    "path": "src/ui/components/Effects/BorderRadius.tsx",
    "content": "import { getBorderRadiusName } from '../../helpers/helpers'\n\ninterface BorderRadiusProps {\n  index: number\n  value: number | string\n  isDefault: boolean\n  defaultIndex: number | null\n  total: number\n  useRem: boolean\n  onSelect: () => void\n}\n\nconst BorderRadius = ({\n  index,\n  value,\n  isDefault,\n  defaultIndex,\n  total,\n  useRem,\n  onSelect,\n}: BorderRadiusProps) => {\n  const displayName = getBorderRadiusName(index, defaultIndex, total)\n  const numValue = typeof value === 'number' ? value : parseFloat(String(value))\n  const displayValue = useRem ? `${numValue / 16}rem` : `${numValue}px`\n\n  return (\n    <div\n      className={`flex flex-col items-center text-center cursor-pointer group ${\n        isDefault ? 'ring-2 ring-slate-900 ring-offset-2 rounded' : ''\n      }`}\n      onClick={onSelect}\n      title={`Click to set as DEFAULT (${displayValue})`}\n    >\n      <div\n        className={`w-12 h-12 bg-slate-200 transition-colors ${\n          isDefault ? '' : 'group-hover:bg-slate-300'\n        }`}\n        style={{ borderRadius: numValue }}\n      ></div>\n      <span\n        className={`block text-[10px] mt-1.5 truncate max-w-full ${\n          isDefault ? 'text-slate-900 font-medium' : 'text-slate-500'\n        }`}\n      >\n        {displayName}\n      </span>\n      <span className=\"block text-slate-400 text-[9px]\">{displayValue}</span>\n    </div>\n  )\n}\n\nexport default BorderRadius\n"
  },
  {
    "path": "src/ui/components/Effects/Effects.tsx",
    "content": "import { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../store/themeStore'\nimport Shadow from './Shadow'\nimport BorderRadius from './BorderRadius'\nimport messages from '../../helpers/randomMessages'\n\nconst Effects = () => {\n  const shadows = useThemeStore((s) => s.boxShadow)\n  const borderRadii = useThemeStore((s) => s.borderRadius)\n  const defaultBorderRadiusIndex = useThemeStore((s) => s.defaultBorderRadiusIndex)\n  const setDefaultBorderRadiusIndex = useThemeStore((s) => s.setDefaultBorderRadiusIndex)\n  const useRem = useThemeStore((s) => s.preferences.useRem)\n\n  const hasShadows = shadows.length > 0\n  const hasBorderRadii = borderRadii.length > 0\n\n  const handleSelectDefault = (index: number) => {\n    // Toggle off if clicking the same one\n    if (defaultBorderRadiusIndex === index) {\n      setDefaultBorderRadiusIndex(null)\n    } else {\n      setDefaultBorderRadiusIndex(index)\n    }\n  }\n\n  const feedbackShadows =\n    messages.emptyShadows[Math.floor(Math.random() * messages.emptyShadows.length)]\n  const feedbackBorderRadii =\n    messages.emptyBorderRadii[\n      Math.floor(Math.random() * messages.emptyBorderRadii.length)\n    ]\n\n  return (\n    <>\n      <div className=\"w-full border-b border-slate-200 py-3\">\n        <h2 className=\"t-beta\">Effects</h2>\n        <p className=\"intro\">Effect Styles we found in your Figma file</p>\n      </div>\n      <h3 className=\"t-gamma my-3\">Border-radius</h3>\n      {hasBorderRadii && (\n        <p className=\"text-xs text-slate-500 mb-2\">\n          Click a radius to set it as DEFAULT (used for <code className=\"bg-slate-100 px-1 rounded\">rounded</code> class)\n        </p>\n      )}\n      {hasBorderRadii ? (\n        <div className=\"flex flex-wrap gap-4\">\n          {borderRadii.map((radius, i) => (\n            <BorderRadius\n              key={i}\n              index={i}\n              value={radius.value}\n              isDefault={defaultBorderRadiusIndex === i}\n              defaultIndex={defaultBorderRadiusIndex}\n              total={borderRadii.length}\n              useRem={useRem}\n              onSelect={() => handleSelectDefault(i)}\n            />\n          ))}\n        </div>\n      ) : (\n        <div className=\"mt-2\">\n          <div className=\"richtext\">\n            <p>{feedbackBorderRadii}</p>\n          </div>\n        </div>\n      )}\n      <h3 className=\"t-gamma my-3\">Shadows</h3>\n      {hasShadows ? (\n        <div className=\"flex flex-wrap gap-4\">\n          {shadows.map((shadow, i) => (\n            <Shadow key={i} {...shadow} />\n          ))}\n        </div>\n      ) : (\n        <div className=\"mt-2\">\n          <div className=\"richtext\">\n            <p>{feedbackShadows}</p>\n          </div>\n        </div>\n      )}\n      <div className=\"flex justify-between mt-4\">\n        <Link to=\"/typography\" className=\"button button--green\">\n          Previous\n        </Link>\n        <Link to=\"/export\" className=\"button button--green\">\n          Next\n        </Link>\n      </div>\n    </>\n  )\n}\n\nexport default Effects\n"
  },
  {
    "path": "src/ui/components/Effects/Shadow.tsx",
    "content": "interface ShadowProps {\n  name: string\n  value: string\n}\n\nconst Shadow = ({ name, value }: ShadowProps) => {\n  return (\n    <div className=\"flex flex-col items-center text-center\">\n      <div className=\"w-12 h-12 rounded bg-white\" style={{ boxShadow: value }}></div>\n      <span className=\"block text-slate-500 text-[10px] mt-1.5 truncate max-w-full\">{name}</span>\n    </div>\n  )\n}\n\nexport default Shadow\n"
  },
  {
    "path": "src/ui/components/Export/Export.tsx",
    "content": "import { Link } from 'react-router-dom'\nimport { Highlight, themes } from 'prism-react-renderer'\nimport { saveAs } from 'file-saver'\nimport { useThemeStore } from '../../store/themeStore'\nimport { cleanupTheme, formatTailwind4Theme, formatTailwind3Theme } from '../../helpers/helpers'\n\nconst Export = () => {\n  const theme = useThemeStore()\n  const preferences = useThemeStore((s) => s.preferences)\n  const setPreferences = useThemeStore((s) => s.setPreferences)\n  const defaultBorderRadiusIndex = useThemeStore((s) => s.defaultBorderRadiusIndex)\n  const { colorFormat, grouped, tailwindVersion, useRem } = preferences\n\n  const isV4 = tailwindVersion === '4'\n  const cleanTheme = cleanupTheme(theme, colorFormat, grouped, useRem, defaultBorderRadiusIndex)\n\n  const groupColors = () => {\n    if (isV4) return\n    setPreferences({ grouped: !grouped })\n  }\n\n  const exportTheme = () => {\n    if (isV4) {\n      const cssContent = formatTailwind4Theme(cleanTheme)\n      const blob = new Blob([cssContent], {\n        type: 'text/css;charset=utf-8',\n      })\n      saveAs(blob, 'theme.css')\n    } else {\n      const jsContent = formatTailwind3Theme(cleanTheme)\n      const blob = new Blob([jsContent], {\n        type: 'application/javascript;charset=utf-8',\n      })\n      saveAs(blob, 'tailwind.config.js')\n    }\n  }\n\n  const markup = isV4 ? formatTailwind4Theme(cleanTheme) : formatTailwind3Theme(cleanTheme)\n\n  const buttonText = grouped ? 'Ungroup colors' : 'Group colors'\n  const switchClass = grouped ? 'bg-green-400 active' : 'bg-gray-400'\n\n  return (\n    <>\n      <div className=\"flex\">\n        <div className=\"w-full border-b border-slate-200 py-3\">\n          <h2 className=\"t-beta\">Export theme</h2>\n          <p className=\"intro\">Almost there...</p>\n        </div>\n      </div>\n\n      <div className=\"mt-4 relative\">\n        <Highlight\n          theme={themes.dracula}\n          code={markup}\n          language={isV4 ? 'css' : 'javascript'}\n        >\n          {({ className, style, tokens, getLineProps, getTokenProps }) => (\n            <pre className={className} style={style}>\n              {tokens.map((line, i) => (\n                <div key={i} {...getLineProps({ line, key: i })}>\n                  {line.map((token, key) => (\n                    <span key={key} {...getTokenProps({ token, key })} />\n                  ))}\n                </div>\n              ))}\n            </pre>\n          )}\n        </Highlight>\n\n        {!isV4 && (\n          <div\n            className=\"absolute top-0 right-0 mr-3 mt-3 flex items-center cursor-pointer\"\n            onClick={groupColors}\n          >\n            <div\n              className={`switch w-10 h-5 rounded-full cursor-pointer mr-3 ${switchClass}`}\n            >\n              <div className=\"w-1/2 h-full rounded-full shadow bg-white\"></div>\n            </div>\n            <span className=\"text-slate-300 text-xs\">{buttonText}</span>\n          </div>\n        )}\n      </div>\n\n      <div className=\"mt-4 flex items-center\">\n        <button className=\"button button--grey mr-3\" onClick={exportTheme}>\n          <svg\n            className=\"fill-current w-3.5 h-3.5 mr-1.5\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 20 20\"\n          >\n            <path d=\"M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z\" />\n          </svg>\n          Create file\n        </button>\n        <a\n          className=\"button button--blue\"\n          href=\"https://github.com/jan-dh/figma-tailwindcss/blob/master/README.md\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n        >\n          How does it work?\n        </a>\n      </div>\n\n      <div className=\"flex justify-between mt-4\">\n        <Link to=\"/effects\" className=\"button button--green\">\n          Previous\n        </Link>\n      </div>\n    </>\n  )\n}\n\nexport default Export\n"
  },
  {
    "path": "src/ui/components/Export/Info.tsx",
    "content": "import { Link } from 'react-router-dom'\n\nconst Info = () => {\n  const theme = `'./theme'`\n  const overWrite = `module.exports = {\n  theme: {\n    colors: {\n      theme.colors\n    }`\n  const extend = `module.exports = {\n  theme: {\n    colors: {\n      gray: {\n        100: #f7fafc',\n        200: '#edf2f7',\n      },\n      ...theme.colors\n    }\n  `\n\n  return (\n    <>\n      <div className=\"flex\">\n        <div className=\"w-full border-b border-gray-200 py-4\">\n          <h2 className=\"t-beta\">Usage</h2>\n          <p className=\"intro\">Everything you need to know to get you started</p>\n        </div>\n      </div>\n      <div className=\"mt-8\">\n        <div className=\"richtext\">\n          <p>\n            Import the <code>theme.js</code> file in to your{' '}\n            <code>tailwind.config.js</code>\n            configuration file to use it:\n          </p>\n          <pre>\n            <code>import theme from {theme};</code>\n          </pre>\n          <h3 className=\"t-gamma\">Overriding the default theme</h3>\n          <p>\n            To override an option in the default theme, create a theme section in your\n            config and add the key you would like to override.\n          </p>\n          <pre>\n            <code>{overWrite}</code>\n          </pre>\n          <h3 className=\"t-gamma\">Add to current values</h3>\n          <p>\n            Using the spread operator at the end of each property you can add your\n            theme values to an existing config or to the default tailwind config.\n          </p>\n          <pre>\n            <code>{extend}</code>\n          </pre>\n        </div>\n      </div>\n      <div className=\"flex justify-between mt-8\">\n        <Link to=\"/export\" className=\"button button--green\">\n          Previous\n        </Link>\n      </div>\n    </>\n  )\n}\n\nexport default Info\n"
  },
  {
    "path": "src/ui/components/Footer/Footer.tsx",
    "content": "import Tailwind from '../../icons/Tailwind'\nimport Github from '../../icons/Github'\n\nconst Footer = () => {\n  return (\n    <div className=\"w-full bg-slate-50 py-2.5 border-t border-slate-100\">\n      <div className=\"px-3 flex text-xs text-slate-500 font-medium\">\n        <div className=\"w-1/2\">\n          <a\n            href=\"https://tailwindcss.com/\"\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n            className=\"inline-flex items-center hover:text-slate-700\"\n          >\n            <Tailwind />\n            <span className=\"ml-1.5\">tailwindcss</span>\n          </a>\n        </div>\n        <div className=\"w-1/2 text-right\">\n          <a\n            href=\"https://github.com/jan-dh/figma-tailwindcss/\"\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n            className=\"inline-flex items-center hover:text-slate-700\"\n          >\n            <Github />\n            <span className=\"ml-1.5\">Github</span>\n          </a>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default Footer\n"
  },
  {
    "path": "src/ui/components/Preferences/Preferences.tsx",
    "content": "import type { ChangeEvent } from 'react'\nimport { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../store/themeStore'\n\nconst Preferences = () => {\n  const preferences = useThemeStore((s) => s.preferences)\n  const setPreferences = useThemeStore((s) => s.setPreferences)\n\n  const updatePreference = (key: string, value: string | boolean) => {\n    setPreferences({ [key]: value })\n  }\n\n  return (\n    <>\n      <div className=\"w-full border-b border-slate-200 py-3\">\n        <h2 className=\"t-beta\">Preferences</h2>\n        <p className=\"intro\">Configure how Tailwind CSS should be generated</p>\n      </div>\n      <div className=\"my-4 richtext\">\n        <h3 className=\"t-gamma\">Tailwind version</h3>\n        <p>Choose which version of Tailwind to target.</p>\n        <div className=\"mt-2 space-x-4\">\n          <label>\n            <input\n              type=\"radio\"\n              name=\"tailwindVersion\"\n              value=\"3\"\n              checked={preferences.tailwindVersion === '3'}\n              onChange={(e: ChangeEvent<HTMLInputElement>) =>\n                updatePreference('tailwindVersion', e.target.value as '3' | '4')\n              }\n            />{' '}\n            Tailwind 3\n          </label>\n          <label>\n            <input\n              type=\"radio\"\n              name=\"tailwindVersion\"\n              value=\"4\"\n              checked={preferences.tailwindVersion === '4'}\n              onChange={(e: ChangeEvent<HTMLInputElement>) =>\n                updatePreference('tailwindVersion', e.target.value as '3' | '4')\n              }\n            />{' '}\n            Tailwind 4\n          </label>\n        </div>\n      </div>\n      <div className=\"my-4 richtext\">\n        <h3 className=\"t-gamma\">Color format</h3>\n        <p>Select how colors should be exported.</p>\n        <select\n          className=\"mt-2\"\n          value={preferences.colorFormat}\n          onChange={(e: ChangeEvent<HTMLSelectElement>) =>\n            updatePreference('colorFormat', e.target.value)\n          }\n        >\n          <option value=\"hex\">Hex (#rrggbb)</option>\n          <option value=\"rgba\">RGBA</option>\n          <option value=\"hsl\">HSL</option>\n          <option value=\"oklch\">OKLCH</option>\n        </select>\n      </div>\n      <div className=\"my-4 richtext\">\n        <h3 className=\"t-gamma\">Font size unit</h3>\n        <p>Choose whether font sizes should be converted to REM or kept as pixels.</p>\n        <div className=\"mt-2 space-x-4\">\n          <label>\n            <input\n              type=\"radio\"\n              name=\"useRem\"\n              value=\"true\"\n              checked={preferences.useRem === true}\n              onChange={() => updatePreference('useRem', true)}\n            />{' '}\n            REM (relative)\n          </label>\n          <label>\n            <input\n              type=\"radio\"\n              name=\"useRem\"\n              value=\"false\"\n              checked={preferences.useRem === false}\n              onChange={() => updatePreference('useRem', false)}\n            />{' '}\n            Pixels (absolute)\n          </label>\n        </div>\n      </div>\n      <div className=\"flex justify-end mt-4\">\n        <Link to=\"/colors\" className=\"button button--green\">\n          Next\n        </Link>\n      </div>\n    </>\n  )\n}\n\nexport default Preferences\n"
  },
  {
    "path": "src/ui/components/Type/FontFamilies.tsx",
    "content": "import { useThemeStore } from '../../store/themeStore'\nimport FontFamily from './FontFamily'\n\nconst FontFamilies = () => {\n  const fontFamilies = useThemeStore((s) => s.fontFamily)\n\n  return (\n    <div className=\"code-block text-white mt-4\">\n      {fontFamilies.map((family, i) => (\n        <FontFamily key={i} {...family} />\n      ))}\n    </div>\n  )\n}\n\nexport default FontFamilies\n"
  },
  {
    "path": "src/ui/components/Type/FontFamily.tsx",
    "content": "interface FontFamilyProps {\n  name: string\n  value: string\n}\n\nconst FontFamily = ({ name, value }: FontFamilyProps) => {\n  return (\n    <div className=\"flex justify-between\">\n      <span>{name}</span>\n      <span className=\"text-code-green\">{value}</span>\n    </div>\n  )\n}\n\nexport default FontFamily\n"
  },
  {
    "path": "src/ui/components/Type/FontSize.tsx",
    "content": "interface FontSizeProps {\n  name: string\n  value: string\n}\n\nconst FontSize = ({ name, value }: FontSizeProps) => {\n  const remSize = Number(value) / 16\n  const fontSize = {\n    fontSize: `${remSize}rem`,\n  }\n\n  return (\n    <div className=\"flex justify-between items-center leading-none\">\n      <p style={fontSize} className=\"text-code-white\">\n        {value}px<span className=\"ml-4 text-code-red\">- {remSize}rem</span>\n      </p>\n      <span className=\"text-code-green\">{name}</span>\n    </div>\n  )\n}\n\nexport default FontSize\n"
  },
  {
    "path": "src/ui/components/Type/FontSizes.tsx",
    "content": "import type { ChangeEvent } from 'react'\nimport { useThemeStore } from '../../store/themeStore'\nimport FontSize from './FontSize'\nimport { calculatePosition } from '../../helpers/helpers'\n\nconst FontSizes = () => {\n  const fontSizes = useThemeStore((s) => s.fontSize)\n  const baseFontSize = useThemeStore((s) => s.baseFontSize)\n  const setFontSize = useThemeStore((s) => s.setFontSize)\n  const setBaseFontSize = useThemeStore((s) => s.setBaseFontSize)\n\n  const updateFontSizes = (e: ChangeEvent<HTMLSelectElement>) => {\n    const basePosition = fontSizes.findIndex((x) => x.value === e.target.value)\n    const size = fontSizes.length\n\n    const newFontSizes = fontSizes.map((item, i) => ({\n      ...item,\n      name: calculatePosition(i, basePosition, size),\n    }))\n\n    setBaseFontSize(Number(e.target.value))\n    setFontSize(newFontSizes)\n  }\n\n  return (\n    <>\n      <label htmlFor=\"sizeSelect\" className=\"block mt-4\">\n        Pick a base font-size\n      </label>\n      <select\n        className=\"mt-2 border rounded p-2 focus-visible:outline-none focus:ring-1 focus:ring-teal-500\"\n        value={baseFontSize || ''}\n        onChange={updateFontSizes}\n      >\n        <option value=\"\">-</option>\n        {fontSizes.map((size, i) => (\n          <option value={size.value} key={i}>\n            {size.value}px\n          </option>\n        ))}\n      </select>\n      <div className=\"mt-4 code-block\">\n        {fontSizes.map((size, i) => (\n          <FontSize key={i} {...size} />\n        ))}\n      </div>\n    </>\n  )\n}\n\nexport default FontSizes\n"
  },
  {
    "path": "src/ui/components/Type/Type.tsx",
    "content": "import type { MouseEvent } from 'react'\nimport { Link } from 'react-router-dom'\nimport { useThemeStore } from '../../store/themeStore'\nimport FontFamilies from './FontFamilies'\nimport FontSizes from './FontSizes'\nimport messages from '../../helpers/randomMessages'\n\nconst Type = () => {\n  const fontSizes = useThemeStore((s) => s.fontSize)\n  const fontFamilies = useThemeStore((s) => s.fontFamily)\n  const baseFontSize = useThemeStore((s) => s.baseFontSize)\n\n  const hasFamilies = fontFamilies.length > 0\n  const hasSizes = fontSizes.length > 0\n\n  const emptyFamily =\n    messages.emptyFamilies[Math.floor(Math.random() * messages.emptyFamilies.length)]\n\n  const emptySize =\n    messages.emptySizes[Math.floor(Math.random() * messages.emptySizes.length)]\n\n  const validateFontSize = (e: MouseEvent<HTMLAnchorElement>) => {\n    if (!baseFontSize && fontSizes.length > 0) {\n      e.preventDefault()\n    }\n  }\n\n  const buttonClass =\n    baseFontSize || !hasSizes\n      ? 'button button--green'\n      : 'button button--green opacity-50'\n\n  return (\n    <>\n      <div className=\"w-full border-b border-slate-200 py-3\">\n        <h2 className=\"t-beta\">Typography</h2>\n        <p className=\"intro\">Font families and font sizes</p>\n      </div>\n      <h3 className=\"t-gamma mt-3\">Font families</h3>\n      {hasFamilies ? (\n        <FontFamilies />\n      ) : (\n        <div className=\"richtext mt-2\">\n          <p>{emptyFamily}</p>\n        </div>\n      )}\n      <div className=\"mt-4\">\n        <h3 className=\"t-gamma\">Font sizes</h3>\n        {hasSizes ? (\n          <FontSizes />\n        ) : (\n          <div className=\"richtext mt-2\">\n            <p>{emptySize}</p>\n          </div>\n        )}\n      </div>\n      <div className=\"flex justify-between mt-4\">\n        <Link to=\"/colors\" className=\"button button--green\">\n          Previous\n        </Link>\n        <Link to=\"/effects\" className={buttonClass} onClick={validateFontSize}>\n          Next\n        </Link>\n      </div>\n    </>\n  )\n}\n\nexport default Type\n"
  },
  {
    "path": "src/ui/helpers/colorFormatter.ts",
    "content": "// Converts sRGB (0-255) to linear RGB (0-1)\nfunction sRGBToLinear(c: number): number {\n  c /= 255\n  return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)\n}\n\n// RGB string to array\nfunction parseRGB(input: string | number[]): [number, number, number, number] {\n  if (typeof input === 'string' && input.startsWith('#')) {\n    let hex = input.slice(1)\n    if (hex.length === 3)\n      hex = hex\n        .split('')\n        .map((h) => h + h)\n        .join('')\n    const r = parseInt(hex.slice(0, 2), 16)\n    const g = parseInt(hex.slice(2, 4), 16)\n    const b = parseInt(hex.slice(4, 6), 16)\n    return [r, g, b, 1]\n  }\n  if (typeof input === 'string' && input.startsWith('rgb')) {\n    const nums = input.match(/[\\d.]+/g)?.map(Number) || [0, 0, 0, 1]\n    return nums.length === 3\n      ? [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0, 1]\n      : [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0, nums[3] ?? 1]\n  }\n  if (Array.isArray(input)) {\n    return input.length === 3\n      ? [input[0] ?? 0, input[1] ?? 0, input[2] ?? 0, 1]\n      : [input[0] ?? 0, input[1] ?? 0, input[2] ?? 0, input[3] ?? 1]\n  }\n  return [0, 0, 0, 1]\n}\n\n// RGB -> HSL\nfunction rgbToHsl([r, g, b]: [number, number, number]): [number, number, number] {\n  r /= 255\n  g /= 255\n  b /= 255\n  const max = Math.max(r, g, b),\n    min = Math.min(r, g, b)\n  let h = 0,\n    s = 0\n  const l = (max + min) / 2\n  if (max !== min) {\n    const d = max - min\n    s = l > 0.5 ? d / (2 - max - min) : d / (max + min)\n    switch (max) {\n      case r:\n        h = (g - b) / d + (g < b ? 6 : 0)\n        break\n      case g:\n        h = (b - r) / d + 2\n        break\n      case b:\n        h = (r - g) / d + 4\n        break\n    }\n    h /= 6\n  }\n  return [h * 360, s * 100, l * 100]\n}\n\n// RGB -> OKLab\nfunction rgbToOklab([r, g, b]: [number, number, number]): [number, number, number] {\n  // convert sRGB -> linear\n  const R = sRGBToLinear(r),\n    G = sRGBToLinear(g),\n    B = sRGBToLinear(b)\n  // linear RGB -> LMS\n  const L = 0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B\n  const M = 0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B\n  const S = 0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B\n  // LMS -> OKLab\n  const l_ = Math.cbrt(L * 0.8189330101 + M * 0.3618667424 - S * 0.1288597137)\n  const m_ = Math.cbrt(L * 0.0329845436 + M * 0.9293118715 + S * 0.0361456387)\n  const s_ = Math.cbrt(L * 0.0482003018 + M * 0.2643662691 + S * 0.633851707)\n  const L_ok = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_\n  const a_ok = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_\n  const b_ok = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_\n  return [L_ok, a_ok, b_ok]\n}\n\n// OKLab -> OKLCH\nfunction oklabToOklch([L, a, b]: [number, number, number]): [number, number, number] {\n  const C = Math.sqrt(a * a + b * b)\n  let H = Math.atan2(b, a) * (180 / Math.PI)\n  if (H < 0) H += 360\n  return [L, C, H]\n}\n\n// Main format function\nexport function formatColor(input: string | number[], format: string = 'hex'): string {\n  const [r, g, b, alpha] = parseRGB(input)\n  switch (format) {\n    case 'rgba':\n      return `rgba(${r}, ${g}, ${b}, ${alpha})`\n    case 'hsl': {\n      const [h, s, l] = rgbToHsl([r, g, b])\n      return `hsl(${h.toFixed(0)}, ${s.toFixed(0)}%, ${l.toFixed(0)}%)`\n    }\n    case 'oklab': {\n      const [L, a, b_] = rgbToOklab([r, g, b])\n      return `oklab(${L.toFixed(3)}, ${a.toFixed(3)}, ${b_.toFixed(3)})`\n    }\n    case 'oklch': {\n      const oklch = oklabToOklch(rgbToOklab([r, g, b]))\n      return `oklch(${oklch[0].toFixed(3)}, ${oklch[1].toFixed(3)}, ${oklch[2].toFixed(0)})`\n    }\n    case 'hex':\n    default:\n      return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`\n  }\n}\n"
  },
  {
    "path": "src/ui/helpers/customHooks.ts",
    "content": "import { useState, useEffect, useCallback } from 'react'\nimport type { ChangeEvent } from 'react'\nimport { useLocation } from 'react-router-dom'\n\nexport function useInput(initialValue: string): [string, (e: ChangeEvent<HTMLInputElement>) => void] {\n  const [value, setValue] = useState(initialValue)\n  function handleChange(e: ChangeEvent<HTMLInputElement>) {\n    setValue(e.target.value)\n  }\n  return [value, handleChange]\n}\n\nexport function ScrollToTop(): null {\n  const { pathname } = useLocation()\n\n  useEffect(() => {\n    window.scrollTo(0, 0)\n  }, [pathname])\n\n  return null\n}\n\nexport function useAutoResize(): void {\n  const { pathname } = useLocation()\n\n  const updateHeight = useCallback(() => {\n    // Small delay to let content render\n    requestAnimationFrame(() => {\n      const height = document.body.scrollHeight\n      parent.postMessage({ pluginMessage: { type: 'resize', height } }, '*')\n    })\n  }, [])\n\n  useEffect(() => {\n    updateHeight()\n  }, [pathname, updateHeight])\n\n  useEffect(() => {\n    // Also resize on window resize and DOM changes\n    const observer = new ResizeObserver(updateHeight)\n    observer.observe(document.body)\n    return () => observer.disconnect()\n  }, [updateHeight])\n}\n"
  },
  {
    "path": "src/ui/helpers/helpers.ts",
    "content": "import { formatColor } from './colorFormatter'\nimport type { ThemeState, CleanTheme, ColorItem } from '../../shared/types'\n\nexport function calculatePosition(index: number, basePosition: number, length: number): string {\n  let value = ''\n  if (index === basePosition) {\n    value = 'base'\n  }\n  if (index === basePosition - 1) {\n    value = 'sm'\n  } else if (index === basePosition - 2) {\n    value = 'xs'\n  } else if (index === basePosition + 1) {\n    value = 'lg'\n  } else if (index === basePosition + 2) {\n    value = 'xl'\n  } else if (index < basePosition - 2) {\n    const numberOfSmaller = length - (length - (basePosition - 2))\n    const position = -(index - (numberOfSmaller + 1))\n    value = `${position}xs`\n  } else if (index > basePosition + 2) {\n    const numberOfBigger = length - (basePosition + 3)\n    const position = numberOfBigger - (length - (index + 2))\n    value = `${position}xl`\n  }\n  return value\n}\n\nexport function getBorderRadiusName(\n  index: number,\n  defaultIndex: number | null,\n  total: number\n): string {\n  // If no default selected, use numeric naming\n  if (defaultIndex === null) {\n    return String(index)\n  }\n\n  const offset = index - defaultIndex\n\n  if (offset === 0) return 'DEFAULT'\n  if (offset === -1) return 'sm'\n  if (offset === -2) return 'xs'\n  if (offset === 1) return 'lg'\n  if (offset === 2) return 'xl'\n  if (offset === 3) return '2xl'\n  if (offset === 4) return '3xl'\n\n  // For values smaller than xs\n  if (offset < -2) {\n    const xsCount = Math.abs(offset) - 1\n    return `${xsCount}xs`\n  }\n\n  // For values larger than 3xl\n  if (offset > 4) {\n    return `${offset - 1}xl`\n  }\n\n  return String(index)\n}\n\nexport function getPart(name: string, i: number): string {\n  let cleanName = name\n  const parts = name.split('/')\n  if (name.indexOf('/') !== -1) {\n    cleanName = parts[parts.length - i] ?? name\n  }\n  return cleanName\n}\n\nexport function groupColors(colors: ColorItem[]): Record<string, string | Record<string, string>> {\n  const groupedColors: Record<string, string | Record<string, string>> = {}\n  colors.forEach((color) => {\n    const { name, value } = color\n    const key = getPart(name, 2)\n    if (!groupedColors[key]) {\n      groupedColors[key] = {}\n    }\n    const cleanName = getPart(name, 1)\n    if (cleanName === name) {\n      groupedColors[key] = value\n    } else {\n      const current = groupedColors[key]\n      if (typeof current === 'object') {\n        current[cleanName] = value\n      }\n    }\n  })\n  return groupedColors\n}\n\nexport function cleanupTheme(\n  theme: ThemeState,\n  colorFormat: string,\n  grouped: boolean,\n  useRem: boolean = true,\n  defaultBorderRadiusIndex: number | null = null\n): CleanTheme {\n  const cleanTheme: CleanTheme = {}\n\n  // Handle colors\n  if (theme.colors.length > 0) {\n    const formattedColors = theme.colors.map(({ name, value }) => ({\n      name,\n      value: formatColor(value, colorFormat),\n    }))\n    cleanTheme.colors = grouped\n      ? groupColors(formattedColors)\n      : Object.assign({}, ...formattedColors.map((c) => ({ [c.name]: c.value })))\n  }\n\n  // Handle fontFamily\n  if (theme.fontFamily.length > 0) {\n    cleanTheme.fontFamily = Object.assign(\n      {},\n      ...theme.fontFamily.map(({ name, value }) => ({ [name]: value }))\n    )\n  }\n\n  // Handle fontSize\n  if (theme.fontSize.length > 0) {\n    cleanTheme.fontSize = Object.assign(\n      {},\n      ...theme.fontSize.map(({ name, value }) => ({\n        [name]: useRem ? `${Number(value) / 16}rem` : `${value}px`,\n      }))\n    )\n  }\n\n  // Handle boxShadow\n  if (theme.boxShadow.length > 0) {\n    cleanTheme.boxShadow = Object.assign(\n      {},\n      ...theme.boxShadow.map(({ name, value }) => ({ [name]: value }))\n    )\n  }\n\n  // Handle borderRadius\n  if (theme.borderRadius.length > 0) {\n    cleanTheme.borderRadius = Object.assign(\n      {},\n      ...theme.borderRadius.map(({ name, value }, index) => {\n        const radiusName =\n          name || getBorderRadiusName(index, defaultBorderRadiusIndex, theme.borderRadius.length)\n        // Convert to rem or keep as px based on preference\n        const numValue = typeof value === 'number' ? value : parseFloat(String(value))\n        const radiusValue = useRem ? `${numValue / 16}rem` : `${numValue}px`\n        return { [radiusName]: radiusValue }\n      })\n    )\n  }\n\n  return cleanTheme\n}\n\nexport function rgbToHex(int: number): string {\n  let hex = Number(int).toString(16)\n  if (hex.length < 2) {\n    hex = `0${hex}`\n  }\n  return hex\n}\n\nexport function makeHex(r: number, g: number, b: number): string {\n  const red = rgbToHex(r)\n  const green = rgbToHex(g)\n  const blue = rgbToHex(b)\n  return `#${red}${green}${blue}`\n}\n\nexport function makeRgb(color: { r: number; g: number; b: number; a?: number }): {\n  r: number\n  g: number\n  b: number\n  a: number\n} {\n  const r = Math.round(255 * color.r)\n  const g = Math.round(255 * color.g)\n  const b = Math.round(255 * color.b)\n  const a = Math.round(100 * (color.a ?? 1)) / 100\n  return { r, g, b, a }\n}\n\nexport function formatTailwind4Theme(cleanTheme: CleanTheme): string {\n  const prefixMap: Record<string, string> = {\n    colors: 'color',\n    fontFamily: 'font',\n    fontSize: 'text',\n    fontWeight: 'font-weight',\n    letterSpacing: 'tracking',\n    lineHeight: 'leading',\n    borderRadius: 'radius',\n    boxShadow: 'shadow',\n  }\n\n  const lines: string[] = []\n\n  const flattenColors = (obj: Record<string, string | Record<string, string>>) => {\n    for (const [colorName, value] of Object.entries(obj)) {\n      if (typeof value === 'string') {\n        lines.push(`  --color-${colorName}: ${value};`)\n      } else if (typeof value === 'object' && value !== null) {\n        for (const [variant, val] of Object.entries(value)) {\n          const safeVariant = variant.replace('/', '-')\n          lines.push(`  --color-${colorName}-${safeVariant}: ${val};`)\n        }\n      }\n    }\n  }\n\n  for (const [section, values] of Object.entries(cleanTheme)) {\n    const ns = prefixMap[section]\n    if (!ns || typeof values !== 'object') continue\n\n    if (section === 'colors') {\n      flattenColors(values as Record<string, string | Record<string, string>>)\n      continue\n    }\n\n    if (section === 'boxShadow') {\n      for (const [key, val] of Object.entries(values)) {\n        if (!val) continue\n        const cleanKey = key.replace(/^shadow-/, '')\n        lines.push(`  --shadow-${cleanKey}: ${val};`)\n      }\n      continue\n    }\n\n    if (section === 'borderRadius') {\n      for (const [key, val] of Object.entries(values)) {\n        // DEFAULT becomes --radius (no suffix), others get the key as suffix\n        const varName = key === 'DEFAULT' ? '--radius' : `--radius-${key}`\n        lines.push(`  ${varName}: ${val};`)\n      }\n      continue\n    }\n\n    for (const [key, val] of Object.entries(values)) {\n      lines.push(`  --${ns}-${key}: ${val};`)\n    }\n  }\n\n  return `@theme {\\n${lines.join('\\n')}\\n}`\n}\n\nexport function formatTailwind3Theme(cleanTheme: CleanTheme): string {\n  const themeObj: Record<string, unknown> = {}\n\n  if (cleanTheme.colors) {\n    themeObj.colors = cleanTheme.colors\n  }\n  if (cleanTheme.fontFamily) {\n    themeObj.fontFamily = cleanTheme.fontFamily\n  }\n  if (cleanTheme.fontSize) {\n    themeObj.fontSize = cleanTheme.fontSize\n  }\n  if (cleanTheme.boxShadow) {\n    themeObj.boxShadow = cleanTheme.boxShadow\n  }\n  if (cleanTheme.borderRadius) {\n    themeObj.borderRadius = cleanTheme.borderRadius\n  }\n\n  const formatValue = (val: unknown, indent = 2): string => {\n    if (typeof val === 'string') {\n      return `'${val}'`\n    }\n    if (typeof val === 'number') {\n      return String(val)\n    }\n    if (Array.isArray(val)) {\n      return `[${val.map((v) => formatValue(v)).join(', ')}]`\n    }\n    if (typeof val === 'object' && val !== null) {\n      const entries = Object.entries(val)\n      if (entries.length === 0) return '{}'\n      const spaces = ' '.repeat(indent)\n      const innerSpaces = ' '.repeat(indent + 2)\n      const lines = entries.map(([k, v]) => {\n        const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `'${k}'`\n        return `${innerSpaces}${key}: ${formatValue(v, indent + 2)}`\n      })\n      return `{\\n${lines.join(',\\n')}\\n${spaces}}`\n    }\n    return String(val)\n  }\n\n  return `module.exports = {\n  theme: {\n    extend: ${formatValue(themeObj, 4)}\n  }\n}`\n}\n"
  },
  {
    "path": "src/ui/helpers/randomMessages.ts",
    "content": "const messages = {\n  emptyColors: [\n    'Ooow, no colors?',\n    'hmmm, I see you have no colors',\n    'Care to add some colors?',\n    'Add some colors to brighten your day',\n  ],\n  emptyFamilies: [\n    \"I guess not every project needs a font family\",\n    'Default font stacks can do just fine',\n    'Always time to add them later',\n  ],\n  emptySizes: [\n    'No sizes? no problem!',\n    \"Ah, you're going with the default font sizes, neat!\",\n    'No font sizes detected',\n  ],\n  emptyShadows: [\n    'No shadows? Smoooth',\n    'As he faced the sun he cast no shadow',\n    'Who needs shadows anyway',\n  ],\n  emptyBorderRadii: ['Pretty square, right?', 'No curves? No problemo'],\n}\n\nexport default messages\n"
  },
  {
    "path": "src/ui/icons/Github.tsx",
    "content": "const Github = () => {\n  return (\n    <svg\n      height=\"18\"\n      width=\"18\"\n      viewBox=\"0 0 16 16\"\n      version=\"1.1\"\n      aria-hidden=\"true\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        fill=\"currentColor\"\n        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\"\n      ></path>\n    </svg>\n  )\n}\n\nexport default Github\n"
  },
  {
    "path": "src/ui/icons/Tailwind.tsx",
    "content": "const Tailwind = () => {\n  return (\n    <svg\n      width=\"22\"\n      height=\"18\"\n      viewBox=\"0 0 18 12\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        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\"\n        fill=\"url(#paint0_linear)\"\n      />\n      <defs>\n        <linearGradient\n          id=\"paint0_linear\"\n          x1=\"4.47292e-07\"\n          y1=\"-8.99991\"\n          x2=\"18\"\n          y2=\"20.9999\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop stopColor=\"#2383AE\" />\n          <stop offset=\"1\" stopColor=\"#6DD7B9\" />\n        </linearGradient>\n      </defs>\n    </svg>\n  )\n}\n\nexport default Tailwind\n"
  },
  {
    "path": "src/ui/main.tsx",
    "content": "import { createRoot } from 'react-dom/client'\nimport { useThemeStore } from './store/themeStore'\nimport App from './App'\nimport './styles/app.css'\nimport type { ThemeState } from '../shared/types'\n\n// Listen for the initial message from the Figma plugin\nwindow.onmessage = (event: MessageEvent) => {\n  const theme = event.data.pluginMessage as Partial<ThemeState>\n  useThemeStore.getState().initializeTheme(theme)\n\n  const container = document.getElementById('app')\n  if (container) {\n    const root = createRoot(container)\n    root.render(<App />)\n  }\n\n  // Only process the first message\n  window.onmessage = null\n}\n"
  },
  {
    "path": "src/ui/store/themeStore.ts",
    "content": "import { create } from 'zustand'\nimport type {\n  ThemeState,\n  ColorItem,\n  FontSizeItem,\n  FontFamilyItem,\n  ShadowItem,\n  BorderRadiusItem,\n  Preferences,\n} from '../../shared/types'\n\ninterface ThemeActions {\n  initializeTheme: (theme: Partial<ThemeState>) => void\n  setColors: (colors: ColorItem[]) => void\n  setGradientColors: (gradientColors: string[]) => void\n  setFontSize: (fontSize: FontSizeItem[]) => void\n  setFontFamily: (fontFamily: FontFamilyItem[]) => void\n  setBoxShadow: (boxShadow: ShadowItem[]) => void\n  setBorderRadius: (borderRadius: BorderRadiusItem[]) => void\n  setBaseFontSize: (baseFontSize: number | false) => void\n  setDefaultBorderRadiusIndex: (index: number | null) => void\n  setPreferences: (preferences: Partial<Preferences>) => void\n  updateColor: (index: number, color: ColorItem) => void\n  removeColor: (index: number) => void\n}\n\nconst defaultPreferences: Preferences = {\n  tailwindVersion: '4',\n  colorFormat: 'hex',\n  grouped: false,\n  useRem: true,\n}\n\nexport const useThemeStore = create<ThemeState & ThemeActions>((set) => ({\n  // State\n  colors: [],\n  gradientColors: [],\n  fontSize: [],\n  fontFamily: [],\n  boxShadow: [],\n  borderRadius: [],\n  baseFontSize: false,\n  defaultBorderRadiusIndex: null,\n  preferences: defaultPreferences,\n\n  // Actions\n  initializeTheme: (theme) =>\n    set((state) => ({\n      ...state,\n      ...theme,\n      preferences: {\n        ...defaultPreferences,\n        ...theme.preferences,\n      },\n    })),\n\n  setColors: (colors) => set({ colors }),\n\n  setGradientColors: (gradientColors) => set({ gradientColors }),\n\n  setFontSize: (fontSize) => set({ fontSize }),\n\n  setFontFamily: (fontFamily) => set({ fontFamily }),\n\n  setBoxShadow: (boxShadow) => set({ boxShadow }),\n\n  setBorderRadius: (borderRadius) => set({ borderRadius }),\n\n  setBaseFontSize: (baseFontSize) => set({ baseFontSize }),\n\n  setDefaultBorderRadiusIndex: (index) => set({ defaultBorderRadiusIndex: index }),\n\n  setPreferences: (prefs) =>\n    set((state) => ({\n      preferences: { ...state.preferences, ...prefs },\n    })),\n\n  updateColor: (index, color) =>\n    set((state) => {\n      const newColors = [...state.colors]\n      newColors[index] = color\n      return { colors: newColors }\n    }),\n\n  removeColor: (index) =>\n    set((state) => ({\n      colors: state.colors.filter((_, i) => i !== index),\n    })),\n}))\n"
  },
  {
    "path": "src/ui/styles/app.css",
    "content": "@import \"tailwindcss\";\n\n@theme {\n  --color-code-black: #2a2734;\n  --color-code-green: #b5f4a5;\n  --color-code-yellow: #ffe484;\n  --color-code-purple: #d9a9ff;\n  --color-code-red: #ff8383;\n  --color-code-blue: #93ddfd;\n  --color-code-white: #fff;\n  --color-tailwind: #2e3748;\n}\n\n/* Base styles */\nhtml {\n  font-size: 100%;\n}\n\nbody {\n  @apply bg-white p-0 m-0 font-sans leading-normal text-slate-900 text-sm;\n}\n\n/* Typography utilities */\n@layer base {\n  .t-alpha {\n    @apply text-3xl;\n  }\n\n  .t-beta {\n    @apply text-base font-semibold text-slate-900;\n  }\n\n  .t-gamma {\n    @apply text-sm text-slate-900 font-medium leading-tight;\n  }\n\n  .intro {\n    @apply text-sm text-slate-500;\n  }\n}\n\n/* Color swatch */\n.color {\n  height: 32px;\n  width: 32px;\n  min-width: 32px;\n  box-sizing: border-box;\n  @apply rounded mr-3 flex-shrink-0;\n}\n\n/* Code block */\n.code-block {\n  @apply bg-code-black rounded-lg p-4 text-sm;\n}\n\n/* Switch toggle */\n.switch > div {\n  @apply transition duration-100 ease-out;\n  transform: translateX(0%);\n}\n.switch.active > div {\n  transform: translateX(100%);\n}\n\n/* Form styles */\n.form-field {\n  @apply flex-1;\n}\n\n.form-label-wrapper {\n  @apply mb-2 flex flex-col items-start;\n}\n\n.form-label {\n  @apply leading-tight;\n}\n\n.form-instructions {\n  @apply text-sm;\n}\n\n.form-control {\n  @apply block w-full py-0 px-2 border border-slate-200 leading-normal rounded bg-white text-slate-900 text-sm shadow-sm;\n  height: 36px;\n}\n\n.form-control:focus {\n  @apply outline-none border-slate-400 ring-1 ring-slate-400;\n}\n\n.form-control--textarea {\n  height: auto;\n}\n\n/* Button styles */\n.button {\n  @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;\n  transition: 0.15s ease-in;\n}\n\n.button--white {\n  @apply bg-white text-slate-700 font-medium hover:bg-slate-50 border border-slate-200;\n}\n\n.button--grey {\n  @apply bg-transparent text-slate-700 font-medium hover:bg-slate-100 border border-slate-200;\n}\n\n.button--blue {\n  @apply bg-slate-100 text-slate-900 hover:bg-slate-200;\n}\n\n.button--green {\n  @apply bg-slate-900 text-white hover:bg-slate-800;\n}\n\n.button--disabled {\n  @apply bg-slate-200 text-slate-400;\n}\n\n.text-link {\n  @apply text-slate-600 hover:text-slate-900 underline;\n}\n\n/* Rich text styles */\n.richtext {\n  @apply text-slate-600 text-sm;\n}\n\n.richtext h2:first-child,\n.richtext h3:first-child,\n.richtext p:first-child {\n  @apply mt-0;\n}\n\n.richtext p {\n  @apply mt-2;\n}\n\n.richtext h2 {\n  @apply text-base font-semibold mt-3 text-slate-900;\n}\n\n.richtext h3 {\n  @apply text-sm text-slate-900 font-medium leading-tight mt-3;\n}\n\n.richtext ul,\n.richtext ol {\n  @apply mt-2;\n}\n\n.richtext ol {\n  @apply list-decimal list-inside;\n}\n\n.richtext ul > li,\n.richtext ol > li {\n  @apply relative mt-1;\n}\n\n.richtext ul > li {\n  @apply pl-4;\n}\n\n.richtext ul > li:before {\n  content: '';\n  top: 7px;\n  left: 0;\n  border-radius: 50%;\n  width: 4px;\n  height: 4px;\n  @apply absolute bg-slate-400;\n}\n\n.richtext a {\n  @apply text-slate-700 underline;\n}\n\n.richtext a:hover {\n  @apply text-slate-900;\n}\n\n.richtext pre code {\n  @apply p-3 my-3 block overflow-auto text-sm leading-normal bg-slate-100 rounded;\n}\n\n.richtext code {\n  padding: 0.15em 0.35em;\n  margin: 0;\n  font-size: 85%;\n  background-color: rgba(0, 0, 0, 0.04);\n  border-radius: 3px;\n}\n\n/* Prism/Code highlighting - override prism-react-renderer theme */\npre[class*='language-'] {\n  @apply p-4 rounded-lg text-sm font-mono;\n  background: #1d1f21 !important;\n  overflow: auto;\n  line-height: 1.5;\n}\n\ncode[class*='language-'] {\n  @apply font-mono;\n  background: transparent !important;\n}\n\n/* Select styling */\nselect {\n  @apply border border-slate-200 rounded px-2 py-1.5 bg-white text-slate-900 text-sm shadow-sm h-8;\n}\n\nselect:focus {\n  @apply outline-none ring-1 ring-slate-400 border-slate-400;\n}\n\n/* Radio button styling */\ninput[type=\"radio\"] {\n  @apply mr-1;\n}\n\n/* Grid gaps */\n.grid {\n  @apply gap-3;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noImplicitOverride\": true,\n\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false,\n\n    \"typeRoots\": [\"./node_modules/@types\", \"./node_modules/@figma\"]\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "ui.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Figma TailwindCSS</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/ui/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "vite.config.code.ts",
    "content": "import { defineConfig } from 'vite'\nimport { resolve } from 'path'\n\n// Figma sandbox code build - outputs IIFE for code.js\nexport default defineConfig({\n  build: {\n    outDir: 'dist',\n    emptyOutDir: false,\n    lib: {\n      entry: resolve(__dirname, 'src/code/index.ts'),\n      name: 'code',\n      fileName: () => 'code.js',\n      formats: ['iife'],\n    },\n    rollupOptions: {\n      output: {\n        inlineDynamicImports: true,\n      },\n    },\n    target: 'es2020',\n    minify: false,\n  },\n})\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tailwindcss from '@tailwindcss/vite'\nimport { viteSingleFile } from 'vite-plugin-singlefile'\nimport { resolve } from 'path'\n\n// UI build config - outputs a single HTML file with inlined CSS/JS\nexport default defineConfig({\n  plugins: [react(), tailwindcss(), viteSingleFile({ removeViteModuleLoader: true })],\n  build: {\n    outDir: 'dist',\n    emptyOutDir: true,\n    assetsInlineLimit: 100000000,\n    chunkSizeWarningLimit: 100000000,\n    cssCodeSplit: false,\n    rollupOptions: {\n      input: resolve(__dirname, 'ui.html'),\n      output: {\n        inlineDynamicImports: true,\n      },\n    },\n  },\n})\n"
  }
]