├── .eslintrc.cjs ├── .github └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .storybook ├── StoryLayout.tsx ├── main.ts ├── preview-head.html ├── preview.tsx └── storybook-reset.css ├── LICENSE ├── README.md ├── commitlint.config.ts ├── docs ├── getting-started.mdx ├── introduction.mdx └── styling.mdx ├── figma-kit ├── package.json ├── postcss.config.cjs ├── release.config.cjs ├── src │ ├── components │ │ ├── alert-dialog │ │ │ ├── alert-dialog.css │ │ │ ├── alert-dialog.stories.tsx │ │ │ ├── alert-dialog.tsx │ │ │ └── index.ts │ │ ├── button │ │ │ ├── button.css │ │ │ ├── button.stories.tsx │ │ │ ├── button.tsx │ │ │ └── index.ts │ │ ├── checkbox │ │ │ ├── checkbox.css │ │ │ ├── checkbox.stories.tsx │ │ │ ├── checkbox.tsx │ │ │ └── index.ts │ │ ├── collapsible │ │ │ ├── collapsible.css │ │ │ ├── collapsible.stories.tsx │ │ │ ├── collapsible.tsx │ │ │ └── index.ts │ │ ├── color-picker │ │ │ ├── color-picker-alpha.tsx │ │ │ ├── color-picker-area.tsx │ │ │ ├── color-picker-hue.tsx │ │ │ ├── color-picker-input.tsx │ │ │ ├── color-picker.css │ │ │ ├── color-picker.stories.tsx │ │ │ ├── color-picker.tsx │ │ │ └── index.ts │ │ ├── context-menu │ │ │ ├── context-menu.stories.tsx │ │ │ ├── context-menu.tsx │ │ │ └── index.ts │ │ ├── dialog.base │ │ │ ├── dialog.base.css │ │ │ ├── dialog.base.tsx │ │ │ └── index.ts │ │ ├── dialog │ │ │ ├── dialog.css │ │ │ ├── dialog.stories.tsx │ │ │ ├── dialog.tsx │ │ │ └── index.ts │ │ ├── dropdown-menu │ │ │ ├── dropdown-menu.stories.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ └── index.ts │ │ ├── flex │ │ │ ├── flex.css │ │ │ ├── flex.stories.tsx │ │ │ ├── flex.tsx │ │ │ └── index.ts │ │ ├── icon-button │ │ │ ├── icon-button.css │ │ │ ├── icon-button.stories.tsx │ │ │ ├── icon-button.tsx │ │ │ └── index.ts │ │ ├── icon │ │ │ ├── icon.css │ │ │ ├── icon.tsx │ │ │ └── index.ts │ │ ├── icons │ │ │ ├── checkmark-indeterminate.tsx │ │ │ ├── checkmark.tsx │ │ │ ├── chevron-down.tsx │ │ │ ├── chevron-left.tsx │ │ │ ├── chevron-right.tsx │ │ │ ├── chevron-up.tsx │ │ │ ├── circle.tsx │ │ │ ├── close.tsx │ │ │ ├── index.ts │ │ │ ├── plus.tsx │ │ │ └── styles.tsx │ │ ├── input │ │ │ ├── index.ts │ │ │ ├── input.css │ │ │ ├── input.stories.tsx │ │ │ └── input.tsx │ │ ├── menu.base │ │ │ └── menu.base.css │ │ ├── popover │ │ │ ├── index.ts │ │ │ ├── popover.stories.tsx │ │ │ └── popover.tsx │ │ ├── radio-group │ │ │ ├── index.ts │ │ │ ├── radio-group.css │ │ │ ├── radio-group.stories.tsx │ │ │ └── radio-group.tsx │ │ ├── segmented-control │ │ │ ├── index.ts │ │ │ ├── segmented-control.css │ │ │ ├── segmented-control.stories.tsx │ │ │ └── segmented-control.tsx │ │ ├── select │ │ │ ├── index.ts │ │ │ ├── select.css │ │ │ ├── select.stories.tsx │ │ │ └── select.tsx │ │ ├── slider │ │ │ ├── index.ts │ │ │ ├── slider.css │ │ │ ├── slider.stories.tsx │ │ │ └── slider.tsx │ │ ├── switch │ │ │ ├── index.ts │ │ │ ├── switch.css │ │ │ ├── switch.stories.tsx │ │ │ └── switch.tsx │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── tabs.css │ │ │ ├── tabs.stories.tsx │ │ │ └── tabs.tsx │ │ ├── text │ │ │ ├── index.ts │ │ │ ├── text.css │ │ │ ├── text.stories.tsx │ │ │ └── text.tsx │ │ ├── textarea │ │ │ ├── index.ts │ │ │ ├── textarea.css │ │ │ ├── textarea.stories.tsx │ │ │ └── textarea.tsx │ │ ├── tooltip │ │ │ ├── index.ts │ │ │ ├── tooltip.css │ │ │ └── tooltip.tsx │ │ └── value-field │ │ │ ├── index.ts │ │ │ ├── named-colors.ts │ │ │ ├── types.ts │ │ │ ├── value-field-base.tsx │ │ │ ├── value-field-elements.tsx │ │ │ ├── value-field-hex.tsx │ │ │ ├── value-field-numeric.tsx │ │ │ ├── value-field.css │ │ │ ├── value-field.mdx │ │ │ └── value-field.stories.tsx │ ├── index.ts │ ├── lib │ │ ├── color.ts │ │ ├── constants.ts │ │ ├── dom │ │ │ ├── focus.ts │ │ │ └── set-input-value.ts │ │ ├── number │ │ │ ├── clamp.ts │ │ │ └── normalize.ts │ │ └── react │ │ │ ├── create-context.tsx │ │ │ ├── use-compose-refs.ts │ │ │ ├── use-controllable-state.ts │ │ │ └── use-select-on-input-click.tsx │ ├── styles │ │ ├── figma-development-theme.css │ │ ├── index.css │ │ └── tokens │ │ │ ├── components.css │ │ │ ├── font-size.css │ │ │ ├── font-weight.css │ │ │ ├── font.css │ │ │ ├── letter-spacing.css │ │ │ ├── line-height.css │ │ │ ├── radius.css │ │ │ ├── shadow.css │ │ │ └── space.css │ └── tailwind │ │ └── preset.js ├── test │ ├── setup.ts │ ├── value-field-base.test.tsx │ ├── value-field-hex.test.tsx │ ├── value-field-numeric-evaluator.test.ts │ └── value-field-numeric.test.tsx ├── tsconfig.json └── vite.config.ts ├── generate-react-cli.json ├── media ├── github-banner-dark.png └── github-banner-light.png ├── package.json ├── patches └── @radix-ui__react-slider@1.2.0.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── tsconfig.node.json └── website ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.tsx ├── index.css ├── lib │ └── .keep ├── main.tsx ├── views │ └── .keep └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'prettier', 8 | 'plugin:react-hooks/recommended', 9 | 'plugin:storybook/recommended', 10 | ], 11 | plugins: ['react-refresh', 'import'], 12 | ignorePatterns: ['dist', '.eslintrc.cjs'], 13 | parser: '@typescript-eslint/parser', 14 | settings: { 15 | 'import/parsers': { 16 | '@typescript-eslint/parser': ['.ts', '.tsx'], 17 | }, 18 | 'import/resolver': { 19 | typescript: { 20 | alwaysTryTypes: true, 21 | project: ['tsconfig.json', 'figma-kit/tsconfig.json', 'website/tsconfig.json'], 22 | }, 23 | }, 24 | }, 25 | overrides: [ 26 | { 27 | files: ['*.stories.tsx'], 28 | rules: { 29 | 'import/no-default-export': 'off', 30 | 'import/exports-last': 'off', 31 | }, 32 | }, 33 | ], 34 | rules: { 35 | 'no-console': ['error', { allow: ['warn', 'error'] }], 36 | 37 | // Import plugin rules 38 | // 39 | // https://github.com/import-js/eslint-plugin-import 40 | // https://typescript-eslint.io/docs/linting/troubleshooting#eslint-plugin-import 41 | 42 | //'import/no-cycle': 'error', // Extremely useful, yet extremely slow to keep enabled. Worth occasionally enabling locally for finding & removing circular imports. 43 | 'import/no-default-export': 'error', 44 | 'import/no-unresolved': 'error', 45 | 'import/no-self-import': 'error', 46 | 'import/no-relative-packages': 'error', 47 | 'import/no-duplicates': 'error', 48 | 'import/no-mutable-exports': 'error', 49 | 'import/exports-last': 'error', 50 | 51 | 'import/order': [ 52 | 'error', 53 | { 54 | groups: ['external', 'internal', 'parent', 'sibling', 'index'], 55 | }, 56 | ], 57 | 58 | // TypeScript overrides 59 | '@typescript-eslint/consistent-type-imports': 'error', // Avoid type-only imports being incorrectly bundled: https://vitejs.dev/guide/features.html#typescript 60 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: 'props|context|event' }], 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | checks: 8 | name: Checks 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | CI: true 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '20.x' 20 | 21 | - name: Setup pnpm 22 | uses: pnpm/action-setup@v3.0.0 23 | with: 24 | run_install: false 25 | 26 | - name: Get pnpm store directory 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 30 | 31 | - uses: actions/cache@v4 32 | name: Setup pnpm cache 33 | with: 34 | path: ${{ env.STORE_PATH }} 35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pnpm-store- 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Lint 43 | run: pnpm lint 44 | 45 | - name: Test 46 | run: pnpm test 47 | 48 | - name: Build 49 | run: pnpm build 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - beta 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: '20.x' 17 | 18 | - name: Setup pnpm 19 | uses: pnpm/action-setup@v3.0.0 20 | with: 21 | run_install: false 22 | 23 | - name: Get pnpm store directory 24 | shell: bash 25 | run: | 26 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 27 | 28 | - uses: actions/cache@v4 29 | name: Setup pnpm cache 30 | with: 31 | path: ${{ env.STORE_PATH }} 32 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pnpm-store- 35 | 36 | - name: Install dependencies 37 | run: pnpm install 38 | 39 | - name: Lint 40 | run: pnpm lint 41 | 42 | - name: Test 43 | run: pnpm test 44 | 45 | - name: Build 46 | run: pnpm build 47 | 48 | - name: Readme 49 | run: cp README.md ./figma-kit 50 | 51 | - name: Release 52 | working-directory: ./figma-kit 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | run: | 57 | npx semantic-release 58 | 59 | - name: Clean up 60 | run: rm ./figma-kit/README.md 61 | 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Build process CSS output 27 | figma-kit/*.css 28 | 29 | # Generated 30 | tsconfig.tsbuildinfo 31 | .eslintcache 32 | 33 | *storybook.log 34 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | node_modules/.bin/lint-staged 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "arrowParens": "always", 7 | "singleQuote": true, 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/StoryLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton, Link, Text, Tooltip } from '../figma-kit/src'; 3 | import { StoryContext } from '@storybook/react'; 4 | 5 | const StoryLayout = (props: { children: React.ReactNode; context: StoryContext }) => { 6 | const { children, context } = props; 7 | const shouldRenderRadixNotice = context.parameters.radixUrl && context.parameters.radixComponentName; 8 | 9 | return ( 10 |
11 | {shouldRenderRadixNotice && ( 12 | 13 | )} 14 |
{children}
15 |
16 | ); 17 | }; 18 | 19 | const RadixNotice = ({ name, url }: { name: string; url: string }) => { 20 | return ( 21 |
22 |
23 | 24 | Based on{' '} 25 | 26 | Radix {name} 27 | 28 | 29 | 30 | 35 | This component is built using the {name} primitive from Radix UI.
36 | Familiarity with Radix Primitives is recommended 37 |
38 | } 39 | > 40 | 41 | 42 | 43 | 44 |
45 | 46 | ); 47 | }; 48 | 49 | const LinkIcon = () => ( 50 | 51 | 57 | 58 | ); 59 | 60 | const InfoIcon = () => ( 61 | 62 | 66 | 67 | ); 68 | 69 | export { StoryLayout }; 70 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../docs/**/*.mdx', '../figma-kit/src/**/*.mdx', '../figma-kit/src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 6 | docs: { 7 | defaultName: 'Documentation', 8 | }, 9 | addons: [ 10 | '@storybook/addon-links', 11 | '@storybook/addon-essentials', 12 | { 13 | name: '@storybook/addon-storysource', 14 | options: { 15 | loaderOptions: { 16 | injectStoryParameters: false, 17 | }, 18 | }, 19 | }, 20 | 'storybook-dark-mode', 21 | ], 22 | framework: { 23 | name: '@storybook/react-vite', 24 | options: {}, 25 | }, 26 | async viteFinal(config) { 27 | const { mergeConfig } = await import('vite'); 28 | 29 | return mergeConfig(config, { 30 | plugins: [tsconfigPaths()], 31 | }); 32 | }, 33 | }; 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import './storybook-reset.css'; 3 | import '../figma-kit/src/styles/figma-development-theme.css'; 4 | import '../figma-kit/src/styles/index.css'; 5 | import { TooltipProvider } from '../figma-kit/src/components/tooltip'; 6 | import React from 'react'; 7 | import { StoryLayout } from './StoryLayout'; 8 | 9 | const preview: Preview = { 10 | argTypes: { 11 | children: { 12 | table: { 13 | disable: true, 14 | }, 15 | }, 16 | }, 17 | decorators: [ 18 | (Story, storyContext) => ( 19 | 20 | 21 | 22 | 23 | 24 | ), 25 | ], 26 | parameters: { 27 | layout: 'fullscreen', 28 | actions: { 29 | disable: true, 30 | }, 31 | backgrounds: { 32 | grid: { 33 | cellSize: 8, 34 | opacity: 0.25, 35 | cellAmount: 4, 36 | offsetX: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded' 37 | offsetY: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded' 38 | }, 39 | }, 40 | options: { 41 | storySort: { 42 | order: [ 43 | 'Introduction', 44 | 'Getting Started', 45 | 'Styling', 46 | 'Components', 47 | [ 48 | 'Text', 49 | 'Flex', 50 | 'Button', 51 | 'Icon Button', 52 | 'Switch', 53 | 'Input', 54 | 'Textarea', 55 | 'Value Field', 56 | 'Checkbox', 57 | 'Radio Group', 58 | 'Segmented Control', 59 | 'Slider', 60 | 'Popover', 61 | 'Dialog', 62 | 'Alert Dialog', 63 | 'Select', 64 | 'Dropdown Menu', 65 | 'Context Menu', 66 | ], 67 | ], 68 | }, 69 | }, 70 | controls: { 71 | matchers: { 72 | color: /(background|color)$/i, 73 | date: /Date$/i, 74 | }, 75 | }, 76 | darkMode: { 77 | darkClass: 'figma-dark', 78 | lightClass: 'light', 79 | classTarget: 'html', 80 | stylePreview: true, 81 | }, 82 | }, 83 | }; 84 | 85 | export default preview; 86 | -------------------------------------------------------------------------------- /.storybook/storybook-reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | html.figma-dark { 6 | background-color: #2c2c2c; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | } 12 | 13 | .story-layout { 14 | position: relative; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | height: 100vh; 19 | overflow-y: auto; 20 | } 21 | 22 | .sbdocs .story-layout { 23 | height: 350px; 24 | } 25 | 26 | .radix-notice { 27 | position: absolute; 28 | top: 16px; 29 | right: 16px; 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .radix-link { 35 | display: inline-flex; 36 | gap: 2px; 37 | align-items: center; 38 | } 39 | 40 | .beta-notice { 41 | border: 1px solid #fab815; 42 | background-color: rgba(255, 241, 194, 0.2); 43 | padding: 12px 16px; 44 | border-radius: 5px; 45 | margin: -24px auto 40px !important; 46 | max-width: 720px; 47 | 48 | p { 49 | margin: 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tigran Petrossian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > Figma Kit is currently in beta. There may be breaking changes before the final release. 3 | 4 |
5 | 6 | 7 | 8 | 9 | Figma kit logo 10 | 11 | 12 |

A set of React components for building Figma plugins.

13 |
14 | 15 | ### Overview 16 | 17 | Figma Kit is an extensive collection of React components that attempts to replicate Figma's user interface look & feel with near-100% feature parity. 18 | 19 | #### Features 20 | 21 | * UI3 Ready 22 | * Automatic dark mode 23 | * First-class Tailwind support with a preset 24 | 25 | ### Documentation 26 | 27 | The documentation for the library is on its way. Meanwhile, feel free to check out the [Storybook](https://storybook.figma-kit.dev) for basic guides, information about available components and functionality. 28 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, CodeOrSourceMdx, Source } from '@storybook/blocks'; 2 | 3 | 4 | 5 | ## Quick start 6 | 7 |
8 | 9 | ### Install Figma Kit 10 | 11 | ``` 12 | npm install figma-kit@beta 13 | ``` 14 | 15 | ### Enable access to Figma's semantic tokens in your plugin UI setup: 16 | 17 | Figma injects a set of [semantic color tokens as CSS custom properties](https://www.figma.com/plugin-docs/css-variables/) into plugin UI. Figma Kit 18 | relies on those to display the components correctly. Enable this by initializing your plugin UI 19 | as shown below: 20 | 21 | 22 | 23 | ### Import Figma Kit styles into your CSS 24 | 25 | 26 | 27 | Alternatively, if your plugin does not use CSS files, you can also import `styles.css` directly 28 | into your root JavaScript/TypeScript file: 29 | 30 | 31 | 32 | Importing CSS files into JS/TS might require additional setup, but it is typically already 33 | supported by build tools like Vite. 34 | 35 | ### Accessing Figma's semantic tokens outside of plugin environment 36 | 37 | When using the library outside of Figma plugin UI, e.g. in a normal browser, you'll still need 38 | access to semantic tokens for components to display correctly. Just for that purpose, Figma kit 39 | exports an optional theme file: 40 | 41 | 42 | 43 | This file is meant to be used for development purposes only, and **should not be included** in 44 | actual plugin code. 45 | 46 | ### Importing and using components 47 | 48 | { 53 | return Hello Figma 54 | } 55 | `} 56 | /> 57 | -------------------------------------------------------------------------------- /docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | import { Flex } from '../figma-kit/src'; 3 | 4 | 5 | 6 |
7 | Note: Figma Kit is currently in beta. 8 | 9 | The documentation website is under development, and there may be breaking changes before the final release. 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | Figma banner 18 | 19 | 20 | 21 | ## Overview 22 | 23 | Figma Kit is an extensive set of React components for building Figma plugins that look and 24 | behave like Figma's interface. 25 | 26 | ### Features 27 | 28 | - UI3 Ready: Components are designed to match Figma's new interface. 29 | - Automatic dark mode: Dark mode is supported out of the box without any setup or additional code, 30 | thanks to Figma's semantic color tokens. 31 | - Incremental Adoption: Components can be gradually integrated into existing plugins. 32 | -------------------------------------------------------------------------------- /docs/styling.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Source } from '@storybook/blocks'; 2 | 3 | 4 | 5 | ## Styling 6 | 7 | Figma Kit is designed to closely resemble Figma's interface, so minimal styling is needed, 8 | mainly for layout adjustments. All Figma Kit components accept the className prop and can be styled 9 | using any preferred styling solution. 10 | Figma injects an extensive set of [semantic tokens](https://www.figma.com/plugin-docs/css-variables/), 11 | which this library relies on, so familiarity with these tokens is recommended. 12 | 13 | 14 | ## Using with Tailwind 15 | 16 | Figma kit ships with a Tailwind preset that makes Figma theme tokens available in Tailwind classes: 17 | 18 | 19 | 20 | ```tsx 21 | import figmaKitPreset from 'figma-kit/tailwind.preset.js'; 22 | 23 | /** @type {import('tailwindcss').Config} */ 24 | export default { 25 | // ... 26 | presets: [figmaKitPreset], 27 | }; 28 | ``` 29 | 30 | 31 | This replaces the default Tailwind theme with Figma tokens, allowing you to use these tokens directly: 32 | 33 | 34 | ```tsx 35 | import { Text } from 'figma-kit' 36 | 37 | const App = () => { 38 | return ( 39 |
40 | Welcome to Figma 41 |
42 | ) 43 | } 44 | ``` 45 | 46 | The preset is built to match Figma's [semantic token format](https://www.figma.com/plugin-docs/css-variables/#tokens-overview), 47 | which differs slightly from typical Tailwind themes. Here, properties are 48 | strictly mapped to their respective tokens: 49 | 50 | ```tsx 51 |
52 |
53 |
54 |
55 | 56 | ``` 57 | 58 | Translates to: 59 | ```css 60 | .text { 61 | color: var(--figma-color-text); 62 | } 63 | .text-secondary { 64 | color: var(--figma-color-text-secondary); 65 | } 66 | .bg { 67 | background-color: var(--figma-color-bg); 68 | } 69 | .bg-secondary { 70 | background-color: var(--figma-color-bg-secondary); 71 | } 72 | ``` 73 | 74 | ### Breaking out of strict property mapping 75 | 76 | In rare cases it might be necessary to reference a token for a different property. In rare cases, 77 | you may need to reference a token for a different property. This can be done using Tailwind's arbitrary value syntax: 78 | 79 | ```tsx 80 |
81 | ``` 82 | 83 | Note: This is an escape hatch intended for edge cases. Figma's color system is well-designed and 84 | rarely requires referencing unrelated tokens. 85 | -------------------------------------------------------------------------------- /figma-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-kit", 3 | "description": "A set of React components for building Figma plugins.", 4 | "homepage": "https://figma-kit.dev", 5 | "version": "0.0.0-semantic-release", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/tigranpetrossian/figma-kit" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "figma", 14 | "figma react components", 15 | "figma design system", 16 | "UI3", 17 | "component-library" 18 | ], 19 | "type": "module", 20 | "files": [ 21 | "dist", 22 | "*.css" 23 | ], 24 | "exports": { 25 | "./styles.css": "./dist/styles.css", 26 | "./figma-development-theme.css": "./dist/figma-development-theme.css", 27 | ".": { 28 | "require": "./dist/index.cjs", 29 | "import": "./dist/index.mjs", 30 | "types": "./dist/index.d.ts" 31 | }, 32 | "./tailwind.preset.js": { 33 | "require": "./dist/tailwind.preset.js" 34 | }, 35 | "./*": "./*" 36 | }, 37 | "main": "dist/index.cjs", 38 | "module": "dist/index.mjs", 39 | "typings": "dist/index.d.ts", 40 | "sideEffects": false, 41 | "scripts": { 42 | "dev": "vite build --mode development --watch", 43 | "build": "pnpm build:js && pnpm build:css && pnpm build:figma-dev-theme-css && pnpm build:tailwind-preset", 44 | "build:js": "vite build", 45 | "build:css": "postcss src/styles/index.css -o dist/styles.css", 46 | "build:figma-dev-theme-css": "postcss src/styles/figma-development-theme.css -o dist/figma-development-theme.css", 47 | "build:tailwind-preset": "cp src/tailwind/preset.js dist/tailwind.preset.js", 48 | "test": "vitest" 49 | }, 50 | "peerDependencies": { 51 | "react": "^18.2.0", 52 | "react-dom": "^18.2.0" 53 | }, 54 | "dependencies": { 55 | "@emmetio/math-expression": "^1.0.5", 56 | "@radix-ui/react-alert-dialog": "^1.1.1", 57 | "@radix-ui/react-collapsible": "^1.1.0", 58 | "@radix-ui/react-context-menu": "^2.1.5", 59 | "@radix-ui/react-dialog": "^1.1.1", 60 | "@radix-ui/react-dropdown-menu": "^2.0.6", 61 | "@radix-ui/react-popover": "^1.1.1", 62 | "@radix-ui/react-radio-group": "^1.2.0", 63 | "@radix-ui/react-select": "^2.0.0", 64 | "@radix-ui/react-slider": "1.2.0", 65 | "@radix-ui/react-slot": "^1.1.0", 66 | "@radix-ui/react-switch": "^1.0.3", 67 | "@radix-ui/react-tabs": "^1.1.0", 68 | "@radix-ui/react-toggle-group": "^1.1.0", 69 | "@radix-ui/react-tooltip": "^1.0.7", 70 | "class-variance-authority": "^0.7.0", 71 | "merge-props": "^6.0.0", 72 | "react-textarea-autosize": "^8.5.3", 73 | "remeda": "^2.0.5" 74 | }, 75 | "devDependencies": { 76 | "@types/react": "^18.2.66", 77 | "@types/react-dom": "^18.2.22" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /figma-kit/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-nesting'), 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /figma-kit/release.config.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('semantic-release').GlobalConfig} 3 | */ 4 | module.exports = { 5 | plugins: [ 6 | [ 7 | '@semantic-release/commit-analyzer', 8 | { 9 | preset: 'conventionalcommits', 10 | releaseRules: [ 11 | { type: 'feat', release: 'minor' }, 12 | { type: 'fix', release: 'patch' }, 13 | { type: 'perf', release: 'patch' }, 14 | { type: 'docs', scope: 'README', release: 'patch' }, 15 | ], 16 | }, 17 | ], 18 | '@semantic-release/release-notes-generator', 19 | '@semantic-release/npm', 20 | '@semantic-release/github', 21 | ], 22 | branches: ['main', { name: 'beta', prerelease: true }], 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /figma-kit/src/components/alert-dialog/alert-dialog.css: -------------------------------------------------------------------------------- 1 | .fp-AlertDialogContent { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | max-width: calc(100vw - 32px); 7 | max-height: 80%; 8 | display: flex; 9 | flex-direction: column; 10 | gap: var(--space-4); 11 | padding: var(--space-4); 12 | 13 | &:where(.fp-placement-center) { 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | } 18 | 19 | &:where(.fp-placement-top) { 20 | top: min(10vh, 80px); 21 | left: 50%; 22 | transform: translateX(-50%); 23 | } 24 | 25 | &:where(.fp-size-1) { 26 | width: 288px; 27 | } 28 | 29 | &:where(.fp-size-2) { 30 | width: 352px; 31 | } 32 | 33 | &:where(.fp-size-3) { 34 | width: 448px; 35 | } 36 | } 37 | 38 | .fp-AlertDialogTitle { 39 | margin: 0; 40 | } 41 | 42 | .fp-AlertDialogActions { 43 | display: flex; 44 | justify-content: flex-end; 45 | align-items: center; 46 | gap: var(--space-2); 47 | padding-top: var(--space-2); 48 | } 49 | -------------------------------------------------------------------------------- /figma-kit/src/components/alert-dialog/alert-dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Button } from '@components/button'; 3 | import * as AlertDialog from './alert-dialog'; 4 | 5 | type Story = StoryObj; 6 | 7 | const meta: Meta = { 8 | title: 'Components/Alert Dialog', 9 | component: AlertDialog.Root, 10 | parameters: { 11 | radixUrl: 'https://www.radix-ui.com/primitives/docs/components/alert-dialog', 12 | radixComponentName: 'Alert Dialog', 13 | }, 14 | }; 15 | 16 | export default meta; 17 | 18 | export const Story: Story = { 19 | args: {}, 20 | render: () => { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | Dialog with primary destructive action 29 | 30 | Moving the file out of Team Foo means some people might lose access to it. 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /figma-kit/src/components/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert-dialog'; 2 | -------------------------------------------------------------------------------- /figma-kit/src/components/button/button.css: -------------------------------------------------------------------------------- 1 | .fp-Button { 2 | box-sizing: border-box; 3 | background-clip: border-box; 4 | background-color: transparent; 5 | user-select: none; 6 | appearance: none; 7 | border: 0; 8 | cursor: default; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | flex-shrink: 0; 13 | width: fit-content; 14 | outline-width: 1px; 15 | outline-offset: -1px; 16 | outline-style: solid; 17 | outline-color: transparent; 18 | font-family: var(--font-family-default); 19 | font-size: var(--font-size-default); 20 | font-weight: var(--font-weight-default); 21 | letter-spacing: var(--letter-spacing-default); 22 | line-height: var(--line-height-default); 23 | border-radius: var(--radius-medium); 24 | } 25 | 26 | .fp-Button:where(.fp-variant-secondary) { 27 | --button-color-bg: transparent; 28 | --button-color-border: var(--figma-color-text); 29 | --button-color-text: var(--figma-color-border); 30 | } 31 | 32 | .fp-Button:where(.fp-variant-secondary) { 33 | color: var(--figma-color-text); 34 | outline-color: var(--figma-color-border); 35 | 36 | &:active { 37 | background-color: var(--figma-color-bg-pressed); 38 | } 39 | 40 | &:focus-visible { 41 | outline-color: var(--figma-color-border-selected); 42 | } 43 | 44 | &:disabled { 45 | color: var(--figma-color-text-disabled); 46 | outline-color: var(--figma-color-border-disabled); 47 | } 48 | } 49 | 50 | .fp-Button:where(.fp-variant-primary) { 51 | background-color: var(--figma-color-bg-brand); 52 | color: var(--figma-color-text-onbrand); 53 | 54 | &:active { 55 | background-color: var(--figma-color-bg-brand-pressed); 56 | } 57 | 58 | &:focus-visible { 59 | outline-color: var(--figma-color-border-onbrand-strong); 60 | box-shadow: 0 0 0 1px var(--figma-color-border-selected-strong); 61 | } 62 | 63 | &:disabled { 64 | color: var(--figma-color-text-ondisabled); 65 | background-color: var(--figma-color-bg-disabled); 66 | } 67 | } 68 | .fp-Button:where(.fp-variant-destructive) { 69 | background-color: var(--figma-color-bg-danger); 70 | color: var(--figma-color-text-ondanger); 71 | 72 | &:active { 73 | background-color: var(--figma-color-bg-danger-pressed); 74 | } 75 | 76 | &:focus-visible { 77 | outline-color: var(--figma-color-border-ondanger-strong); 78 | box-shadow: 0 0 0 1px var(--figma-color-border-danger-strong); 79 | } 80 | 81 | &:disabled { 82 | color: var(--figma-color-text-ondisabled); 83 | background-color: var(--figma-color-bg-disabled); 84 | } 85 | } 86 | 87 | .fp-Button:where(.fp-variant-success) { 88 | background-color: var(--figma-color-bg-success); 89 | color: var(--figma-color-text-onsuccess); 90 | 91 | &:active { 92 | background-color: var(--figma-color-bg-success-pressed); 93 | } 94 | 95 | &:focus-visible { 96 | outline-color: var(--figma-color-border-onbrand-strong); 97 | box-shadow: 0 0 0 1px var(--figma-color-border-success-strong); 98 | } 99 | 100 | &:disabled { 101 | color: var(--figma-color-text-ondisabled); 102 | background-color: var(--figma-color-bg-disabled); 103 | } 104 | } 105 | 106 | .fp-Button:where(.fp-variant-inverse) { 107 | background-color: var(--figma-color-bg-inverse); 108 | color: var(--figma-color-text-oninverse); 109 | font-weight: 400; 110 | 111 | &:active { 112 | background-color: var(--figma-color-bg-brand-pressed); 113 | } 114 | 115 | &:focus-visible { 116 | outline-color: var(--figma-color-border-onbrand-strong); 117 | box-shadow: 0 0 0 1px var(--figma-color-border-selected); 118 | } 119 | 120 | &:disabled { 121 | color: var(--figma-color-text-ondisabled); 122 | background-color: var(--figma-color-bg-disabled); 123 | } 124 | } 125 | 126 | .fp-Button:where(.fp-variant-text) { 127 | background-color: transparent; 128 | outline-color: transparent; 129 | color: var(--figma-color-text); 130 | 131 | &:hover { 132 | background-color: var(--figma-color-bg-hover); 133 | } 134 | 135 | &:active { 136 | background-color: var(--figma-color-bg-pressed); 137 | } 138 | 139 | &:focus-visible { 140 | outline-color: var(--figma-color-border-selected); 141 | } 142 | 143 | &:disabled { 144 | color: var(--figma-color-text-disabled); 145 | outline-color: var(--figma-color-border-disabled); 146 | } 147 | } 148 | 149 | .fp-Button:where(.fp-full-width) { 150 | width: 100%; 151 | max-width: 100%; 152 | } 153 | 154 | .fp-Button:where(.fp-size-small) { 155 | height: var(--space-6); 156 | padding: var(--space-1) var(--space-2); 157 | } 158 | 159 | .fp-Button:where(.fp-size-medium) { 160 | height: var(--space-8); 161 | padding: var(--space-2) var(--space-3); 162 | } 163 | -------------------------------------------------------------------------------- /figma-kit/src/components/button/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import reactElementToJSXString from 'react-element-to-jsx-string'; 3 | import { Button } from './button'; 4 | 5 | type Story = StoryObj; 6 | 7 | const meta = { 8 | component: Button, 9 | title: 'Components/Button', 10 | args: { 11 | variant: undefined, 12 | }, 13 | parameters: { 14 | controls: { 15 | expanded: true, 16 | }, 17 | }, 18 | argTypes: { 19 | variant: { 20 | description: 'Contextual variant of the button.', 21 | type: 'string', 22 | table: { 23 | type: { 24 | summary: 'enum', 25 | }, 26 | defaultValue: { 27 | summary: 'secondary', 28 | }, 29 | }, 30 | options: ['primary', 'secondary', 'inverse', 'destructive', 'success', 'text'], 31 | control: { type: 'radio' }, 32 | }, 33 | size: { 34 | description: 'Size of the button.', 35 | type: 'string', 36 | table: { 37 | type: { 38 | summary: 'enum', 39 | }, 40 | defaultValue: { 41 | summary: 'small', 42 | }, 43 | }, 44 | options: ['small', 'medium'], 45 | control: { type: 'radio' }, 46 | }, 47 | fullWidth: { 48 | description: 'Set to `true` for the button to fill its parent container.', 49 | type: 'boolean', 50 | }, 51 | disabled: { 52 | description: 'Set to `true` to disable the button.', 53 | type: 'boolean', 54 | }, 55 | }, 56 | } satisfies Meta; 57 | 58 | export default meta; 59 | 60 | // eslint-disable-next-line 61 | const getCode = (args: any) => reactElementToJSXString( 32 | 33 | 34 | Show version history 35 | Publish library… 36 | Export… 37 | 38 | Add to sidebar 39 | 40 | Create branch… 41 | 42 | 43 | File color profile... 44 | 45 | Display P3 46 | Change to sRGB 47 | 48 | 49 | 50 | Duplicate 51 | Rename 52 | Move to project… 53 | Delete… 54 | 55 | This 56 | Or this 57 | Or this one 58 | 59 | Display P3 60 | Change to sRGB 61 | 62 | 63 | ); 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /figma-kit/src/components/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dropdown-menu'; 2 | -------------------------------------------------------------------------------- /figma-kit/src/components/flex/flex.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Flex } from './flex'; 3 | 4 | type Story = StoryObj; 5 | 6 | const meta: Meta = { 7 | title: 'Components/Flex', 8 | component: Flex, 9 | parameters: { 10 | controls: { 11 | expanded: true, 12 | }, 13 | }, 14 | argTypes: { 15 | direction: { 16 | description: 'The direction of the flex container.', 17 | type: 'string', 18 | table: { 19 | type: { 20 | summary: 'enum', 21 | }, 22 | defaultValue: { 23 | summary: 'row', 24 | }, 25 | }, 26 | options: ['row', 'column', 'rowReverse', 'columnReverse'], 27 | control: { type: 'radio' }, 28 | }, 29 | align: { 30 | description: 'The alignment of the flex items along the cross axis.', 31 | type: 'string', 32 | table: { 33 | type: { 34 | summary: 'enum', 35 | }, 36 | }, 37 | options: ['start', 'center', 'end', 'baseline', 'stretch'], 38 | control: { type: 'radio' }, 39 | }, 40 | justify: { 41 | description: 'The alignment of the flex items along the main axis.', 42 | type: 'string', 43 | table: { 44 | type: { 45 | summary: 'enum', 46 | }, 47 | }, 48 | options: ['start', 'center', 'end', 'between'], 49 | control: { type: 'radio' }, 50 | }, 51 | wrap: { 52 | description: 'The wrapping behavior of the flex container.', 53 | type: 'string', 54 | table: { 55 | type: { 56 | summary: 'enum', 57 | }, 58 | }, 59 | options: ['nowrap', 'wrap', 'wrapReverse'], 60 | control: { type: 'radio' }, 61 | }, 62 | gap: { 63 | description: 'The gap between flex items.', 64 | type: 'string', 65 | table: { 66 | type: { 67 | summary: 'string', 68 | }, 69 | }, 70 | options: [ 71 | '0', 72 | 'px', 73 | '0.5', 74 | '1', 75 | '1.5', 76 | '2', 77 | '2.5', 78 | '3', 79 | '3.5', 80 | '4', 81 | '5', 82 | '6', 83 | '7', 84 | '8', 85 | '9', 86 | '10', 87 | '11', 88 | '12', 89 | '13', 90 | '14', 91 | '15', 92 | '16', 93 | ], 94 | control: { type: 'select' }, 95 | }, 96 | columnGap: { 97 | description: 'The gap between flex columns.', 98 | type: 'string', 99 | table: { 100 | type: { 101 | summary: 'string', 102 | }, 103 | }, 104 | options: [ 105 | '0', 106 | 'px', 107 | '0_5', 108 | '1', 109 | '1_5', 110 | '2', 111 | '2_5', 112 | '3', 113 | '3_5', 114 | '4', 115 | '5', 116 | '6', 117 | '7', 118 | '8', 119 | '9', 120 | '10', 121 | '11', 122 | '12', 123 | '13', 124 | '14', 125 | '15', 126 | '16', 127 | ], 128 | control: { type: 'select' }, 129 | }, 130 | rowGap: { 131 | description: 'The gap between flex rows.', 132 | type: 'string', 133 | table: { 134 | type: { 135 | summary: 'string', 136 | }, 137 | }, 138 | options: [ 139 | '0', 140 | 'px', 141 | '0_5', 142 | '1', 143 | '1_5', 144 | '2', 145 | '2_5', 146 | '3', 147 | '3_5', 148 | '4', 149 | '5', 150 | '6', 151 | '7', 152 | '8', 153 | '9', 154 | '10', 155 | '11', 156 | '12', 157 | '13', 158 | '14', 159 | '15', 160 | '16', 161 | ], 162 | control: { type: 'select' }, 163 | }, 164 | }, 165 | }; 166 | 167 | export default meta; 168 | 169 | const PreviewBox = () =>
; 170 | 171 | const flexCode = ` 172 | 173 | `; 174 | 175 | export const Story: Story = { 176 | parameters: { 177 | storySource: { 178 | source: flexCode, 179 | }, 180 | }, 181 | args: { 182 | align: 'center', 183 | justify: 'center', 184 | gap: '2', 185 | }, 186 | render(args) { 187 | return ( 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | ); 196 | }, 197 | }; 198 | -------------------------------------------------------------------------------- /figma-kit/src/components/flex/flex.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { VariantProps } from 'class-variance-authority'; 3 | import { cva } from 'class-variance-authority'; 4 | 5 | const gapVariants = { 6 | '0': 'fp-gap-0', 7 | px: 'fp-gap-px', 8 | '0.5': 'fp-gap-0_5', 9 | '1': 'fp-gap-1', 10 | '1.5': 'fp-gap-1_5', 11 | '2': 'fp-gap-2', 12 | '2.5': 'fp-gap-2_5', 13 | '3': 'fp-gap-3', 14 | '3.5': 'fp-gap-3_5', 15 | '4': 'fp-gap-4', 16 | '5': 'fp-gap-5', 17 | '6': 'fp-gap-6', 18 | '7': 'fp-gap-7', 19 | '8': 'fp-gap-8', 20 | '9': 'fp-gap-9', 21 | '10': 'fp-gap-10', 22 | '11': 'fp-gap-11', 23 | '12': 'fp-gap-12', 24 | '13': 'fp-gap-13', 25 | '14': 'fp-gap-14', 26 | '15': 'fp-gap-15', 27 | '16': 'fp-gap-16', 28 | }; 29 | 30 | const flex = cva('fp-Flex', { 31 | variants: { 32 | direction: { 33 | row: 'fp-direction-row', 34 | column: 'fp-direction-column', 35 | rowReverse: 'fp-direction-row-reverse', 36 | columnReverse: 'fp-direction-column-reverse', 37 | }, 38 | align: { 39 | start: 'fp-align-start', 40 | center: 'fp-align-center', 41 | end: 'fp-align-end', 42 | baseline: 'fp-align-baseline', 43 | stretch: 'fp-align-stretch', 44 | }, 45 | justify: { 46 | start: 'fp-justify-start', 47 | center: 'fp-justify-center', 48 | end: 'fp-justify-end', 49 | between: 'fp-justify-between', 50 | }, 51 | wrap: { 52 | nowrap: 'fp-wrap-nowrap', 53 | wrap: 'fp-wrap-wrap', 54 | wrapReverse: 'fp-wrap-wrap-reverse', 55 | }, 56 | gap: gapVariants, 57 | columnGap: gapVariants, 58 | rowGap: gapVariants, 59 | }, 60 | defaultVariants: { 61 | direction: 'row', 62 | }, 63 | }); 64 | 65 | type FlexElement = React.ElementRef<'div'>; 66 | type FlexProps = React.ComponentPropsWithoutRef<'div'> & VariantProps; 67 | 68 | const Flex = React.forwardRef((props, ref) => { 69 | const { className, direction, align, justify, wrap, gap, columnGap, rowGap, ...flexProps } = props; 70 | 71 | return ( 72 |
77 | ); 78 | }); 79 | 80 | Flex.displayName = 'Flex'; 81 | 82 | export type { FlexProps }; 83 | export { Flex }; 84 | -------------------------------------------------------------------------------- /figma-kit/src/components/flex/index.ts: -------------------------------------------------------------------------------- 1 | export * from './flex'; 2 | -------------------------------------------------------------------------------- /figma-kit/src/components/icon-button/icon-button.css: -------------------------------------------------------------------------------- 1 | .fp-IconButton { 2 | box-sizing: border-box; 3 | background-clip: border-box; 4 | background-color: transparent; 5 | user-select: none; 6 | appearance: none; 7 | border: 0; 8 | padding: 0; 9 | cursor: default; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex-shrink: 0; 14 | --color-icon: var(--figma-color-icon); 15 | border-radius: var(--radius-medium); 16 | 17 | &:where(:hover:not(:disabled)) { 18 | background-color: var(--figma-color-bg-pressed); 19 | } 20 | 21 | &:focus-visible { 22 | outline-offset: -1px; 23 | outline: 1px solid var(--figma-color-border-selected); 24 | } 25 | 26 | &:disabled { 27 | --color-icon: var(--figma-color-icon-disabled); 28 | } 29 | } 30 | 31 | .fp-IconButton:where(.fp-size-small) { 32 | width: var(--space-6); 33 | height: var(--space-6); 34 | } 35 | 36 | .fp-IconButton:where(.fp-size-medium) { 37 | width: var(--space-8); 38 | height: var(--space-8); 39 | } 40 | 41 | .fp-IconButton:where(.fp-active-appearance-subtle) { 42 | &[data-state='open'] { 43 | background-color: var(--figma-color-bg-selected); 44 | --color-icon: var(--figma-color-icon-brand); 45 | } 46 | } 47 | 48 | .fp-IconButton:where(.fp-active-appearance-solid) { 49 | &[data-state='open'] { 50 | background-color: var(--figma-color-bg-selected-strong); 51 | --color-icon: var(--figma-color-icon-onbrand); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /figma-kit/src/components/icon-button/icon-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import React from 'react'; 3 | import { TooltipProvider } from '@components/tooltip'; 4 | import { PlusIcon, StylesIcon } from '@components/icons'; 5 | import * as Popover from '@components/popover'; 6 | import { Text } from '@components/text'; 7 | import { IconButton } from './icon-button'; 8 | 9 | type Story = StoryObj; 10 | 11 | const meta: Meta = { 12 | component: IconButton, 13 | title: 'Components/Icon Button', 14 | parameters: { controls: { expanded: true } }, 15 | argTypes: { 16 | 'aria-label': { 17 | description: 'Text for screen readers, also used as tooltip text by default.', 18 | type: { 19 | name: 'string', 20 | required: true, 21 | }, 22 | }, 23 | size: { 24 | description: 'Size of the button.', 25 | table: { 26 | type: { 27 | summary: 'enum', 28 | }, 29 | defaultValue: { 30 | summary: 'small', 31 | }, 32 | }, 33 | options: ['small', 'medium'], 34 | control: { type: 'radio' }, 35 | }, 36 | activeAppearance: { 37 | description: 'Appearance of the button when in active/pressed mode', 38 | table: { 39 | type: { 40 | summary: 'enum', 41 | }, 42 | defaultValue: { 43 | summary: 'subtle', 44 | }, 45 | }, 46 | options: ['subtle', 'solid'], 47 | control: { type: 'radio' }, 48 | }, 49 | disabled: { 50 | type: 'boolean', 51 | }, 52 | tooltipContent: { 53 | description: 'Custom content for the tooltip. Defaults to aria-label if not specified.', 54 | type: 'string', 55 | }, 56 | disableTooltip: { 57 | description: "Disables the tooltip. Use sparingly when the button's function is clear.", 58 | type: 'boolean', 59 | }, 60 | }, 61 | decorators: [ 62 | (Story) => ( 63 | 64 | 65 | 66 | ), 67 | ], 68 | }; 69 | 70 | export default meta; 71 | 72 | const code = ` 73 | 74 | 75 | 76 | `; 77 | 78 | export const Default: Story = { 79 | parameters: { 80 | storySource: { 81 | source: code, 82 | }, 83 | }, 84 | render: (args) => { 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Popover 95 | 96 | 97 | 98 | 99 | 100 | A sample popover for demonstrating icon button active state. 101 | 102 | 103 | 104 | ); 105 | }, 106 | args: { 107 | 'aria-label': 'Open popover', 108 | activeAppearance: 'subtle', 109 | size: 'small', 110 | children: , 111 | }, 112 | }; 113 | -------------------------------------------------------------------------------- /figma-kit/src/components/icon-button/icon-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { VariantProps } from 'class-variance-authority'; 3 | import { cva } from 'class-variance-authority'; 4 | import { Tooltip } from '@components/tooltip'; 5 | 6 | const iconButton = cva('fp-IconButton', { 7 | variants: { 8 | size: { 9 | small: 'fp-size-small', 10 | medium: 'fp-size-medium', 11 | }, 12 | activeAppearance: { 13 | subtle: 'fp-active-appearance-subtle', 14 | solid: 'fp-active-appearance-solid', 15 | }, 16 | }, 17 | defaultVariants: { 18 | size: 'small', 19 | activeAppearance: 'subtle', 20 | }, 21 | }); 22 | 23 | type IconButtonElement = React.ElementRef<'button'>; 24 | type IconButtonProps = React.ComponentPropsWithoutRef<'button'> & 25 | VariantProps & { 26 | 'aria-label': string; 27 | tooltipContent?: React.ReactNode; 28 | disableTooltip?: boolean; 29 | }; 30 | 31 | const IconButton = React.forwardRef((props, ref) => { 32 | const { 33 | className, 34 | size, 35 | activeAppearance, 36 | 'aria-label': ariaLabel, 37 | tooltipContent, 38 | disableTooltip, 39 | ...iconButtonProps 40 | } = props; 41 | const buttonElement = ( 42 |