├── .babelrc.json ├── .eslintrc.js ├── .github └── workflows │ └── deploy-github-pages.yaml ├── .gitignore ├── .husky └── pre-commit ├── .storybook ├── YourTheme.js ├── comp-icon.svg ├── docs-container.js ├── main.js ├── manager-head.html ├── manager.js ├── preview-head.html ├── preview.js ├── sb-logo.png └── theme.js ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── fui-badge └── package.json ├── fui-button └── package.json ├── fui-checkbox └── package.json ├── fui-empty └── package.json ├── fui-modal └── package.json ├── fui-notification └── package.json ├── fui-option-group └── package.json ├── fui-popover └── package.json ├── fui-radio └── package.json ├── fui-select └── package.json ├── fui-status-message └── package.json ├── fui-stepper └── package.json ├── fui-switch └── package.json ├── fui-text-input └── package.json ├── fui-toggle └── package.json ├── fui-tooltip └── package.json ├── package.json ├── src ├── components │ ├── fui-badge │ │ ├── fui-badge.css │ │ ├── fui-badge.stories.tsx │ │ └── fui-badge.tsx │ ├── fui-button │ │ ├── fui-button.css │ │ ├── fui-button.stories.tsx │ │ └── fui-button.tsx │ ├── fui-checkbox │ │ ├── fui-checkbox.css │ │ ├── fui-checkbox.stories.tsx │ │ └── fui-checkbox.tsx │ ├── fui-empty │ │ ├── fui-empty.css │ │ ├── fui-empty.stories.tsx │ │ └── fui-empty.tsx │ ├── fui-modal │ │ ├── fui-modal.css │ │ ├── fui-modal.stories.tsx │ │ └── fui-modal.tsx │ ├── fui-notification │ │ ├── fui-notification.css │ │ ├── fui-notification.stories.tsx │ │ └── fui-notification.tsx │ ├── fui-option-group │ │ ├── fui-option-group.css │ │ ├── fui-option-group.stories.tsx │ │ └── fui-option-group.tsx │ ├── fui-popover │ │ ├── fui-popover.css │ │ ├── fui-popover.stories.tsx │ │ ├── fui-popover.tsx │ │ └── popover.tsx │ ├── fui-radio │ │ ├── fui-radio.css │ │ ├── fui-radio.stories.tsx │ │ └── fui-radio.tsx │ ├── fui-select │ │ ├── fui-select.css │ │ ├── fui-select.stories.tsx │ │ └── fui-select.tsx │ ├── fui-status-message │ │ ├── fui-status-message.css │ │ ├── fui-status-message.stories.tsx │ │ └── fui-status-message.tsx │ ├── fui-stepper │ │ ├── fui-stepper.css │ │ ├── fui-stepper.stories.tsx │ │ └── fui-stepper.tsx │ ├── fui-switch │ │ ├── fui-switch.css │ │ ├── fui-switch.stories.tsx │ │ └── fui-switch.tsx │ ├── fui-text-input │ │ ├── fui-text-input.css │ │ ├── fui-text-input.stories.tsx │ │ └── fui-text-input.tsx │ ├── fui-toggle │ │ ├── fui-toggle.css │ │ ├── fui-toggle.stories.tsx │ │ └── fui-toggle.tsx │ ├── fui-tooltip │ │ ├── fui-tooltip.css │ │ ├── fui-tooltip.stories.tsx │ │ └── fui-tooltip.tsx │ └── prefix.ts ├── css │ ├── disableable-colors.css │ ├── effects.css │ ├── global-colors.css │ ├── interactable.css │ ├── layout.css │ ├── main.css │ ├── shape.css │ ├── text-styles.css │ └── theme-colors.css ├── icons │ ├── fui-icon-check-12x12.tsx │ ├── fui-icon-check-16x16.tsx │ ├── fui-icon-check-8x8.tsx │ ├── fui-icon-chevron-down-12x12.tsx │ ├── fui-icon-exclamation-mark-16x16.tsx │ ├── fui-icon-exclamation-mark-8x8.tsx │ ├── fui-icon-indeterminate-line-2x10.tsx │ ├── fui-icon-minus-12x12.tsx │ ├── fui-icon-minus-8x8.tsx │ ├── fui-icon-placeholder-16x16.tsx │ ├── fui-icon-placeholder-32x32.tsx │ ├── fui-icon-plus-12x12.tsx │ ├── fui-icon-plus-8x8.tsx │ ├── fui-icon-x-12x12.tsx │ ├── fui-icon-x-16x16.tsx │ ├── fui-icon-x-8x8.tsx │ └── icons.stories.tsx ├── illustrations │ ├── fui-illustration-cat.tsx │ └── illustrations.stories.tsx └── stories │ ├── Introduction.mdx │ ├── setup.mdx │ └── theming.mdx ├── style └── package.json ├── tsconfig.json └── vite.config.ts /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: ['plugin:react/recommended', 'standard-with-typescript', 'plugin:storybook/recommended'], 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | project: './tsconfig.json', 12 | }, 13 | plugins: ['react'], 14 | settings: { 15 | react: { 16 | version: 'detect' 17 | } 18 | }, 19 | rules: { 20 | "@typescript-eslint/semi": ["error", "always"], 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/strict-boolean-expressions": "off", 23 | } 24 | }; -------------------------------------------------------------------------------- /.github/workflows/deploy-github-pages.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "main" 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - id: build-publish 16 | uses: bitovi/github-actions-storybook-to-github-pages@v1.0.2 17 | with: 18 | install_command: npm install 19 | path: storybook-static -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | storybook-static 4 | dist 5 | build-storybook.log -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run precommit 5 | -------------------------------------------------------------------------------- /.storybook/YourTheme.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming/create'; 2 | import logoUrl from './sb-logo.png'; 3 | 4 | export default create({ 5 | base: 'light', 6 | brandTitle: 'Functional UI Kit', 7 | brandUrl: 'https://functional-ui-kit.com/', 8 | brandImage: logoUrl, 9 | brandTarget: '_blank', 10 | fontBase: '"Inter", sans-serif', 11 | fontCode: '"IBM Plex Mono", monospace', 12 | colorPrimary: "#2856E0", 13 | colorSecondary: "#2856E0", 14 | appBg: "#E9ECF5", 15 | appContentBg: "#E9ECF5", 16 | appBorderColor: "#BDBFC7", 17 | appBorderRadius: 4, 18 | textColor: "#171719", 19 | textInverseColor: "#F5F6F9", 20 | textMutedColor: "#47484D", 21 | barTextColor: "#171719", 22 | barSelectedColor: "rgba(53, 84, 206, 0.1)", 23 | barBg: "#F0F4FF", 24 | buttonBg: "#2856E0", 25 | booleanBg: "#F0F4FF", 26 | booleanSelectedBg: "#2856E0", 27 | inputBg: "#F5F6F9", 28 | inputBorder: "#BDBFC7", 29 | inputTextColor: "#171719", 30 | inputBorderRadius: 4, 31 | }); 32 | -------------------------------------------------------------------------------- /.storybook/comp-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.storybook/docs-container.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DocsContainer as BaseContainer } from "@storybook/addon-docs/blocks"; 3 | import { useDarkMode } from "storybook-dark-mode"; 4 | import { themes } from "@storybook/theming"; 5 | 6 | export const DocsContainer = ({ children, ...rest }) => { 7 | const dark = useDarkMode(); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 3 | addons: ["@storybook/manager-api, @storybook/theming, @storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/addon-mdx-gfm", '@storybook/addon-toolbars', '@storybook/addon-designs', 'storybook-dark-mode'], 4 | framework: { 5 | name: "@storybook/react-webpack5", 6 | options: {} 7 | }, 8 | docs: { 9 | autodocs: "tag" 10 | } 11 | }; 12 | export default config; -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | import yourTheme from './YourTheme'; 3 | 4 | addons.setConfig({ 5 | theme: yourTheme 6 | }); -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 69 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/css/main.css'; 2 | import { themes } from '@storybook/theming'; 3 | import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; 4 | import React from 'react'; 5 | import { addons } from '@storybook/preview-api'; 6 | import { DocsContainer } from './docs-container'; 7 | 8 | const channel = addons.getChannel(); 9 | 10 | const withThemeProvider = (Story) => { 11 | const [isDark, setDark] = React.useState(false); 12 | 13 | React.useEffect(() => { 14 | channel.on(DARK_MODE_EVENT_NAME, setDark); 15 | return () => channel.off(DARK_MODE_EVENT_NAME, setDark); 16 | }, [channel, setDark]); 17 | 18 | document.body.setAttribute('data-theme', isDark ? 'dark' : 'light'); 19 | return Story(); 20 | }; 21 | 22 | const preview = { 23 | decorators: [withThemeProvider], 24 | parameters: { 25 | backgrounds: { disable: true }, 26 | actions: { argTypesRegex: "^on[A-Z].*" }, 27 | controls: { 28 | matchers: { 29 | color: /(background|color)$/i, 30 | date: /Date$/, 31 | }, 32 | }, 33 | docs: { 34 | container: DocsContainer, 35 | }, 36 | }, 37 | }; 38 | 39 | export default preview; 40 | -------------------------------------------------------------------------------- /.storybook/sb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functional-ui/functional-ui-kit/811febafbe9c5649a0ed55cbce558c7480f6ca65/.storybook/sb-logo.png -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | // .storybook/YourTheme.js 2 | 3 | import { create } from '@storybook/theming/create'; 4 | 5 | const getCssVarStringValue = (varName) => getComputedStyle(document.documentElement).getPropertyValue(varName); 6 | 7 | export default create({ 8 | base: 'light', 9 | // Typography 10 | fontBase: '"Inter", sans-serif', 11 | fontCode: '"IBM Plex Mono", monospace', 12 | 13 | brandTitle: 'Functional UI Kit', 14 | brandUrl: 'https://functional-ui-kit.com/', 15 | brandImage: 'https://framerusercontent.com/images/tVkfz6i8rL4iFXxNlvKJhZuM.png', 16 | brandTarget: '_self', 17 | 18 | // 19 | colorPrimary: 'red', 20 | colorSecondary: '#585C6D', 21 | 22 | // UI 23 | appBg: '#ddd', 24 | appContentBg: '#ffffff', 25 | appBorderColor: getCssVarStringValue('--fui-color-divider-soft'), 26 | appBorderRadius: getCssVarStringValue('--fui-border-radius-xlg'), 27 | 28 | // Text colors 29 | textColor: '#10162F', 30 | textInverseColor: '#ffffff', 31 | 32 | // Toolbar default and active colors 33 | barTextColor: '#9E9E9E', 34 | barSelectedColor: '#585C6D', 35 | barBg: '#ffffff', 36 | 37 | // Form colors 38 | inputBg: '#ffffff', 39 | inputBorder: getCssVarStringValue('--fui-color-divider-solid'), 40 | inputTextColor: '#10162F', 41 | inputBorderRadius: getCssVarStringValue('--fui-border-radius-md'), 42 | colorPrimary: getCssVarStringValue('--fui-color-brand'), 43 | }); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.3] - 2024-06-13 9 | * [Infra] - Fixed types exporting 10 | 11 | ## [1.0.2] - 2024-06-13 12 | * [FuiOptionGroup] - Fixed component import 13 | 14 | ## [1.0.1] - 2024-04-05 15 | * [Infra] - Fixed issues with importing in `create-react-app` projects 16 | 17 | ## [1.0.0] - 2024-03-10 18 | * [FuiCheckbox] - Fixed naming ([#8](https://github.com/functional-ui/functional-ui-kit/issues/8)) 19 | * [FuiTextInput / FuiRadio / FuiStepper] - Improved adherence to WCAG ([#7](https://github.com/functional-ui/functional-ui-kit/issues/7)) 20 | * [Docs] - Upgrade to Storybook@7.6.17 21 | 22 | ## [0.1.0] - 2024-02-07 23 | * [FuiPopover] - Removed `react-tiny-popover` and replace with `floating-ui` 24 | * [FuiOptionGroup] - New Component 🎉 25 | * [Docs] - Upgrade to Storybook@7.6.10 26 | 27 | ## [0.0.2] - 2023-12-21 28 | Initial Release 🎉 -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * almog4130@gmail.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 functional-ui 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 | functional-ui-kit-cover 2 | 3 | 9 | 10 | # Welcome to Functional UI Kit 11 | 12 | Functional UI Kit is a professionally crafted UI Kit for design & development teams, and individuals. We provide core components you would need in every project, focusing on accessibility, development experience and unified designer-developer experience. 13 | 14 | We've made sure that Figma variables and CSS variables work together effortlessly. They share the same names, usage and inheritance structure. This isn't just an extra feature, **it's the core approach.** 15 | 16 | Each Figma variable has a direct counterpart in CSS, so there's no confusion. The design stays crystal clear as you move into the development phase. 17 | 18 | 25 | 26 | 27 | # Setup 28 | ### Install 29 | Install the latest version from NPM. 30 | ``` 31 | npm install functional-ui-kit 32 | ``` 33 | 34 | ### Setup CSS 35 | Import the functional-ui-kit CSS file into your project in your css file 36 | ```css 37 | @import 'functional-ui-kit/style'; 38 | 39 | html { 40 | ... 41 | ``` 42 | you can also import the CSS file directly into your main React App file 43 | ```js 44 | import React, { Component } from 'react' 45 | import 'functional-ui-kit/style'; 46 | 47 | class App extends Component { 48 | ... 49 | ``` 50 | 51 | ### Customizing Theme 52 | You can customize the theme by overriding the CSS variables. You can find the list of variables in the [theme file](https:\/\/github.com/functional-ui/functional-ui-kit/blob/main/src/css/theme-colors.css) 53 | ```css 54 | :root { 55 | --fui-color-brand: var(--fui-color-green-700); 56 | } 57 | 58 | [data-theme="dark"], 59 | [data-theme="dark"] * { 60 | --fui-color-brand: var(--fui-color-green-500); 61 | } 62 | ... 63 | ``` 64 | 65 | ### Using Components 66 | You can use the components by importing them into your React App file 67 | ```js 68 | import React, { Component } from 'react' 69 | import { FuiBadge } from 'functional-ui-kit/fui-badge'; 70 | 71 | function App() { 72 | return ( 73 |
74 | 75 |
76 | ); 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /fui-badge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-badge/index.js", 3 | "types": "../dist/fui-badge/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-button/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-button/index.js", 3 | "types": "../dist/fui-button/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-checkbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-checkbox/index.js", 3 | "types": "../dist/fui-checkbox/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-empty/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-empty/index.js", 3 | "types": "../dist/fui-empty/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-modal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-modal/index.js", 3 | "types": "../dist/fui-modal/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-notification/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-notification/index.js", 3 | "types": "../dist/fui-notification/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-option-group/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-option-group/index.js", 3 | "types": "../dist/fui-option-group/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-popover/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-popover/index.js", 3 | "types": "../dist/fui-popover/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-radio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-radio/index.js", 3 | "types": "../dist/fui-radio/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-select/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-select/index.js", 3 | "types": "../dist/fui-select/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-status-message/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-status-message/index.js", 3 | "types": "../dist/fui-status-message/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-stepper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-stepper/index.js", 3 | "types": "../dist/fui-stepper/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-switch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-switch/index.js", 3 | "types": "../dist/fui-switch/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-text-input/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-text-input/index.js", 3 | "types": "../dist/fui-text-input/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-toggle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-toggle/index.js", 3 | "types": "../dist/fui-toggle/index.d.ts" 4 | } -------------------------------------------------------------------------------- /fui-tooltip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/fui-tooltip/index.js", 3 | "types": "../dist/fui-tooltip/index.d.ts" 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functional-ui-kit", 3 | "version": "1.0.3", 4 | "description": "Functional UI Kit", 5 | "homepage": "https://functional-ui-kit.com/", 6 | "scripts": { 7 | "build": "vite build --config vite.config.ts", 8 | "dev": "npm run storybook", 9 | "lint": "eslint ./src --fix", 10 | "precommit": "lint-staged", 11 | "prepare": "husky install", 12 | "storybook": "storybook dev -p 6006", 13 | "storybook:build": "storybook build", 14 | "build-storybook": "npm run storybook:build", 15 | "storybook:deploy": "npx chromatic --exit-once-uploaded --project-token=chpt_74ebca5d2886a87" 16 | }, 17 | "files": [ 18 | "dist", 19 | "fui-badge", 20 | "fui-button", 21 | "fui-popover", 22 | "fui-checkbox", 23 | "fui-empty", 24 | "fui-modal", 25 | "fui-notification", 26 | "fui-radio", 27 | "fui-select", 28 | "fui-switch", 29 | "fui-status-message", 30 | "fui-option-group", 31 | "fui-stepper", 32 | "fui-text-input", 33 | "fui-toggle", 34 | "fui-tooltip", 35 | "style" 36 | ], 37 | "author": "Alex Yakir", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/functional-ui/functional-ui-kit.git" 41 | }, 42 | "license": "ISC", 43 | "dependencies": { 44 | "@floating-ui/react": "^0.25.2", 45 | "classnames": "^2.3.2", 46 | "react-modal": "^3.16.1" 47 | }, 48 | "devDependencies": { 49 | "@babel/preset-env": "^7.21.5", 50 | "@babel/preset-react": "^7.18.6", 51 | "@babel/preset-typescript": "^7.21.5", 52 | "@storybook/addon-designs": "^7.0.7", 53 | "@storybook/addon-essentials": "^7.6.17", 54 | "@storybook/addon-interactions": "^7.6.17", 55 | "@storybook/addon-links": "^7.6.17", 56 | "@storybook/addon-mdx-gfm": "^7.6.17", 57 | "@storybook/addon-toolbars": "^7.6.17", 58 | "@storybook/blocks": "^7.6.17", 59 | "@storybook/manager-api": "^7.6.17", 60 | "@storybook/react": "^7.6.17", 61 | "@storybook/react-webpack5": "^7.6.17", 62 | "@storybook/testing-library": "^0.2.2", 63 | "@storybook/theming": "^7.6.17", 64 | "@types/react": "^18.0.0", 65 | "@types/react-modal": "^3.16.0", 66 | "@typescript-eslint/eslint-plugin": "^5.59.5", 67 | "chromatic": "^6.19.9", 68 | "eslint": "^8.40.0", 69 | "eslint-config-standard-with-typescript": "^34.0.1", 70 | "eslint-plugin-import": "^2.27.5", 71 | "eslint-plugin-n": "^15.7.0", 72 | "eslint-plugin-promise": "^6.1.1", 73 | "eslint-plugin-react": "^7.32.2", 74 | "eslint-plugin-storybook": "^0.6.15", 75 | "husky": "^8.0.3", 76 | "lint-staged": "^13.2.3", 77 | "prop-types": "^15.8.1", 78 | "react": "^18.2.0", 79 | "react-dom": "^18.2.0", 80 | "storybook": "^7.6.17", 81 | "storybook-dark-mode": "^3.0.3", 82 | "typescript": "^5.0.4", 83 | "vite": "^4.4.4", 84 | "vite-plugin-dts": "^3.3.1" 85 | }, 86 | "peerDependencies": { 87 | "react": "^18.0.0", 88 | "react-dom": "^18.0.0" 89 | }, 90 | "lint-staged": { 91 | "src/**/*.ts": "eslint --fix" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/fui-badge/fui-badge.css: -------------------------------------------------------------------------------- 1 | .fui-badge { 2 | display: inline-flex; 3 | height: var(--fui-control-height-sm); 4 | min-height: var(--fui-control-height-sm); 5 | max-height: var(--fui-control-height-sm); 6 | padding: 0px var(--fui-space-sm); 7 | justify-content: center; 8 | align-items: center; 9 | gap: var(--fui-space-xs); 10 | flex-shrink: 0; 11 | border-radius: var(--fui-radius-round); 12 | border: 1px solid; 13 | font: var(--running-small-bold); 14 | } 15 | 16 | .fui-badge [class*="fui-icon"] { 17 | color: inherit; 18 | } 19 | 20 | .fui-badge-color-neutral { 21 | border-color: var(--fui-color-badge-neutral-border); 22 | color: var(--fui-color-badge-neutral-text); 23 | background: var(--fui-color-badge-neutral-bg); 24 | } 25 | 26 | .fui-badge-color-blue { 27 | border-color: var(--fui-color-badge-blue-border); 28 | color: var(--fui-color-badge-blue-text); 29 | background: var(--fui-color-badge-blue-bg); 30 | } 31 | 32 | .fui-badge-color-red { 33 | border-color: var(--fui-color-badge-red-border); 34 | color: var(--fui-color-badge-red-text); 35 | background: var(--fui-color-badge-red-bg); 36 | } 37 | 38 | .fui-badge-color-yellow { 39 | border-color: var(--fui-color-badge-yellow-border); 40 | color: var(--fui-color-badge-yellow-text); 41 | background: var(--fui-color-badge-yellow-bg); 42 | } 43 | 44 | .fui-badge-color-green { 45 | border-color: var(--fui-color-badge-green-border); 46 | color: var(--fui-color-badge-green-text); 47 | background: var(--fui-color-badge-green-bg); 48 | } 49 | 50 | .fui-badge-color-purple { 51 | border-color: var(--fui-color-badge-purple-border); 52 | color: var(--fui-color-badge-purple-text); 53 | background: var(--fui-color-badge-purple-bg); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/fui-badge/fui-badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiBadge, type FuiBadgeProps } from './fui-badge'; 5 | import FuiIconPlaceholder16X16 from '../../icons/fui-icon-placeholder-16x16'; 6 | 7 | const meta = { 8 | title: ' Components/Badge', 9 | component: FuiBadge as React.FC, 10 | tags: ['autodocs'], 11 | parameters: { 12 | design: { 13 | type: 'figma', 14 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2461-2893' 15 | }, 16 | docs: { 17 | description: { 18 | component: 'Assist people to quickly grasp information, status & classification. Provide feedback, and establish trust and security.' 19 | } 20 | } 21 | }, 22 | argTypes: { 23 | color: { name: '🔗 color' }, 24 | icon: { 25 | control: 'boolean', 26 | name: '🔗 icon', 27 | mapping: { 28 | false: undefined, 29 | true: 30 | } 31 | }, 32 | iconRight: { 33 | control: 'boolean', 34 | name: '🔗 iconRight', 35 | mapping: { 36 | false: undefined, 37 | true: 38 | } 39 | }, 40 | label: { name: '🔗 label' }, 41 | className: { 42 | control: { 43 | disable: true 44 | } 45 | }, 46 | ariaLabel: { 47 | control: { 48 | disable: true 49 | } 50 | } 51 | } 52 | } satisfies Meta; 53 | 54 | export default meta; 55 | type Story = StoryObj; 56 | 57 | export const Default: Story = { 58 | args: { 59 | label: 'Default', 60 | color: 'blue' 61 | }, 62 | name: 'Basic' 63 | }; 64 | 65 | export const Colors: Story = { 66 | args: { 67 | label: 'Badge', 68 | color: 'blue' 69 | }, 70 | render: (args) => ( 71 |
72 | {['blue', 'green', 'red', 'purple', 'yellow', 'neutral'].map((color) => ( 73 | 74 | ))} 75 |
76 | ), 77 | name: 'Badge Color', 78 | parameters: { 79 | docs: { 80 | description: { 81 | story: 'Pass the `color` props to customize your badge.' 82 | } 83 | }, 84 | design: { 85 | type: 'figma', 86 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2461-2893' 87 | } 88 | }, 89 | argTypes: { 90 | color: { 91 | table: { 92 | disable: true 93 | } 94 | } 95 | } 96 | }; 97 | 98 | export const Icon: Story = { 99 | args: { 100 | label: 'Badge', 101 | color: 'blue' 102 | }, 103 | render: (args) => ( 104 |
105 | } /> 106 | } /> 107 | } iconRight={} /> 108 |
109 | ), 110 | name: 'Icon & Icon Right', 111 | parameters: { 112 | docs: { 113 | description: { 114 | story: 'Pass the `icon` or `iconRight` props to customize your badge.' 115 | } 116 | }, 117 | design: { 118 | type: 'figma', 119 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2461-2908' 120 | } 121 | }, 122 | argTypes: { 123 | icon: { 124 | table: { 125 | disable: true 126 | } 127 | }, 128 | iconRight: { 129 | table: { 130 | disable: true 131 | } 132 | } 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /src/components/fui-badge/fui-badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import classnames from 'classnames'; 4 | 5 | const compPrefix = `${prefix}-badge`; 6 | 7 | export type FuiBadgeColor = 'blue' | 'green' | 'red' | 'purple' | 'yellow' | 'neutral'; 8 | 9 | export interface FuiBadgeProps { 10 | color: FuiBadgeColor 11 | icon?: JSX.Element 12 | iconRight?: JSX.Element 13 | label: string 14 | 15 | ariaLabel?: string 16 | className?: string 17 | } 18 | 19 | export const FuiBadge = (props: FuiBadgeProps) => { 20 | const classNames = classnames( 21 | compPrefix, 22 | `${compPrefix}-color-${props.color}`, 23 | props.className 24 | ); 25 | const icon = React.useMemo(() =>
{props.icon}
, [props.icon]); 26 | const iconRight = React.useMemo(() =>
{props.iconRight}
, [props.iconRight]); 27 | const badgeLabel = React.useMemo(() =>
{props.label}
, [props.label]); 28 | 29 | return ( 30 |
31 | {props.icon && icon} 32 | {props.label} 33 | {props.iconRight && iconRight} 34 |
35 | ); 36 | }; 37 | 38 | FuiBadge.defaultProps = { 39 | color: 'blue' 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/fui-button/fui-button.css: -------------------------------------------------------------------------------- 1 | .fui-button { 2 | display: flex; 3 | align-items: center; 4 | gap: var(--fui-space-md); 5 | position: relative; 6 | border-radius: var(--fui-border-radius-md); 7 | color: inherit; 8 | } 9 | 10 | .fui-button [class*="fui-icon"] { 11 | color: inherit; 12 | } 13 | 14 | .fui-button-label-container { 15 | width: 100%; 16 | } 17 | 18 | .fui-button--size-small .fui-button-label-container { 19 | padding: 0 var(--fui-space-xxs); 20 | } 21 | 22 | .fui-button--size-medium .fui-button-label-container { 23 | padding: 0 var(--fui-space-xs); 24 | } 25 | 26 | .fui-button--size-large .fui-button-label-container { 27 | padding: 0 var(--fui-space-sm); 28 | } 29 | 30 | .fui-button:after { 31 | border-radius: var(--fui-border-radius-md); 32 | } 33 | 34 | .fui-button.fui-interactable:focus-visible { 35 | outline-offset: 0em; 36 | } 37 | 38 | .fui-button--size-large { 39 | height: var(--fui-control-height-lg); 40 | min-width: var(--fui-control-height-lg); 41 | padding: 0 var(--fui-space-xlg); 42 | font: var(--running-medium-bold); 43 | } 44 | 45 | .fui-button--size-medium { 46 | padding: 0; 47 | font: var(--running-medium-bold); 48 | } 49 | 50 | .fui-button--size-medium:not(.fui-button--hierarchy-tertiary) { 51 | height: var(--fui-control-height-md); 52 | min-width: var(--fui-control-height-md); 53 | padding: 0 var(--fui-space-lg); 54 | } 55 | 56 | .fui-button--size-small { 57 | height: var(--fui-control-height-sm); 58 | min-width: var(--fui-control-height-sm); 59 | padding: 0 var(--fui-space-md); 60 | font: var(--running-small-bold); 61 | gap: var(--fui-space-xs); 62 | } 63 | 64 | .fui-button--hierarchy-primary { 65 | color: var(--fui-disableable-color-foreground-light); 66 | border: none; 67 | } 68 | 69 | .fui-button--action-neutral.fui-button--hierarchy-primary { 70 | background: var(--fui-disableable-color-button-primary-bg-neutral); 71 | } 72 | 73 | .fui-button--action-success.fui-button--hierarchy-primary { 74 | background-color: var(--fui-disableable-color-button-primary-bg-success); 75 | } 76 | 77 | .fui-button--action-destructive.fui-button--hierarchy-primary { 78 | background-color: var(--fui-disableable-color-button-primary-bg-destructive); 79 | } 80 | 81 | .fui-button--hierarchy-secondary { 82 | background-color: transparent; 83 | border: 1px solid; 84 | } 85 | 86 | .fui-button--action-neutral.fui-button--hierarchy-secondary { 87 | border-color: var(--fui-disableable-color-button-secondary-border-neutral); 88 | color: var(--fui-disableable-color-foreground-primary); 89 | background-color: var(--fui-disableable-color-button-secondary-bg-neutral); 90 | } 91 | 92 | .fui-button--action-success.fui-button--hierarchy-secondary { 93 | border-color: var(--fui-disableable-color-button-secondary-border-success); 94 | color: var(--fui-disableable-color-foreground-success); 95 | background-color: var(--fui-disableable-color-button-secondary-bg-success); 96 | } 97 | 98 | .fui-button--action-destructive.fui-button--hierarchy-secondary { 99 | border-color: var(--fui-disableable-color-button-secondary-border-destructive); 100 | color: var(--fui-disableable-color-foreground-danger); 101 | background-color: var(--fui-disableable-color-button-secondary-bg-destructive); 102 | } 103 | 104 | .fui-button--hierarchy-tertiary { 105 | border: none; 106 | background-color: transparent; 107 | padding: 0 var(--fui-space-md); 108 | } 109 | 110 | .fui-button--action-neutral.fui-button--hierarchy-tertiary, 111 | .fui-button--action-neutral.fui-button--hierarchy-tertiary [class*="fui-icon"] { 112 | color: var(--fui-disableable-color-foreground-primary); 113 | } 114 | 115 | .fui-button--action-success.fui-button--hierarchy-tertiary, 116 | .fui-button--action-success.fui-button--hierarchy-tertiary [class*="fui-icon"] { 117 | color: var(--fui-disableable-color-foreground-success); 118 | } 119 | 120 | .fui-button--action-destructive.fui-button--hierarchy-tertiary, 121 | .fui-button--action-destructive.fui-button--hierarchy-tertiary [class*="fui-icon"] { 122 | color: var(--fui-disableable-color-foreground-danger); 123 | } 124 | 125 | .fui-button--hierarchy-tertiary .fui-button__loader { 126 | top: 50%; 127 | right: -26px; 128 | transform: translateY(-50%); 129 | } 130 | 131 | .fui-button__loader { 132 | height: 20px; 133 | width: 20px; 134 | background-color: white; 135 | position: absolute; 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | top: -6px; 140 | right: -6px; 141 | box-shadow: 0px 5px 2px rgba(0, 0, 0, 0.01), 0px 3px 2px rgba(0, 0, 0, 0.05), 0px 1px 1px rgba(0, 0, 0, 0.09); 142 | border-radius: 50%; 143 | } 144 | 145 | .fui-button__loader::before { 146 | content: ''; 147 | width: 12px; 148 | height: 12px; 149 | border-radius: 50%; 150 | background: conic-gradient(var(--fui-color-brand) 0deg, white 360deg); 151 | animation: rotation 400ms infinite linear; 152 | } 153 | 154 | .fui-button__loader::after { 155 | content: ''; 156 | position: absolute; 157 | width: 8px; 158 | height: 8px; 159 | background-color: white; 160 | border-radius: 50%; 161 | } 162 | 163 | @keyframes rotation { 164 | from { 165 | transform: rotate(0deg); 166 | } 167 | 168 | to { 169 | transform: rotate(-359deg); 170 | } 171 | } -------------------------------------------------------------------------------- /src/components/fui-button/fui-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import classnames from 'classnames'; 4 | 5 | const compPrefix = `${prefix}-button`; 6 | 7 | export interface Option { 8 | label: string 9 | value: string 10 | } 11 | 12 | export interface FuiButtonProps { 13 | size: 'small' | 'medium' | 'large' 14 | hierarchy: 'primary' | 'secondary' | 'tertiary' 15 | actionType: 'neutral' | 'success' | 'destructive' 16 | disabled?: boolean 17 | label?: string 18 | icon?: JSX.Element 19 | iconRight?: JSX.Element 20 | isLoading?: boolean 21 | onClick?: () => void 22 | 23 | ariaLabel?: string 24 | className?: string 25 | } 26 | 27 | export const FuiButton = (props: FuiButtonProps) => { 28 | const classNames = classnames( 29 | `${compPrefix}`, 30 | `${compPrefix}--size-${props.hierarchy === 'tertiary' ? 'medium' : props.size}`, 31 | `${compPrefix}--hierarchy-${props.hierarchy}`, 32 | `${compPrefix}--action-${props.actionType}`, 33 | props.disabled ? `${prefix}-disabled` : `${prefix}-interactable`, 34 | props.className 35 | ); 36 | 37 | return ( 38 | 50 | ); 51 | }; 52 | 53 | FuiButton.defaultProps = { 54 | size: 'medium', 55 | hierarchy: 'primary', 56 | actionType: 'neutral' 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/fui-checkbox/fui-checkbox.css: -------------------------------------------------------------------------------- 1 | .fui-checkbox-wrapper { 2 | display: flex; 3 | align-items: center; 4 | gap: var(--fui-space-sm); 5 | position: relative; 6 | width: fit-content; 7 | } 8 | 9 | .fui-checkbox-wrapper::after { 10 | margin: -2px; 11 | border-radius: var(--fui-border-radius-sm); 12 | } 13 | 14 | .fui-checkbox-wrapper label { 15 | cursor: unset; 16 | color: var(--fui-disableable-color-foreground-default); 17 | font: var(--running-small-bold); 18 | line-height: 18px; 19 | } 20 | 21 | .fui-checkbox { 22 | height: 18px; 23 | width: 18px; 24 | border-radius: var(--fui-border-radius-sm); 25 | border: 2px solid var(--fui-disableable-color-foreground-primary); 26 | display: inline-flex; 27 | justify-content: center; 28 | align-items: center; 29 | background: var(--fui-disableable-color-background-base); 30 | color: var(--fui-color-background-white); 31 | } 32 | 33 | .fui-checkbox-wrapper.checked .fui-checkbox { 34 | background: var(--fui-disableable-color-foreground-primary); 35 | } 36 | 37 | .fui-checkbox [class*="fui-icon"] { 38 | color: var(--fui-color-background-white); 39 | } -------------------------------------------------------------------------------- /src/components/fui-checkbox/fui-checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiCheckbox } from './fui-checkbox'; 5 | 6 | const meta = { 7 | title: ' Components/Checkbox', 8 | component: FuiCheckbox, 9 | tags: ['autodocs'], 10 | parameters: { 11 | design: { 12 | type: 'figma', 13 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2461-15311' 14 | }, 15 | docs: { 16 | description: { 17 | component: 'Checkboxes allow people to select multiple options or toggle a single choice. For single selections, consider radio buttons or drop-downs. Note that checkboxes require a submission step, while switches offer real-time interaction.' 18 | } 19 | } 20 | }, 21 | argTypes: { 22 | checked: { name: '🔗 checked' }, 23 | indeterminate: { name: '🔗 indeterminate' }, 24 | disabled: { name: '🔗 disabled' }, 25 | checkLabel: { name: '🔗 checkLabel' }, 26 | className: { 27 | control: { 28 | disable: true 29 | } 30 | }, 31 | ariaLabel: { 32 | control: { 33 | disable: true 34 | } 35 | } 36 | } 37 | } satisfies Meta; 38 | 39 | export default meta; 40 | type Story = StoryObj; 41 | 42 | export const Default: Story = { 43 | render: (args) => { 44 | const [checked, setChecked] = React.useState(!!args.checked); 45 | 46 | React.useEffect(() => { 47 | setChecked(!!args.checked); 48 | }, [args.checked]); 49 | 50 | return ( 51 | 52 | ); 53 | }, 54 | args: { 55 | checkLabel: 'Label' 56 | }, 57 | name: 'Basic' 58 | }; 59 | 60 | export const Disabled: Story = { 61 | render: (args) => { 62 | return ( 63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | ); 72 | }, 73 | parameters: { 74 | design: { 75 | type: 'figma', 76 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2464-15606' 77 | } 78 | }, 79 | args: { 80 | }, 81 | name: 'Disabled Checkbox', 82 | argTypes: { 83 | disabled: { 84 | table: { 85 | disable: true 86 | } 87 | }, 88 | checked: { 89 | table: { 90 | disable: true 91 | } 92 | }, 93 | indeterminate: { 94 | table: { 95 | disable: true 96 | } 97 | } 98 | } 99 | }; 100 | 101 | export const Indeterminate: Story = { 102 | render: (args) => { 103 | const [checked1, setChecked1] = React.useState(true); 104 | const [checked2, setChecked2] = React.useState(false); 105 | 106 | const onToggleMaster = () => { 107 | if (checked1 && checked2) { 108 | setChecked1(false); 109 | setChecked2(false); 110 | } else { 111 | setChecked1(true); 112 | setChecked2(true); 113 | } 114 | }; 115 | 116 | return ( 117 | <> 118 | 119 |
120 | 121 | 122 |
123 | 124 | ); 125 | }, 126 | args: { 127 | }, 128 | parameters: { 129 | design: { 130 | type: 'figma', 131 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2461-15326' 132 | } 133 | }, 134 | name: 'Indeterminate', 135 | argTypes: { 136 | disabled: { 137 | table: { 138 | disable: true 139 | } 140 | }, 141 | checked: { 142 | table: { 143 | disable: true 144 | } 145 | }, 146 | indeterminate: { 147 | table: { 148 | disable: true 149 | } 150 | } 151 | } 152 | }; 153 | -------------------------------------------------------------------------------- /src/components/fui-checkbox/fui-checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import FuiIconCheck12X12 from '../../icons/fui-icon-check-12x12'; 4 | import classnames from 'classnames'; 5 | import FuiIconIndeterminateLine2x10 from '../../icons/fui-icon-indeterminate-line-2x10'; 6 | 7 | const compPrefix = `${prefix}-checkbox`; 8 | 9 | export interface FuiCheckboxProps { 10 | checked?: boolean 11 | indeterminate?: boolean 12 | disabled?: boolean 13 | onToggle: (isChecked: boolean) => void 14 | checkLabel?: string 15 | 16 | ariaLabel?: string 17 | className?: string 18 | } 19 | 20 | export const FuiCheckbox = (props: FuiCheckboxProps) => { 21 | const classNames = classnames( 22 | `${compPrefix}-wrapper`, 23 | props.disabled ? `${prefix}-disabled` : `${prefix}-interactable`, 24 | props.className, 25 | (props.checked || props.indeterminate) ? 'checked' : '' 26 | ); 27 | 28 | const onClick = (e: React.MouseEvent) => { 29 | e.preventDefault(); 30 | e.stopPropagation(); 31 | props.onToggle(!props.checked); 32 | }; 33 | 34 | const onSpace = (e: React.KeyboardEvent) => { 35 | if (e.key === ' ') { 36 | e.preventDefault(); 37 | props.onToggle(!props.checked); 38 | } 39 | } 40 | 41 | return ( 42 |
43 |
44 | {props.checked ? : props.indeterminate ? : null} 45 |
46 | {props.checkLabel && } 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/fui-empty/fui-empty.css: -------------------------------------------------------------------------------- 1 | .fui-empty { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding: var(--fui-space-xxlg); 6 | border: 1px dashed var(--fui-color-divider-soft); 7 | border-radius: var(--fui-border-radius-sm); 8 | text-align: center; 9 | } 10 | 11 | .fui-empty-content { 12 | display: flex; 13 | flex-direction: column; 14 | gap: var(--fui-space-xxs); 15 | } 16 | 17 | .fui-empty-title { 18 | font: var(--running-small-bold); 19 | color: var(--fui-color-foreground-default); 20 | } 21 | 22 | .fui-empty-description { 23 | font: var(--running-small-regular); 24 | color: var(--fui-color-foreground-soft); 25 | } 26 | 27 | .fui-empty-with-action .fui-empty-content { 28 | margin-bottom: var(--fui-space-lg); 29 | } 30 | 31 | .fui-empty-with-illustration .fui-empty-content { 32 | margin-top: var(--fui-space-xxlg); 33 | } -------------------------------------------------------------------------------- /src/components/fui-empty/fui-empty.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiEmpty } from './fui-empty'; 5 | import FuiIllustrationCat from '../../illustrations/fui-illustration-cat'; 6 | 7 | const meta = { 8 | title: ' Components/Empty', 9 | component: FuiEmpty, 10 | tags: ['autodocs'], 11 | parameters: { 12 | design: { 13 | type: 'figma', 14 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2464-16262' 15 | }, 16 | docs: { 17 | description: { 18 | component: 'Guide people in the absence of content, reducing confusion, encouraging actions, reinforcing branding, and offering friendly tips or tutorials to help people navigate new situations or create content when no data is available.' 19 | } 20 | } 21 | }, 22 | argTypes: { 23 | title: { 24 | name: '🔗 title' 25 | }, 26 | description: { 27 | name: '🔗 description' 28 | }, 29 | action: { 30 | name: '🔗 action', 31 | control: 'boolean', 32 | mapping: { 33 | false: undefined, 34 | true: { label: 'Try This', hierarchy: 'tertiary' } 35 | } 36 | }, 37 | illustration: { 38 | name: '🔗 illustration', 39 | control: 'boolean', 40 | mapping: { 41 | false: undefined, 42 | true: 43 | } 44 | }, 45 | className: { 46 | control: { 47 | disable: true 48 | } 49 | } 50 | } 51 | } satisfies Meta; 52 | 53 | export default meta; 54 | type Story = StoryObj; 55 | 56 | export const Default: Story = { 57 | name: 'Basic', 58 | args: { 59 | title: 'This is empty', 60 | description: 'Add content and bring this space to life!' 61 | } 62 | }; 63 | 64 | export const WithActionAndIllustration: Story = { 65 | parameters: { 66 | design: { 67 | type: 'figma', 68 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2464-16264' 69 | } 70 | }, 71 | args: { 72 | title: 'This is empty', 73 | description: 'Add content and bring this space to life!', 74 | action: { label: 'Try This', onClick: () => { console.log('Clicked empty state action'); }, hierarchy: 'tertiary', size: 'medium', actionType: 'neutral' }, 75 | illustration: 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/fui-empty/fui-empty.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import { FuiButton, type FuiButtonProps } from '../fui-button/fui-button'; 4 | import classnames from 'classnames'; 5 | 6 | const compPrefix = `${prefix}-empty`; 7 | 8 | export interface FuiEmptyProps { 9 | title: string 10 | description?: string 11 | action?: FuiButtonProps | JSX.Element 12 | illustration?: JSX.Element 13 | 14 | className?: string 15 | } 16 | 17 | export const FuiEmpty = (props: FuiEmptyProps) => { 18 | const classNames = classnames(compPrefix, props.className, { 19 | [`${compPrefix}-with-action`]: !!props.action, 20 | [`${compPrefix}-with-illustration`]: !!props.illustration 21 | }); 22 | 23 | const action = React.useMemo(() => { 24 | if (props.action) { 25 | if (React.isValidElement(props.action)) { 26 | return props.action; 27 | } else { 28 | return ; 29 | } 30 | } 31 | return null; 32 | }, [props.action]); 33 | 34 | return ( 35 |
36 | {props.illustration} 37 |
38 | {props.title} 39 |

{props.description}

40 |
41 | {action} 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/fui-modal/fui-modal.css: -------------------------------------------------------------------------------- 1 | .fui-modal-overlay { 2 | position: fixed; 3 | inset: 0px; 4 | background: var(--fui-color-background-opaque); 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .fui-modal { 11 | display: flex; 12 | min-width: 300px; 13 | width: fit-content; 14 | flex-direction: column; 15 | border-radius: var(--fui-radius-xxlg); 16 | border: 1px solid var(--fui-color-divider-soft); 17 | background: var(--fui-color-background-base); 18 | box-shadow: var(--fui-shadow-soft-elevation-2); 19 | overflow: hidden; 20 | position: relative; 21 | } 22 | 23 | .fui-modal-header { 24 | gap: var(--fui-space-lg); 25 | align-self: stretch; 26 | display: flex; 27 | min-height: 66px; 28 | padding: var(--fui-space-xxlg); 29 | font: var(--h6-bold); 30 | color: var(--fui-color-foreground-default); 31 | align-items: center; 32 | } 33 | 34 | .fui-modal-header.layout-vertical { 35 | padding: calc(var(--fui-space-xlg) + var(--fui-space-lg)) var(--fui-space-xxlg) calc(var(--fui-space-xxlg) + var(--fui-space-md)) var(--fui-space-xxlg); 36 | flex-direction: column; 37 | } 38 | 39 | .fui-modal-header-content-wrapper { 40 | display: flex; 41 | flex-direction: column; 42 | align-items: flex-start; 43 | flex: 1 0 0; 44 | } 45 | 46 | .fui-modal-header-content-wrapper.layout-vertical { 47 | align-items: center; 48 | align-self: stretch; 49 | } 50 | 51 | .fui-modal-subtitle { 52 | font: var(--running-medium-regular); 53 | color: var(--fui-color-foreground-opaque); 54 | } 55 | 56 | .fui-modal-divided .fui-modal-header { 57 | border-bottom: 1px solid var(--fui-color-divider-soft); 58 | margin-bottom: var(--fui-space-xxlg); 59 | } 60 | 61 | .fui-modal-divided .fui-modal-footer { 62 | border-top: 1px solid var(--fui-color-divider-soft); 63 | margin-top: var(--fui-space-xxlg); 64 | } 65 | 66 | .fui-modal-footer { 67 | justify-content: flex-end; 68 | gap: var(--fui-space-lg) var(--fui-space-md); 69 | align-self: stretch; 70 | flex-wrap: wrap; 71 | display: flex; 72 | padding: var(--fui-space-xxlg); 73 | align-items: center; 74 | align-content: center; 75 | } 76 | 77 | .fui-modal-footer.layout-vertical { 78 | flex-direction: column; 79 | justify-content: center; 80 | gap: var(--fui-space-xlg); 81 | } 82 | 83 | .fui-modal-footer-actions { 84 | display: flex; 85 | gap: var(--fui-space-lg) var(--fui-space-md); 86 | } 87 | 88 | .fui-modal-footer.layout-vertical .fui-modal-footer-actions { 89 | flex-direction: column; 90 | justify-content: center; 91 | align-items: flex-start; 92 | gap: var(--fui-space-lg); 93 | align-self: stretch; 94 | width: 100%; 95 | } 96 | 97 | .fui-modal-footer.layout-vertical .fui-modal-footer-actions .fui-button { 98 | width: 100%; 99 | } 100 | 101 | .fui-modal-footer-content { 102 | display: flex; 103 | align-items: flex-start; 104 | flex: 1 0 0; 105 | font: var(--running-medium-regular); 106 | color: var(--fui-color-foreground-opaque); 107 | } 108 | 109 | .fui-modal-dismiss { 110 | position: absolute; 111 | display: flex; 112 | top: var(--fui-space-xxlg); 113 | right: var(--fui-space-xxlg); 114 | width: var(--fui-control-height-sm); 115 | height: var(--fui-control-height-sm); 116 | padding: 0px var(--fui-space-xs); 117 | justify-content: center; 118 | align-items: center; 119 | align-self: flex-end; 120 | } 121 | 122 | .ReactModal__Overlay { 123 | opacity: 0; 124 | transition: all 100ms ease-in; 125 | } 126 | 127 | .ReactModal__Overlay--after-open{ 128 | opacity: 1; 129 | } 130 | 131 | .ReactModal__Overlay--before-close{ 132 | opacity: 0; 133 | } 134 | 135 | 136 | .ReactModal__Content { 137 | opacity: 0; 138 | transform: translateY(30px); 139 | transition: all 150ms ease-in; 140 | } 141 | 142 | .ReactModal__Content--after-open{ 143 | transform: translateY(0); 144 | opacity: 1; 145 | } 146 | 147 | .ReactModal__Content--before-close{ 148 | transform: translateY(30px); 149 | opacity: 0; 150 | } 151 | -------------------------------------------------------------------------------- /src/components/fui-modal/fui-modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import { FuiButton, type FuiButtonProps } from '../fui-button/fui-button'; 4 | import Modal from 'react-modal'; 5 | import FuiIconX12x12 from '../../icons/fui-icon-x-16x16'; 6 | import classnames from 'classnames'; 7 | 8 | const compPrefix = `${prefix}-modal`; 9 | export interface ModalAction { 10 | label: string 11 | onClick: () => void 12 | } 13 | 14 | interface ModalHeaderConfig { 15 | modalTitle: string 16 | layout?: 'horizontal' | 'vertical' 17 | modalSubtitle?: string 18 | icon?: JSX.Element 19 | } 20 | 21 | interface ModalFooterConfig { 22 | primaryAction: FuiButtonProps 23 | secondaryAction?: FuiButtonProps 24 | footerContent?: string 25 | layout?: 'horizontal' | 'vertical' 26 | } 27 | 28 | export interface FuiModalProps { 29 | open: boolean 30 | onOpenChange: (nextOpen: boolean) => void 31 | shouldCloseOnOverlayClick?: boolean 32 | modalHeader?: JSX.Element | ModalHeaderConfig 33 | modalBody?: JSX.Element 34 | modalFooter?: JSX.Element | ModalFooterConfig 35 | showDismiss?: boolean 36 | divided?: boolean 37 | 38 | className?: string 39 | } 40 | 41 | export const FuiModal = (props: FuiModalProps) => { 42 | const header = React.useMemo(() => { 43 | if (!props.modalHeader) return null; 44 | if (React.isValidElement(props.modalHeader)) return props.modalHeader; 45 | const { modalTitle, modalSubtitle, icon, layout } = props.modalHeader as ModalHeaderConfig; 46 | return ( 47 |
48 | {icon &&
{icon}
} 49 |
50 | {modalTitle} 51 | {modalSubtitle && {modalSubtitle}} 52 |
53 |
54 | ); 55 | }, [props.modalHeader]); 56 | 57 | const footer = React.useMemo(() => { 58 | if (!props.modalFooter) return null; 59 | if (React.isValidElement(props.modalFooter)) return props.modalFooter; 60 | const { primaryAction, secondaryAction, footerContent, layout } = props.modalFooter as ModalFooterConfig; 61 | return ( 62 |
63 | {(footerContent && layout === 'horizontal' || !layout) && {footerContent}} 64 |
65 | {secondaryAction && } 66 | 67 |
68 | {footerContent && layout === 'vertical' && {footerContent}} 69 |
70 | ); 71 | }, [props.modalFooter]); 72 | 73 | const classNames = classnames( 74 | compPrefix, 75 | props.className, 76 | props.divided ? `${compPrefix}-divided` : '' 77 | ); 78 | 79 | return ( 80 | { props.onOpenChange(false); }} 83 | shouldCloseOnOverlayClick={props.shouldCloseOnOverlayClick} 84 | className={classNames} 85 | overlayClassName={`${compPrefix}-overlay`} 86 | > 87 | {props.showDismiss && { props.onOpenChange(false); }} className={`${compPrefix}-dismiss`} hierarchy='tertiary' icon={} />} 88 | {header} 89 | {props.modalBody} 90 | {footer} 91 | 92 | ); 93 | }; 94 | 95 | FuiModal.defaultProps = { 96 | shouldCloseOnOverlayClick: true, 97 | layout: 'horizontal' 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/fui-notification/fui-notification.css: -------------------------------------------------------------------------------- 1 | .fui-notification { 2 | display: flex; 3 | min-width: 220px; 4 | min-height: 66px; 5 | overflow: hidden; 6 | border-radius: var(--fui-radius-md); 7 | padding: var(--fui-space-xlg); 8 | align-items: center; 9 | gap: var(--fui-space-md); 10 | flex: 1 0 0; 11 | align-self: stretch; 12 | } 13 | 14 | .fui-notification-type-neutral { 15 | border-top: 1px solid var(--fui-color-divider-soft); 16 | border-right: 1px solid var(--fui-color-divider-soft); 17 | border-bottom: 1px solid var(--fui-color-divider-soft); 18 | border-left: 6px solid var(--fui-color-divider-soft); 19 | background: var(--fui-color-background-base); 20 | box-shadow: var(--fui-shadow-crisp-down); 21 | } 22 | 23 | .fui-notification-type-attention { 24 | border-top: 1px solid var(--fui-color-divider-soft); 25 | border-right: 1px solid var(--fui-color-divider-soft); 26 | border-bottom: 1px solid var(--fui-color-divider-soft); 27 | border-left: 6px solid var(--fui-color-status-attention); 28 | background: var(--fui-color-background-base); 29 | box-shadow: var(--fui-shadow-crisp-down); 30 | } 31 | 32 | .fui-notification-type-informative { 33 | border-top: 1px solid var(--fui-color-divider-soft); 34 | border-right: 1px solid var(--fui-color-divider-soft); 35 | border-bottom: 1px solid var(--fui-color-divider-soft); 36 | border-left: 6px solid var(--fui-color-status-info); 37 | background: var(--fui-color-background-base); 38 | box-shadow: var(--fui-shadow-crisp-down); 39 | } 40 | 41 | .fui-notification-type-informative [class*="fui-icon"] { 42 | color: var(--fui-color-status-info); 43 | } 44 | 45 | .fui-notification-type-dangerous { 46 | border-left: 6px solid var(--fui-color-status-danger); 47 | background: var(--fui-color-status-danger-subtle); 48 | } 49 | 50 | .fui-notification-type-dangerous [class*="fui-icon"] { 51 | color: var(--fui-color-status-danger); 52 | } 53 | 54 | .fui-notification-type-success { 55 | border-left: 6px solid var(--fui-color-status-success); 56 | background: var(--fui-color-status-success-subtle); 57 | } 58 | 59 | .fui-notification-type-success [class*="fui-icon"] { 60 | color: var(--fui-color-status-success); 61 | } 62 | 63 | .fui-notification-container { 64 | display: flex; 65 | align-items: center; 66 | gap: var(--fui-space-lg); 67 | flex: 1 0 0; 68 | } 69 | 70 | .fui-notification-text-container { 71 | display: flex; 72 | flex-direction: column; 73 | align-items: flex-start; 74 | flex: 1 0 0; 75 | } 76 | 77 | .fui-notification-title { 78 | font: var(--running-medium-bold); 79 | color: var(--fui-disableable-color-foreground-default); 80 | } 81 | 82 | .fui-notification-description { 83 | font: var(--running-small-regular); 84 | color: var(--fui-color-foreground-opaque); 85 | } 86 | 87 | [class*="fui-notification-icon-wrapper"] { 88 | width: 32px; 89 | height: 32px; 90 | } 91 | 92 | .fui-notification-icon-wrapper-dangerous { 93 | display: flex; 94 | justify-content: center; 95 | align-items: center; 96 | border-radius: var(--fui-radius-round); 97 | border: 1px solid var(--fui-color-status-danger); 98 | color: var(--fui-color-status-danger); 99 | } 100 | 101 | .fui-notification-icon-wrapper-informative { 102 | display: flex; 103 | justify-content: center; 104 | align-items: center; 105 | border-radius: var(--fui-radius-round); 106 | border: 1px solid var(--fui-color-status-info); 107 | color: var(--fui-color-status-info); 108 | } 109 | 110 | .fui-notification-icon-wrapper-success { 111 | display: flex; 112 | justify-content: center; 113 | align-items: center; 114 | border-radius: var(--fui-radius-round); 115 | border: 1px solid var(--fui-color-status-success); 116 | color: var(--fui-color-status-success); 117 | } -------------------------------------------------------------------------------- /src/components/fui-notification/fui-notification.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiNotification } from './fui-notification'; 5 | import FuiIconPlaceholder32X32 from '../../icons/fui-icon-placeholder-32x32'; 6 | 7 | const meta = { 8 | title: ' Components/Notification', 9 | component: FuiNotification, 10 | parameters: { 11 | design: { 12 | type: 'figma', 13 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2511-14321&mode=design&t=jq0JgMhh6dwhuYIm-4' 14 | }, 15 | docs: { 16 | description: { 17 | component: 'Confirm actions, update people about timely events and provide status updates for processes. When providing an action, opt for non-destructive actions and clear distinctions for destructive ones. The information in notifications is valuable but not critical, aiming for quick and concise updates that people can grasp at a glance.' 18 | } 19 | } 20 | }, 21 | tags: ['autodocs'], 22 | argTypes: { 23 | title: { 24 | name: '🔗 title' 25 | }, 26 | type: { 27 | name: '🔗 type' 28 | }, 29 | description: { 30 | name: '🔗 description' 31 | }, 32 | action: { 33 | name: '🔗 action', 34 | control: 'boolean', 35 | mapping: { 36 | false: undefined, 37 | true: { label: 'Action', onClick: () => { }, hierarchy: 'tertiary', size: 'medium' } 38 | } 39 | }, 40 | icon: { 41 | name: '🔗 icon', 42 | control: 'boolean', 43 | mapping: { 44 | false: undefined, 45 | true: 46 | } 47 | }, 48 | className: { 49 | control: { 50 | disable: true 51 | } 52 | }, 53 | ariaLabel: { 54 | control: { 55 | disable: true 56 | } 57 | } 58 | } 59 | } satisfies Meta; 60 | 61 | export default meta; 62 | type Story = StoryObj; 63 | 64 | export const Default: Story = { 65 | name: 'Basic', 66 | args: { 67 | title: 'Notification Title', 68 | action: { label: 'Action', onClick: () => { }, hierarchy: 'tertiary', size: 'medium' }, 69 | description: 'This is the notification description, I like.', 70 | type: 'neutral' 71 | } 72 | }; 73 | 74 | export const Types: Story = { 75 | name: 'Notification Types', 76 | render: (args) => ( 77 |
78 | {['neutral', 'attention', 'informative', 'dangerous', 'success'].map((type: any) => 79 | 80 | )} 81 |
82 | ), 83 | parameters: { 84 | design: { 85 | type: 'figma', 86 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2511-14324&mode=design&t=jq0JgMhh6dwhuYIm-4' 87 | } 88 | }, 89 | args: { 90 | title: 'Notification Title', 91 | action: { label: 'Action', onClick: () => { }, hierarchy: 'tertiary', size: 'medium' }, 92 | description: 'This is the notification description, I like.', 93 | }, 94 | argTypes: { 95 | type: { 96 | table: { 97 | disable: true 98 | } 99 | } 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/components/fui-notification/fui-notification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import FuiIconExclamationMark16x16 from '../../icons/fui-icon-exclamation-mark-16x16'; 4 | import FuiIconCheck16X16 from '../../icons/fui-icon-check-16x16'; 5 | import { FuiButton, FuiButtonProps } from '../fui-button/fui-button'; 6 | import classnames from 'classnames'; 7 | 8 | const compPrefix = `${prefix}-notification`; 9 | 10 | export interface FuiNotificationProps { 11 | title: string 12 | type?: 'neutral' | 'attention' | 'informative' | 'dangerous' | 'success' 13 | description?: string 14 | action?: Omit 15 | icon?: JSX.Element 16 | 17 | className?: string 18 | ariaLabel?: string 19 | } 20 | 21 | const renderNotificationIcon = (type: FuiNotificationProps['type']) => { 22 | if (type === 'attention') { 23 | const attentionTriangle = ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | return attentionTriangle; 31 | } 32 | if (type === 'dangerous') { 33 | return (
); 34 | } 35 | if (type === 'informative') { 36 | return (
); 37 | } 38 | if (type === 'success') { 39 | return (
); 40 | } 41 | }; 42 | 43 | const getActionType = (type: FuiNotificationProps['type']) => { 44 | switch (type) { 45 | case 'attention': 46 | case 'neutral': 47 | case 'informative': 48 | return 'neutral'; 49 | case 'dangerous': 50 | return 'destructive'; 51 | case 'success': 52 | return 'success'; 53 | } 54 | }; 55 | 56 | export const FuiNotification = (props: FuiNotificationProps) => { 57 | const classNames = classnames( 58 | compPrefix, 59 | `${compPrefix}-type-${props.type}`, 60 | props.className 61 | ); 62 | 63 | return ( 64 |
65 |
66 | {props.icon || renderNotificationIcon(props.type)} 67 |
68 |
{props.title}
69 |
{props.description}
70 |
71 |
72 | {props.action ? : null} 73 |
74 | ); 75 | }; 76 | 77 | FuiNotification.defaultProps = { 78 | type: 'neutral' 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/fui-option-group/fui-option-group.css: -------------------------------------------------------------------------------- 1 | .fui-option-group .fui-option-group-menu-option::after { 2 | border-radius: var(--fui-radius-sm); 3 | width: calc(100% + var(--fui-space-lg)); 4 | left: 50%; 5 | transform: translateX(-50%); 6 | } 7 | 8 | .fui-option-group { 9 | border-radius: var(--fui-radius-lg); 10 | border: 1px solid var(--fui-color-divider-soft); 11 | display: flex; 12 | padding: var(--fui-space-lg) var(--fui-space-xlg); 13 | flex-direction: column; 14 | align-items: flex-start; 15 | background: var(--fui-color-background-base); 16 | } 17 | 18 | .fui-option-group-menu-option { 19 | display: flex; 20 | height: 38px; 21 | gap: var(--fui-space-sm); 22 | align-items: center; 23 | align-self: stretch; 24 | font: var(--running-small-bold); 25 | position: relative; 26 | } 27 | 28 | .fui-option-group-menu-group { 29 | display: flex; 30 | flex-direction: column; 31 | min-height: 38px; 32 | align-self: stretch; 33 | margin-bottom: var(--fui-space-md); 34 | } 35 | 36 | .fui-option-group-menu-group:last-child { 37 | margin-bottom: 0; 38 | } 39 | 40 | .fui-option-group-menu-group-label { 41 | font: var(--running-small-bold); 42 | color: var(--fui-color-foreground-softest); 43 | } 44 | 45 | .fui-option-group-menu-option-label { 46 | display: flex; 47 | flex: 1 0 0; 48 | color: var(--fui-disableable-color-foreground-default); 49 | } -------------------------------------------------------------------------------- /src/components/fui-option-group/fui-option-group.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | import FuiIconPlaceholder16X16 from '../../icons/fui-icon-placeholder-16x16'; 4 | import { FuiOptionGroup } from './fui-option-group'; 5 | 6 | const meta = { 7 | title: ' Components/OptionGroup', 8 | component: FuiOptionGroup, 9 | tags: ['autodocs'], 10 | parameters: { 11 | design: { 12 | type: 'figma', 13 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2574-19579&mode=design&t=jq0JgMhh6dwhuYIm-4' 14 | }, 15 | docs: { 16 | description: { 17 | component: 'Embed this component within other components such as Popover or Select to showcase a configurable grouped list of list items or options. Each option can be individually configured, and the grouping allows for versatile customization.' 18 | } 19 | } 20 | }, 21 | argTypes: { 22 | options: { 23 | name: '🔗 options', 24 | control: { 25 | disable: true 26 | } 27 | }, 28 | className: { 29 | control: { 30 | disable: true 31 | } 32 | }, 33 | } 34 | } satisfies Meta; 35 | 36 | export default meta; 37 | type Story = StoryObj; 38 | 39 | export const Default: Story = { 40 | name: 'Basic', 41 | args: { 42 | options: [ 43 | { 44 | label: 'Group Title', options: [ 45 | { label: 'Option', value: '1' }, 46 | { label: 'Option', value: '2' }, 47 | { label: 'Option', value: '3' }, 48 | ] 49 | } 50 | ] 51 | } 52 | }; 53 | 54 | export const CustomOptions: Story = { 55 | parameters: { 56 | docs: { 57 | description: { 58 | story: 'You can customize your options by passing in a `prefix` (on single select) or `suffix` (on single or multi select).' 59 | } 60 | } 61 | }, 62 | args: { 63 | options: [ 64 | { label: 'Option 1', value: '1', prefix: , suffix: }, 65 | { label: 'Option 2', value: '2', prefix: , suffix: }, 66 | { label: 'Option 3', value: '3', prefix: , suffix: } 67 | ] 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/fui-option-group/fui-option-group.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import classNames from 'classnames'; 4 | 5 | const compPrefix = `${prefix}-option-group`; 6 | 7 | export interface Option { 8 | label: string 9 | value: string 10 | prefix?: JSX.Element 11 | suffix?: JSX.Element 12 | } 13 | 14 | export interface OptionGroup { 15 | label: string 16 | options: Option[] 17 | } 18 | 19 | export interface FuiOptionGroupProps { 20 | options: (Option | OptionGroup)[] 21 | className?: string 22 | onSelect?: (value: string) => void 23 | } 24 | 25 | const isOptionGroup = (option: Option | OptionGroup): option is OptionGroup => { 26 | return (option as OptionGroup).options !== undefined; 27 | }; 28 | 29 | export const FuiOptionGroup = ({ 30 | options, 31 | className, 32 | onSelect, 33 | }: FuiOptionGroupProps) => { 34 | const classnames = classNames(compPrefix, className); 35 | 36 | const renderMenuOption = (option: Option, index: number) => { 37 | const onKeyDown = (e: React.KeyboardEvent) => { 38 | if (e.key === ' ') { 39 | e.preventDefault(); 40 | onSelect?.(option.value); 41 | } 42 | }; 43 | 44 | return ( 45 |
{ onSelect?.(option.value); }}> 46 | {option.prefix} 47 |
{option.label}
48 | {option.suffix} 49 |
50 | ); 51 | }; 52 | 53 | const renderOptions = (options: (Option | OptionGroup)[]) => { 54 | return options.map((option, index) => { 55 | if (isOptionGroup(option)) { 56 | return ( 57 |
58 |
{option.label}
59 | {option.options.map(renderMenuOption)} 60 |
61 | ); 62 | } 63 | return renderMenuOption(option, index); 64 | }); 65 | }; 66 | 67 | return ( 68 |
69 | {renderOptions(options)} 70 |
71 | ); 72 | }; -------------------------------------------------------------------------------- /src/components/fui-popover/fui-popover.css: -------------------------------------------------------------------------------- 1 | .fui-popover-trigger { 2 | } 3 | 4 | .fui-popover { 5 | box-shadow: 0px 0px 10px -2px rgba(0, 0, 0, 0.12); 6 | border-radius: var(--fui-border-radius-lg); 7 | border: 1px solid var(--fui-color-divider-soft); 8 | background: var(--fui-color-background-base); 9 | color: var(--fui-disableable-color-foreground-default); 10 | } 11 | 12 | .fui-popover-title { 13 | padding: var(--fui-space-xlg); 14 | border-bottom: 1px solid var(--fui-color-divider-soft); 15 | } 16 | 17 | .fui-popover-title-text { 18 | font: var(--running-medium-bold); 19 | font-family: var(--fui-font-family); 20 | } 21 | 22 | .fui-popover-title-dismiss { 23 | position: absolute; 24 | top: 6px; 25 | right: 6px; 26 | width: 26px; 27 | height: 26px; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .fui-popover-footer { 34 | padding: var(--fui-space-xxlg); 35 | border-top: 1px solid var(--fui-color-divider-soft); 36 | display: flex; 37 | justify-content: flex-end; 38 | gap: var(--fui-space-md); 39 | } -------------------------------------------------------------------------------- /src/components/fui-popover/fui-popover.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiPopover, type FuiPopoverProps } from './fui-popover'; 5 | import { FuiButton } from '../fui-button/fui-button'; 6 | import FuiIllustrationCat from '../../illustrations/fui-illustration-cat'; 7 | import { FuiRadio } from '../fui-radio/fui-radio'; 8 | import { FuiEmpty } from '../fui-empty/fui-empty'; 9 | 10 | const meta = { 11 | title: ' Components/Popover', 12 | component: FuiPopover, 13 | tags: ['autodocs'], 14 | parameters: { 15 | design: { 16 | type: 'figma', 17 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=3079-28605&mode=dev' 18 | }, 19 | docs: { 20 | description: { 21 | component: 'Help people conveniently access functionality or info. Popover is a modular element that appears above other content when triggered. Should ideally not obstruct the element that triggered them or essential content. Including a Close button is recommended for clarity, but a Popover often closes by clicking outside or selecting an item within. Should not be obscured by other elements, except for alerts. Avoid making a Popover too big.' 22 | } 23 | } 24 | }, 25 | argTypes: { 26 | header: { 27 | name: '🔗 header', 28 | control: 'text' 29 | }, 30 | body: { 31 | name: '🔗 body', 32 | control: { 33 | disable: true 34 | } 35 | }, 36 | footer: { 37 | name: '🔗 footer', 38 | control: { 39 | disable: true 40 | } 41 | }, 42 | children: { 43 | control: { 44 | disable: true 45 | } 46 | }, 47 | className: { 48 | control: { 49 | disable: true 50 | } 51 | }, 52 | ariaLabel: { 53 | control: { 54 | disable: true 55 | } 56 | } 57 | } 58 | } satisfies Meta; 59 | 60 | export default meta; 61 | type Story = StoryObj; 62 | 63 | export const Default: Story = { 64 | render: (args) => { 65 | const [open, setOpen] = React.useState(true); 66 | 67 | React.useEffect(() => { 68 | setOpen(args.isOpen); 69 | }, [args.isOpen]); 70 | 71 | const body = ( 72 |
73 | 74 |
75 | ); 76 | 77 | const toggleOpen = () => { 78 | setOpen(open => !open); 79 | }; 80 | 81 | return ( 82 |
83 | { setOpen(false); }} 88 | body={body} 89 | > 90 |
91 | 92 |
93 |
94 |
95 | ); 96 | }, 97 | name: 'Basic', 98 | args: { 99 | body:
, 100 | children:
, 101 | isOpen: false 102 | } 103 | }; 104 | 105 | export const DismissibleWithTitle: Story = { 106 | parameters: { 107 | design: { 108 | type: 'figma', 109 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=3079-19738&mode=dev' 110 | } 111 | }, 112 | render: (args) => { 113 | const [open, setOpen] = React.useState(false); 114 | 115 | React.useEffect(() => { 116 | setOpen(args.isOpen); 117 | }, [args.isOpen]); 118 | 119 | const body = ( 120 |
121 | 122 |
123 | ); 124 | 125 | return ( 126 |
127 | { setOpen(false); }} 132 | body={body} 133 | header={{ titleText: 'Popover Title', dismissible: true, onDismiss: () => { setOpen(false); } }} 134 | > 135 |
136 | { setOpen(!open); }} label='Open Popover' /> 137 |
138 |
139 |
140 | ); 141 | }, 142 | args: { 143 | body:
, 144 | children:
, 145 | isOpen: false 146 | } 147 | }; 148 | 149 | export const Position: Story = { 150 | render: (args) => { 151 | const [open, setOpen] = React.useState(false); 152 | const [position, setPosition] = React.useState('bottom'); 153 | 154 | React.useEffect(() => { 155 | setOpen(args.isOpen); 156 | }, [args.isOpen]); 157 | 158 | const body = ( 159 |
160 | 161 |
162 | ); 163 | 164 | return ( 165 |
166 |
167 | { setPosition('bottom'); }} label='Bottom' /> 168 | { setPosition('left'); }} label='Left' /> 169 | { setPosition('right'); }} label='Right' /> 170 | { setPosition('top'); }} label='Top' /> 171 |
172 | { setOpen(false); } }} 176 | placement={position} 177 | onClickOutside={() => { setOpen(false); }} 178 | body={body} 179 | > 180 |
181 | { setOpen(!open); }} label='Open Popover' /> 182 |
183 |
184 |
185 | ); 186 | }, 187 | name: 'Change Popover Position', 188 | args: { 189 | body:
, 190 | children:
, 191 | isOpen: false 192 | } 193 | }; 194 | 195 | export const Actions: Story = { 196 | parameters: { 197 | design: { 198 | type: 'figma', 199 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=3081-56588&mode=dev' 200 | } 201 | }, 202 | render: (args) => { 203 | const [open, setOpen] = React.useState(false); 204 | 205 | React.useEffect(() => { 206 | setOpen(args.isOpen); 207 | }, [args.isOpen]); 208 | 209 | const body = ( 210 |
211 | 212 |
213 | ); 214 | 215 | return ( 216 |
217 | { setOpen(false); } }} 221 | onClickOutside={() => { setOpen(false); }} 222 | placement={'bottom'} 223 | body={body} 224 | footer={{ primaryAction: { label: 'Submit', actionType: 'neutral', hierarchy: 'primary', size: 'medium', onClick: () => { setOpen(false); } }, secondaryAction: { label: 'Cancel', actionType: 'neutral', hierarchy: 'tertiary', size: 'medium', onClick: () => { setOpen(false); } } }} 225 | > 226 |
227 | { setOpen(!open); }} label='Open Popover' /> 228 |
229 |
230 |
231 | ); 232 | }, 233 | name: 'Footer Actions', 234 | args: { 235 | body:
, 236 | children:
, 237 | isOpen: false 238 | } 239 | }; 240 | -------------------------------------------------------------------------------- /src/components/fui-popover/fui-popover.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import { FuiButton, type FuiButtonProps } from '../fui-button/fui-button'; 4 | import FuiIconX12x12 from '../../icons/fui-icon-x-12x12'; 5 | import classnames from 'classnames'; 6 | import { Popover, PopoverContent, PopoverTrigger } from './popover'; 7 | import { Placement } from '@floating-ui/react'; 8 | 9 | const compPrefix = `${prefix}-popover`; 10 | 11 | interface PopoverHeaderConfig { 12 | titleText: string 13 | dismissible?: boolean 14 | onDismiss?: () => void 15 | } 16 | 17 | interface PopoverFooterConfig { 18 | primaryAction: FuiButtonProps 19 | secondaryAction?: FuiButtonProps 20 | footerContent?: string 21 | } 22 | 23 | export type FuiPopoverProps = { 24 | body: JSX.Element 25 | header?: JSX.Element | PopoverHeaderConfig 26 | footer?: JSX.Element | PopoverFooterConfig 27 | ariaLabel?: string 28 | className?: string 29 | placement?: Placement 30 | children: React.ReactNode 31 | isOpen: boolean 32 | onClickOutside?: () => void 33 | }; 34 | 35 | export const FuiPopover = (props: FuiPopoverProps) => { 36 | const header = React.useMemo(() => { 37 | if (props.header) { 38 | if (!React.isValidElement(props.header)) { 39 | const { titleText, dismissible, onDismiss } = props.header as PopoverHeaderConfig; 40 | const dismissButton = ( 41 | } onClick={onDismiss} hierarchy='tertiary' actionType='neutral' size='medium' /> 42 | ); 43 | return ( 44 |
45 | {titleText} 46 | {dismissible ? dismissButton : null} 47 |
48 | ); 49 | } 50 | return props.header; 51 | } 52 | }, [props.header]); 53 | 54 | const footer = React.useMemo(() => { 55 | if (!props.footer) return null; 56 | if (React.isValidElement(props.footer)) { 57 | return props.footer; 58 | } 59 | const { primaryAction, secondaryAction } = props.footer as PopoverFooterConfig; 60 | if (primaryAction ?? secondaryAction) { 61 | return ( 62 |
63 | {secondaryAction && } 64 | {primaryAction && } 65 |
66 | ); 67 | } 68 | }, [props.footer]); 69 | 70 | const content = React.useMemo(() => { 71 | return ( 72 |
73 | {header} 74 | {props.body} 75 | {footer} 76 |
77 | ); 78 | }, [props.body]); 79 | 80 | return ( 81 |
82 | 83 | 84 | {props.children} 85 | 86 | 87 | {content} 88 | 89 | 90 |
91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /src/components/fui-popover/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | useFloating, 4 | autoUpdate, 5 | offset, 6 | flip, 7 | shift, 8 | useClick, 9 | useDismiss, 10 | useRole, 11 | useInteractions, 12 | useMergeRefs, 13 | Placement, 14 | FloatingPortal, 15 | FloatingFocusManager, 16 | useId 17 | } from "@floating-ui/react"; 18 | 19 | interface PopoverOptions { 20 | initialOpen?: boolean; 21 | placement?: Placement; 22 | modal?: boolean; 23 | open?: boolean; 24 | onOpenChange?: (open: boolean) => void; 25 | } 26 | 27 | export function usePopover ({ 28 | initialOpen = false, 29 | placement = "bottom", 30 | modal, 31 | open: controlledOpen, 32 | onOpenChange: setControlledOpen 33 | }: PopoverOptions = {}) { 34 | const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); 35 | const [labelId, setLabelId] = React.useState(); 36 | const [descriptionId, setDescriptionId] = React.useState< 37 | string | undefined 38 | >(); 39 | 40 | const open = controlledOpen ?? uncontrolledOpen; 41 | const setOpen = setControlledOpen ?? setUncontrolledOpen; 42 | 43 | const data = useFloating({ 44 | placement, 45 | open, 46 | onOpenChange: setOpen, 47 | whileElementsMounted: autoUpdate, 48 | middleware: [ 49 | offset(5), 50 | flip({ 51 | crossAxis: placement.includes("-"), 52 | fallbackAxisSideDirection: "end", 53 | padding: 5 54 | }), 55 | shift({ padding: 5 }), 56 | ] 57 | }); 58 | 59 | const context = data.context; 60 | 61 | const click = useClick(context, { 62 | enabled: controlledOpen == null 63 | }); 64 | const dismiss = useDismiss(context); 65 | const role = useRole(context); 66 | 67 | const interactions = useInteractions([click, dismiss, role]); 68 | 69 | return React.useMemo( 70 | () => ({ 71 | open, 72 | setOpen, 73 | ...interactions, 74 | ...data, 75 | modal, 76 | labelId, 77 | descriptionId, 78 | setLabelId, 79 | setDescriptionId 80 | }), 81 | [open, setOpen, interactions, data, modal, labelId, descriptionId] 82 | ); 83 | } 84 | 85 | type ContextType = 86 | | (ReturnType & { 87 | setLabelId: React.Dispatch>; 88 | setDescriptionId: React.Dispatch< 89 | React.SetStateAction 90 | >; 91 | }) 92 | | null; 93 | 94 | const PopoverContext = React.createContext(null); 95 | 96 | export const usePopoverContext = () => { 97 | const context = React.useContext(PopoverContext); 98 | 99 | if (context == null) { 100 | throw new Error("Popover components must be wrapped in "); 101 | } 102 | 103 | return context; 104 | }; 105 | 106 | export function Popover ({ 107 | children, 108 | modal = false, 109 | ...restOptions 110 | }: { 111 | children: React.ReactNode; 112 | } & PopoverOptions) { 113 | // This can accept any props as options, e.g. `placement`, 114 | // or other positioning options. 115 | const popover = usePopover({ modal, ...restOptions }); 116 | return ( 117 | 118 | {children} 119 | 120 | ); 121 | } 122 | 123 | interface PopoverTriggerProps { 124 | children: React.ReactNode; 125 | asChild?: boolean; 126 | } 127 | 128 | export const PopoverTrigger = React.forwardRef< 129 | HTMLElement, 130 | React.HTMLProps & PopoverTriggerProps 131 | >(function PopoverTrigger ({ children, asChild = false, ...props }, propRef) { 132 | const context = usePopoverContext(); 133 | const childrenRef = (children as any).ref; 134 | const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]); 135 | 136 | // `asChild` allows the user to pass any element as the anchor 137 | if (asChild && React.isValidElement(children)) { 138 | return React.cloneElement( 139 | children, 140 | context.getReferenceProps({ 141 | ref, 142 | ...props, 143 | ...children.props, 144 | "data-state": context.open ? "open" : "closed" 145 | }) 146 | ); 147 | } 148 | 149 | return ( 150 | 159 | ); 160 | }); 161 | 162 | export const PopoverContent = React.forwardRef< 163 | HTMLDivElement, 164 | React.HTMLProps 165 | >(function PopoverContent ({ style, ...props }, propRef) { 166 | const { context: floatingContext, ...context } = usePopoverContext(); 167 | const ref = useMergeRefs([context.refs.setFloating, propRef]); 168 | 169 | if (!floatingContext.open) return null; 170 | 171 | return ( 172 | 173 | 174 |
181 | {props.children} 182 |
183 |
184 |
185 | ); 186 | }); 187 | 188 | export const PopoverHeading = React.forwardRef< 189 | HTMLHeadingElement, 190 | React.HTMLProps 191 | >(function PopoverHeading (props, ref) { 192 | const { setLabelId } = usePopoverContext(); 193 | const id = useId(); 194 | 195 | // Only sets `aria-labelledby` on the Popover root element 196 | // if this component is mounted inside it. 197 | React.useLayoutEffect(() => { 198 | setLabelId(id); 199 | return () => setLabelId(undefined); 200 | }, [id, setLabelId]); 201 | 202 | return ( 203 |

204 | {props.children} 205 |

206 | ); 207 | }); 208 | 209 | export const PopoverDescription = React.forwardRef< 210 | HTMLParagraphElement, 211 | React.HTMLProps 212 | >(function PopoverDescription (props, ref) { 213 | const { setDescriptionId } = usePopoverContext(); 214 | const id = useId(); 215 | 216 | // Only sets `aria-describedby` on the Popover root element 217 | // if this component is mounted inside it. 218 | React.useLayoutEffect(() => { 219 | setDescriptionId(id); 220 | return () => setDescriptionId(undefined); 221 | }, [id, setDescriptionId]); 222 | 223 | return

; 224 | }); 225 | 226 | export const PopoverClose = React.forwardRef< 227 | HTMLButtonElement, 228 | React.ButtonHTMLAttributes 229 | >(function PopoverClose (props, ref) { 230 | const { setOpen } = usePopoverContext(); 231 | return ( 232 | 85 |

86 | { props.onChange(Number(e.target.value)); }} disabled={isAllDisabled} /> 87 |
88 | 91 |
92 |
93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/fui-switch/fui-switch.css: -------------------------------------------------------------------------------- 1 | .fui-switch { 2 | display: flex; 3 | padding: var(--fui-space-xxs); 4 | flex-direction: column; 5 | justify-content: center; 6 | gap: 10px; 7 | flex-shrink: 0; 8 | border-radius: var(--fui-radius-round); 9 | position: relative; 10 | transition: background-color 0.2s cubic-bezier(0.08, 0.09, 0.15, 1); 11 | } 12 | 13 | .fui-switch-size-small { 14 | width: 44px; 15 | height: var(--fui-control-height-sm); 16 | min-width: 44px; 17 | max-width: 44px; 18 | min-height: var(--fui-control-height-sm); 19 | max-height: var(--fui-control-height-sm); 20 | } 21 | 22 | .fui-switch-size-medium { 23 | width: 64px; 24 | height: var(--fui-control-height-md); 25 | min-width: 64px; 26 | max-width: 64px; 27 | min-height: var(--fui-control-height-md); 28 | max-height: var(--fui-control-height-md); 29 | } 30 | 31 | .fui-switch-off { 32 | outline: 1px solid var(--fui-disableable-color-divider-soft); 33 | background-color: var(--fui-disableable-color-background-subdued); 34 | } 35 | 36 | .fui-switch-on { 37 | background-color: var(--fui-disableable-color-button-primary-bg-neutral); 38 | outline: 3px solid var(--fui-disableable-color-background-subdued); 39 | } 40 | 41 | .fui-switch-knob { 42 | display: flex; 43 | justify-content: center; 44 | flex-direction: column; 45 | align-items: center; 46 | flex-shrink: 0; 47 | border-radius: var(--fui-radius-round); 48 | background: var(--fui-disableable-color-background-white); 49 | position: absolute; 50 | transition: right 0.2s cubic-bezier(0.08, 0.09, 0.15, 1), box-shadow 0.2s cubic-bezier(0.08, 0.09, 0.15, 1); 51 | box-shadow: -2px 0px 6px -2px rgba(0, 0, 0, 0.12); 52 | } 53 | 54 | .fui-switch.fui-interactable { 55 | border-radius: var(--fui-radius-round); 56 | overflow: hidden 57 | } 58 | 59 | .fui-switch-size-small .fui-switch-knob { 60 | width: 22px; 61 | height: 22px; 62 | right: calc(100% - 25px); 63 | } 64 | 65 | .fui-switch-size-medium .fui-switch-knob { 66 | width: 30px; 67 | height: 30px; 68 | right: calc(100% - 32px); 69 | } 70 | 71 | .fui-switch-on .fui-switch-knob { 72 | box-shadow: -2px 0px 6px -2px rgba(0, 0, 0, 0.20); 73 | right: var(--fui-space-xxs); 74 | } 75 | 76 | .fui-switch-off .fui-switch-knob { 77 | border: 1px solid var(--fui-disableable-color-divider-soft); 78 | box-shadow: 2px 0px 3px -2px rgba(0, 0, 0, 0.10); 79 | } 80 | 81 | .fui-switch-thumb { 82 | width: 70%; 83 | height: 70%; 84 | flex-shrink: 0; 85 | border-radius: var(--fui-radius-round); 86 | background: linear-gradient(180deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0.00) 50%); 87 | } -------------------------------------------------------------------------------- /src/components/fui-switch/fui-switch.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiSwitch } from './fui-switch'; 5 | 6 | const meta = { 7 | title: ' Components/Switch', 8 | component: FuiSwitch, 9 | tags: ['autodocs'], 10 | parameters: { 11 | design: { 12 | type: 'figma', 13 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2540-39847&mode=design&t=jq0JgMhh6dwhuYIm-4' 14 | }, 15 | docs: { 16 | description: { 17 | component: 'Help people choose between two opposing values. Use for options with immediate effects with clear identification through context or labels if needed. Switch has more visual weight than a checkbox, so it looks better when it controls more functionality, so avoid employing it for minor settings.' 18 | } 19 | } 20 | }, 21 | argTypes: { 22 | on: { 23 | name: '🔗 on' 24 | }, 25 | size: { 26 | name: '🔗 size', 27 | control: 'select' 28 | }, 29 | disabled: { 30 | name: '🔗 disabled' 31 | }, 32 | className: { 33 | control: { 34 | disable: true 35 | } 36 | }, 37 | ariaLabel: { 38 | control: { 39 | disable: true 40 | } 41 | } 42 | } 43 | } satisfies Meta; 44 | 45 | export default meta; 46 | type Story = StoryObj; 47 | 48 | export const Default: Story = { 49 | render: (args) => { 50 | const [on, setOn] = React.useState(args.on); 51 | 52 | React.useEffect(() => { 53 | setOn(args.on); 54 | }, [args.on]); 55 | 56 | return ( 57 | <> 58 | 59 | 60 | ); 61 | }, 62 | name: 'Basic', 63 | args: { 64 | on: false 65 | } 66 | }; 67 | 68 | export const SwitchSizes: Story = { 69 | render: (args) => { 70 | const [on, setOn] = React.useState(args.on); 71 | 72 | React.useEffect(() => { 73 | setOn(args.on); 74 | }, [args.on]); 75 | 76 | return ( 77 |
78 | 79 | 80 |
81 | ); 82 | }, 83 | parameters: { 84 | design: { 85 | type: 'figma', 86 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2540-14945&mode=design&t=jq0JgMhh6dwhuYIm-4' 87 | } 88 | }, 89 | argTypes: { 90 | size: { 91 | table: { 92 | disable: true 93 | } 94 | } 95 | }, 96 | args: { 97 | on: true 98 | } 99 | }; 100 | 101 | export const Disabled: Story = { 102 | render: (args) => { 103 | return ( 104 |
105 | 106 | 107 | 108 | 109 |
110 | ); 111 | }, 112 | parameters: { 113 | design: { 114 | type: 'figma', 115 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2540-14958&mode=design&t=jq0JgMhh6dwhuYIm-4' 116 | } 117 | }, 118 | argTypes: { 119 | disabled: { 120 | table: { 121 | disable: true 122 | } 123 | } 124 | }, 125 | args: { 126 | disabled: true, 127 | on: false 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /src/components/fui-switch/fui-switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import classnames from 'classnames'; 4 | 5 | const compPrefix = `${prefix}-switch`; 6 | 7 | export interface FuiSwitchProps { 8 | on: boolean 9 | onChange: (on: boolean) => void 10 | size?: 'small' | 'medium' 11 | disabled?: boolean 12 | 13 | className?: string 14 | ariaLabel?: string 15 | } 16 | 17 | export const FuiSwitch = ({ 18 | size = 'medium', 19 | ...props 20 | }: FuiSwitchProps) => { 21 | const classNames = classnames( 22 | compPrefix, 23 | `${compPrefix}-size-${size}`, 24 | props.on ? `${compPrefix}-on` : `${compPrefix}-off`, 25 | props.disabled ? `${prefix}-disabled` : `${prefix}-interactable`, 26 | props.className 27 | ); 28 | 29 | return ( 30 |
{ !props.disabled && props.onChange(!props.on); }} role='checkbox' aria-label={props.ariaLabel} className={classNames}> 31 |
32 |
33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/fui-text-input/fui-text-input.css: -------------------------------------------------------------------------------- 1 | .fui-text-input-wrapper { 2 | display: flex; 3 | min-width: 150px; 4 | min-height: var(--fui-control-height-md); 5 | flex-direction: column; 6 | align-items: flex-start; 7 | flex-shrink: 0; 8 | } 9 | 10 | .fui-text-input-container { 11 | display: flex; 12 | height: var(--fui-control-height-md); 13 | align-items: center; 14 | align-self: stretch; 15 | border-radius: var(--fui-radius-md); 16 | border: 1px solid var(--fui-disableable-color-divider-solid); 17 | background: var(--fui-disableable-color-background-base); 18 | padding: var(--fui-space-xs); 19 | gap: var(--fui-space-xxs); 20 | } 21 | 22 | .fui-text-input-input-container { 23 | width: 100%; 24 | height: 100%; 25 | display: flex; 26 | align-items: center; 27 | position: relative; 28 | } 29 | 30 | .fui-text-input-input-container.has-prefix { 31 | padding-left: 0; 32 | } 33 | 34 | .fui-text-input-input-container::after { 35 | margin: -1px; 36 | border-radius: var(--fui-radius-sm); 37 | } 38 | 39 | .fui-text-input-input { 40 | padding: 0 var(--fui-space-md); 41 | border: none; 42 | width: 100%; 43 | height: 100%; 44 | background-color: transparent; 45 | color: var(--fui-disableable-color-foreground-default); 46 | } 47 | 48 | .fui-text-input-input:focus-visible { 49 | outline: 2px solid; 50 | outline-color: var(--fui-color-state-focus); 51 | border-radius: var(--fui-radius-xs); 52 | } 53 | 54 | .fui-text-input-input::placeholder { 55 | font: var(--running-small-regular); 56 | color: var(--fui-disableable-color-foreground-softest); 57 | } 58 | 59 | .fui-text-input-clearable-icon-container { 60 | display: flex; 61 | width: var(--fui-space-xxlg); 62 | height: var(--fui-space-xxlg); 63 | justify-content: center; 64 | align-items: center; 65 | position: relative; 66 | height: 24px; 67 | width: 24px; 68 | margin-right: var(--fui-space-xxs); 69 | } 70 | 71 | .fui-text-input-clearable-icon-container::after { 72 | border-radius: var(--fui-radius-sm); 73 | margin: -1px 0; 74 | } 75 | 76 | .fui-text-input-label { 77 | color: var(--fui-color-foreground-soft); 78 | font: var(--running-small-bold); 79 | } 80 | 81 | .fui-text-input-suffix { 82 | margin-right: var(--fui-space-xs); 83 | } 84 | 85 | .fui-text-input-prefix { 86 | margin-left: var(--fui-space-xs); 87 | } -------------------------------------------------------------------------------- /src/components/fui-text-input/fui-text-input.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiTextInput } from './fui-text-input'; 5 | import FuiIconPlaceholder16X16 from '../../icons/fui-icon-placeholder-16x16'; 6 | 7 | const meta = { 8 | title: ' Components/TextInput', 9 | component: FuiTextInput, 10 | tags: ['autodocs'], 11 | parameters: { 12 | design: { 13 | type: 'figma', 14 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2547-68586&mode=design&t=jq0JgMhh6dwhuYIm-4' 15 | }, 16 | docs: { 17 | description: { 18 | component: 'Request a small amount of textual information from people, such as a name or an email address. Display a label above the text field to help communicate its purpose. To the extent possible, match the size of a text field to the quantity of anticipated text. Use status to validate fields when it makes sense.' 19 | } 20 | } 21 | }, 22 | argTypes: { 23 | prefix: { 24 | name: '🔗 prefix', 25 | control: 'boolean', 26 | mapping: { 27 | false: undefined, 28 | true: 29 | } 30 | }, 31 | suffix: { 32 | name: '🔗 suffix', 33 | control: 'boolean', 34 | mapping: { 35 | false: undefined, 36 | true: 37 | } 38 | }, 39 | clearable: { 40 | name: '🔗 clearable' 41 | }, 42 | disabled: { 43 | name: '🔗 disabled' 44 | }, 45 | label: { 46 | name: '🔗 label' 47 | }, 48 | placeholder: { 49 | name: '🔗 placeholder' 50 | }, 51 | status: { 52 | name: '🔗 status', 53 | control: 'boolean', 54 | mapping: { 55 | false: undefined, 56 | true: { type: 'invalid', message: 'Invalid Input' } 57 | } 58 | }, 59 | value: { 60 | name: '🔗 value' 61 | }, 62 | className: { 63 | control: { 64 | disable: true 65 | } 66 | }, 67 | ariaLabel: { 68 | control: { 69 | disable: true 70 | } 71 | } 72 | } 73 | } satisfies Meta; 74 | 75 | export default meta; 76 | type Story = StoryObj; 77 | 78 | export const Default: Story = { 79 | render: (args) => { 80 | const [value, setValue] = React.useState(args.value); 81 | 82 | React.useEffect(() => { 83 | setValue(args.value); 84 | }, [args.value]); 85 | 86 | return ( 87 | 88 | ); 89 | }, 90 | name: 'Basic', 91 | args: { 92 | value: '', 93 | placeholder: 'Placeholder' 94 | } 95 | }; 96 | 97 | export const PrefixAndSuffix: Story = { 98 | render: (args) => { 99 | const [value, setValue] = React.useState(args.value); 100 | 101 | React.useEffect(() => { 102 | setValue(args.value); 103 | }, [args.value]); 104 | 105 | return ( 106 | 107 | ); 108 | }, 109 | parameters: { 110 | design: { 111 | type: 'figma', 112 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2544-55579&mode=design&t=jq0JgMhh6dwhuYIm-4' 113 | } 114 | }, 115 | argTypes: { 116 | suffix: { 117 | table: { 118 | disable: true 119 | } 120 | }, 121 | prefix: { 122 | table: { 123 | disable: true 124 | } 125 | } 126 | }, 127 | args: { 128 | value: '', 129 | prefix: , 130 | suffix: 131 | } 132 | }; 133 | 134 | export const Clearable: Story = { 135 | render: (args) => { 136 | const [value, setValue] = React.useState(args.value); 137 | 138 | React.useEffect(() => { 139 | setValue(args.value); 140 | }, [args.value]); 141 | 142 | return ( 143 | 144 | ); 145 | }, 146 | parameters: { 147 | design: { 148 | type: 'figma', 149 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2545-56021&mode=design&t=jq0JgMhh6dwhuYIm-4' 150 | } 151 | }, 152 | argTypes: { 153 | clearable: { 154 | table: { 155 | disable: true 156 | } 157 | } 158 | }, 159 | args: { 160 | value: '', 161 | clearable: true 162 | } 163 | }; 164 | 165 | export const Disabled: Story = { 166 | render: (args) => { 167 | const [value, setValue] = React.useState(args.value); 168 | 169 | React.useEffect(() => { 170 | setValue(args.value); 171 | }, [args.value]); 172 | 173 | return ( 174 | <> 175 | 176 | } suffix={} /> 177 | 178 | 179 | ); 180 | }, 181 | parameters: { 182 | design: { 183 | type: 'figma', 184 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2545-55880&mode=design&t=jq0JgMhh6dwhuYIm-4' 185 | } 186 | }, 187 | argTypes: { 188 | disabled: { 189 | table: { 190 | disable: true 191 | } 192 | } 193 | }, 194 | args: { 195 | value: 'Value', 196 | disabled: true 197 | } 198 | }; 199 | 200 | export const LabelAndStatus: Story = { 201 | render: (args) => { 202 | const [value, setValue] = React.useState(args.value); 203 | 204 | React.useEffect(() => { 205 | setValue(args.value); 206 | }, [args.value]); 207 | 208 | return ( 209 | 210 | ); 211 | }, 212 | parameters: { 213 | design: { 214 | type: 'figma', 215 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2544-55650&mode=design&t=jq0JgMhh6dwhuYIm-4' 216 | } 217 | }, 218 | argTypes: { 219 | label: { 220 | table: { 221 | disable: true 222 | } 223 | }, 224 | status: { 225 | table: { 226 | disable: true 227 | } 228 | } 229 | }, 230 | args: { 231 | value: '', 232 | label: 'Input Title', 233 | status: { type: 'invalid', message: 'Invalid Input' } 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /src/components/fui-text-input/fui-text-input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import FuiIconX12x12 from '../../icons/fui-icon-x-12x12'; 4 | import { FuiButton } from '../fui-button/fui-button'; 5 | import { FuiStatusMessage, type FuiStatusMessageProps } from '../fui-status-message/fui-status-message'; 6 | import classnames from 'classnames'; 7 | 8 | const compPrefix = `${prefix}-text-input`; 9 | 10 | export interface FuiTextInputProps { 11 | value: string 12 | onChange: (value: string) => void 13 | 14 | clearable?: boolean 15 | disabled?: boolean 16 | label?: string 17 | placeholder?: string 18 | status?: FuiStatusMessageProps 19 | prefix?: JSX.Element 20 | suffix?: JSX.Element 21 | 22 | ariaLabel?: string 23 | className?: string 24 | } 25 | 26 | export const FuiTextInput = (props: FuiTextInputProps) => { 27 | const [isActive, setIsActive] = React.useState(false); 28 | const inputId = React.useMemo(() => `${compPrefix}-input-${Math.random().toString(36).substring(7)}`, []); 29 | const labelId = React.useMemo(() => `${compPrefix}-label-${Math.random().toString(36).substring(7)}`, []); 30 | 31 | const clearValue = React.useCallback(() => { 32 | props.onChange(''); 33 | }, [props.onChange]); 34 | 35 | const inputContainerClassNames = classnames( 36 | `${compPrefix}-input-container`, 37 | { 38 | [`${prefix}-disabled`]: props.disabled, 39 | [`${prefix}-interactable`]: !props.disabled && !isActive, 40 | [`has-prefix`]: !!props.prefix, 41 | [`has-suffix`]: !!props.suffix, 42 | } 43 | ); 44 | 45 | 46 | return ( 47 |
48 | {props.label ? : null} 49 | {props.status ? : null} 50 |
51 | {props.prefix ?
{props.prefix}
: null} 52 |
53 | { props.onChange(e.target.value); }} 60 | placeholder={props.placeholder} 61 | disabled={props.disabled} 62 | className={`${compPrefix}-input`} 63 | onFocus={() => { setIsActive(true); }} 64 | onBlur={() => { setIsActive(false); }} 65 | /> 66 |
67 | {props.clearable ? } className={`${compPrefix}-clearable-icon-container`} /> : null} 68 | {props.suffix ?
{props.suffix}
: null} 69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/fui-toggle/fui-toggle.css: -------------------------------------------------------------------------------- 1 | .fui-toggle { 2 | padding: 0; 3 | margin: 0; 4 | display: flex; 5 | width: fit-content; 6 | align-items: center; 7 | gap: var(--fui-space-xs); 8 | border-radius: var(--fui-radius-md); 9 | border: 0; 10 | background: var(--fui-disableable-color-background-subdued); 11 | position: relative; 12 | } 13 | 14 | .fui-toggle-option:not(.checked) .fui-toggle-label { 15 | color: var(--fui-disableable-color-foreground-soft); 16 | } 17 | 18 | .fui-toggle-size-medium { 19 | outline: 2px solid var(--fui-disableable-color-background-subdued); 20 | height: var(--fui-control-height-md); 21 | } 22 | 23 | .fui-toggle-size-large { 24 | outline: 3px solid var(--fui-disableable-color-background-subdued); 25 | height: var(--fui-control-height-lg); 26 | } 27 | 28 | .fui-toggle-input { 29 | display: none; 30 | } 31 | 32 | .fui-toggle-option-wrapper { 33 | height: 100%; 34 | } 35 | 36 | .fui-toggle-option { 37 | display: flex; 38 | min-width: var(--fui-control-height-lg); 39 | padding: 0 var(--fui-space-lg); 40 | justify-content: center; 41 | align-items: center; 42 | gap: var(--fui-space-sm); 43 | align-self: stretch; 44 | border-radius: var(--fui-radius-md); 45 | position: relative; 46 | overflow: hidden; 47 | height: 100%; 48 | font: var(--running-small-bold); 49 | color: var(--fui-disableable-color-foreground-default); 50 | } 51 | 52 | .fui-toggle-size-medium .fui-toggle-option { 53 | padding: var(--fui-space-sm) var(--fui-space-md); 54 | } 55 | 56 | .fui-toggle-option.checked:after { 57 | display: none; 58 | } 59 | 60 | .fui-toggle-active-bg { 61 | background-color: var(--fui-disableable-color-background-base); 62 | box-shadow: var(--fui-shadow-soft-elevation-0); 63 | border-radius: var(--fui-radius-md); 64 | height: 100%; 65 | top: 50%; 66 | transform: translateY(-50%); 67 | position: absolute; 68 | transition: left 100ms ease-in; 69 | } -------------------------------------------------------------------------------- /src/components/fui-toggle/fui-toggle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiToggle } from './fui-toggle'; 5 | import FuiIconPlaceholder16X16 from '../../icons/fui-icon-placeholder-16x16'; 6 | 7 | const meta = { 8 | title: ' Components/Toggle', 9 | component: FuiToggle, 10 | tags: ['autodocs'], 11 | parameters: { 12 | design: { 13 | type: 'figma', 14 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2541-41544&mode=design&t=jq0JgMhh6dwhuYIm-4' 15 | }, 16 | docs: { 17 | description: { 18 | component: 'Help people navigate between mutually exclusive panes of content in the same view or toggle between two or more configurations.' 19 | } 20 | } 21 | }, 22 | argTypes: { 23 | size: { 24 | control: 'select', 25 | name: '🔗 size' 26 | }, 27 | disabled: { 28 | name: '🔗 disabled' 29 | }, 30 | options: { 31 | control: { 32 | disable: true 33 | } 34 | }, 35 | className: { 36 | control: { 37 | disable: true 38 | } 39 | }, 40 | ariaLabel: { 41 | control: { 42 | disable: true 43 | } 44 | }, 45 | value: { 46 | control: { 47 | disable: true 48 | } 49 | } 50 | } 51 | } satisfies Meta; 52 | 53 | export default meta; 54 | type Story = StoryObj; 55 | 56 | export const Default: Story = { 57 | render: (args) => { 58 | const [value, setValue] = React.useState(args.value); 59 | return ( 60 | <> 61 | 62 | 63 | ); 64 | }, 65 | name: 'Basic', 66 | args: { 67 | options: [ 68 | { label: 'Label', value: '1' }, 69 | { label: 'Label', value: '2' }, 70 | { label: 'Label', value: '3' } 71 | ], 72 | value: '1' 73 | } 74 | }; 75 | 76 | export const Disabled: Story = { 77 | render: (args) => { 78 | const [value, setValue] = React.useState(args.value); 79 | return ( 80 | <> 81 | 82 | 83 | 84 | ); 85 | }, 86 | argTypes: { 87 | disabled: { 88 | table: { 89 | disable: true 90 | } 91 | } 92 | }, 93 | parameters: { 94 | design: { 95 | type: 'figma', 96 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2541-43091&mode=design&t=jq0JgMhh6dwhuYIm-4' 97 | } 98 | }, 99 | args: { 100 | options: [ 101 | { label: 'Option 1', value: '1' }, 102 | { label: 'Option 2', value: '2', disabled: true }, 103 | { label: 'Option 3', value: '3', disabled: true }, 104 | { label: 'Option 4', value: '4' } 105 | ], 106 | disabled: true, 107 | value: '1' 108 | } 109 | }; 110 | 111 | export const ToggleSizes: Story = { 112 | render: (args) => { 113 | const [value, setValue] = React.useState(args.value); 114 | return ( 115 |
116 | 117 | 118 |
119 | ); 120 | }, 121 | parameters: { 122 | design: { 123 | type: 'figma', 124 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2541-41547&mode=design&t=jq0JgMhh6dwhuYIm-4' 125 | } 126 | }, 127 | argTypes: { 128 | size: { 129 | table: { 130 | disable: true 131 | } 132 | } 133 | }, 134 | args: { 135 | options: [ 136 | { label: 'Label', value: '7' }, 137 | { label: 'Label', value: '8' }, 138 | { label: 'Label', value: '9' } 139 | ], 140 | value: '7' 141 | } 142 | }; 143 | 144 | export const PrefixAndSuffix: Story = { 145 | render: (args) => { 146 | const [value, setValue] = React.useState(args.value); 147 | return ( 148 | <> 149 | 150 | 151 | ); 152 | }, 153 | argTypes: { 154 | size: { 155 | table: { 156 | disable: true 157 | } 158 | } 159 | }, 160 | args: { 161 | options: [ 162 | { label: 'Label', value: '4', prefix: , suffix: }, 163 | { label: 'Label', value: '5', prefix: , suffix: , disabled: true }, 164 | { label: 'Label', value: '6', prefix: , suffix: } 165 | ], 166 | value: '4' 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/components/fui-toggle/fui-toggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef, useState } from 'react'; 2 | import { prefix } from '../prefix'; 3 | import classnames from 'classnames'; 4 | 5 | const compPrefix = `${prefix}-toggle`; 6 | 7 | export interface ToggleSegment { 8 | label: string 9 | value: string 10 | disabled?: boolean 11 | prefix?: JSX.Element 12 | suffix?: JSX.Element 13 | } 14 | 15 | export interface FuiToggleProps { 16 | size?: 'large' | 'medium' 17 | options: ToggleSegment[] 18 | value: string 19 | onChange: (value: string) => void 20 | disabled?: boolean 21 | 22 | className?: string 23 | ariaLabel?: string 24 | } 25 | 26 | export const FuiToggle = (props: FuiToggleProps) => { 27 | const ref = useRef(null); 28 | const [activeTabLeftLocation, setActiveTabLeftLocation] = useState(0); 29 | const [activeTabWidth, setactiveTabWidth] = useState(0); 30 | 31 | useLayoutEffect(() => { 32 | if (ref.current) { 33 | setactiveTabWidth(ref.current.offsetWidth); 34 | setActiveTabLeftLocation(ref.current.offsetLeft); 35 | } 36 | }, [props.value, props.size]); 37 | 38 | const classNames = classnames( 39 | compPrefix, 40 | `${compPrefix}-size-${props.size}`, 41 | props.disabled ? `${prefix}-disabled` : '', 42 | props.className 43 | ); 44 | 45 | return ( 46 |
47 |
48 | {props.options.map((option) => { 49 | const onChange = () => { 50 | if (props.disabled) return; 51 | props.onChange(option.value); 52 | }; 53 | const checked = option.value === props.value; 54 | 55 | const onSpace = (e: React.KeyboardEvent) => { 56 | if (e.key === ' ') { 57 | e.preventDefault(); 58 | onChange(); 59 | } 60 | }; 61 | 62 | return ( 63 |
64 | 73 | 78 |
79 | ); 80 | })} 81 |
82 | ); 83 | }; 84 | 85 | FuiToggle.defaultProps = { 86 | size: 'medium' as const 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/fui-tooltip/fui-tooltip.css: -------------------------------------------------------------------------------- 1 | .fui-tooltip { 2 | background-color: var(--fui-color-background-contrast); 3 | border-radius: var(--fui-radius-md); 4 | box-shadow: var(--fui-shadow-soft-elevation-0); 5 | color: var(--fui-color-foreground-inverted); 6 | font: var(--running-medium-bold); 7 | padding: var(--fui-space-lg); 8 | margin-bottom: var(--fui-space-lg); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/fui-tooltip/fui-tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FuiTooltip, type FuiTooltipProps } from './fui-tooltip'; 5 | import { FuiRadio } from '../fui-radio/fui-radio'; 6 | import { FuiButton } from '../fui-button/fui-button'; 7 | 8 | const meta = { 9 | title: ' Components/Tooltip', 10 | component: FuiTooltip, 11 | tags: ['autodocs'], 12 | parameters: { 13 | design: { 14 | type: 'figma', 15 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2542-48551&mode=design&t=jq0JgMhh6dwhuYIm-4' 16 | }, 17 | docs: { 18 | description: { 19 | component: 'Offer concise feedback to inform people about the outcomes of actions or provide brief information about interface components when their cursor interacts with them. Prioritize the specific control, use action-oriented language, and keep the messages brief. Tooltips visually stand out by using contrasting colors with the theme interface.' 20 | } 21 | } 22 | }, 23 | argTypes: { 24 | tooltipBody: { 25 | control: 'text', 26 | name: '🔗 tooltipBody' 27 | }, 28 | children: { 29 | control: { 30 | disable: 'true' 31 | } 32 | }, 33 | ariaLabel: { 34 | control: { 35 | disable: true 36 | } 37 | }, 38 | className: { 39 | control: { 40 | disable: true 41 | } 42 | } 43 | } 44 | } satisfies Meta; 45 | 46 | export default meta; 47 | type Story = StoryObj; 48 | 49 | export const Default: Story = { 50 | render: (args) => { 51 | return ( 52 |
53 | 54 |
55 | ); 56 | }, 57 | name: 'Basic', 58 | args: { 59 | tooltipBody: 'Tooltip text', 60 | children: , 61 | } 62 | }; 63 | 64 | export const Placement: Story = { 65 | render: (args) => { 66 | const [placement, setPlacement] = React.useState('top'); 67 | 68 | return ( 69 |
70 |
71 | { setPlacement('top'); }} checked={placement === 'top'} /> 72 | { setPlacement('bottom'); }} checked={placement === 'bottom'} /> 73 | { setPlacement('left'); }} checked={placement === 'left'} /> 74 | { setPlacement('right'); }} checked={placement === 'right'} /> 75 |
76 | 77 |
78 | ); 79 | }, 80 | argTypes: { 81 | placement: { 82 | table: { 83 | disable: true 84 | } 85 | } 86 | }, 87 | args: { 88 | tooltipBody: 'hello there you handsome', 89 | children: , 90 | } 91 | }; 92 | 93 | export const OpenAndCloseDelay: Story = { 94 | render: (args) => { 95 | return ( 96 |
97 | 98 |
99 | ); 100 | }, 101 | argTypes: { 102 | closeDelay: { 103 | table: { 104 | disable: true 105 | } 106 | }, 107 | openDelay: { 108 | table: { 109 | disable: true 110 | } 111 | } 112 | }, 113 | args: { 114 | tooltipBody: 'hello there you handsome', 115 | children: , 116 | closeDelay: 500, 117 | openDelay: 500 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /src/components/fui-tooltip/fui-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../prefix'; 3 | import { type Placement, useFloating, useHover, useInteractions, offset, FloatingPortal, useTransitionStyles } from '@floating-ui/react'; 4 | import classnames from 'classnames'; 5 | 6 | const compPrefix = `${prefix}-tooltip`; 7 | 8 | export interface FuiTooltipProps { 9 | children: React.ReactNode 10 | tooltipBody: React.ReactNode 11 | placement?: Placement 12 | openDelay?: number 13 | closeDelay?: number 14 | 15 | ariaLabel?: string 16 | className?: string 17 | } 18 | 19 | export const FuiTooltip = (props: FuiTooltipProps) => { 20 | const [isOpen, setIsOpen] = React.useState(false); 21 | let timeout: any; 22 | 23 | const onOpenChange = React.useCallback((nextOpen: boolean) => { 24 | if (timeout) clearTimeout(timeout); 25 | if (nextOpen) { 26 | if (props.closeDelay) { 27 | timeout = setTimeout(() => { 28 | setIsOpen(nextOpen); 29 | }, props.openDelay); 30 | } else { 31 | setIsOpen(nextOpen); 32 | } 33 | } else { 34 | if (props.closeDelay) { 35 | timeout = setTimeout(() => { 36 | setIsOpen(nextOpen); 37 | }, props.closeDelay); 38 | } else { 39 | setIsOpen(nextOpen); 40 | } 41 | } 42 | }, [props.openDelay, props.closeDelay, setIsOpen]); 43 | 44 | const { refs, floatingStyles, context } = useFloating({ 45 | open: isOpen, 46 | onOpenChange, 47 | placement: props.placement, 48 | middleware: [offset(7)] 49 | }); 50 | 51 | const { styles } = useTransitionStyles(context); 52 | const hover = useHover(context); 53 | 54 | const { getReferenceProps, getFloatingProps } = useInteractions([ 55 | hover 56 | ]); 57 | 58 | const classNames = classnames( 59 | compPrefix, 60 | props.className 61 | ); 62 | 63 | return ( 64 | <> 65 | 66 | {props.children} 67 | 68 | {isOpen && ( 69 | 70 |
71 | {props.tooltipBody} 72 |
73 |
74 | )} 75 | 76 | ); 77 | }; 78 | 79 | FuiTooltip.defaultProps = { 80 | placement: 'top' as const 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/prefix.ts: -------------------------------------------------------------------------------- 1 | export const prefix = 'fui'; 2 | -------------------------------------------------------------------------------- /src/css/disableable-colors.css: -------------------------------------------------------------------------------- 1 | [class*="fui"] { 2 | --fui-disableable-color-background-base: var(--fui-color-background-base); 3 | --fui-disableable-color-background-elevated: var(--fui-color-background-elevated); 4 | --fui-disableable-color-background-subdued: var(--fui-color-background-subdued); 5 | --fui-disableable-color-background-white: var(--fui-color-background-white); 6 | --fui-disableable-color-button-primary-bg-destructive: var(--fui-color-button-primary-bg-destructive); 7 | --fui-disableable-color-button-primary-bg-neutral: var(--fui-color-brand); 8 | --fui-disableable-color-button-primary-bg-success: var(--fui-color-button-primary-bg-success); 9 | --fui-disableable-color-button-secondary-bg-destructive: var(--fui-color-button-secondary-bg-destructive); 10 | --fui-disableable-color-button-secondary-bg-neutral: var(--fui-color-button-secondary-bg-neutral); 11 | --fui-disableable-color-button-secondary-bg-success: var(--fui-color-button-secondary-bg-success); 12 | --fui-disableable-color-button-secondary-border-destructive: var(--fui-color-button-secondary-border-destructive); 13 | --fui-disableable-color-button-secondary-border-neutral: var(--fui-color-button-secondary-border-neutral); 14 | --fui-disableable-color-button-secondary-border-success: var(--fui-color-button-secondary-border-success); 15 | --fui-disableable-color-divider-soft: var(--fui-color-divider-soft); 16 | --fui-disableable-color-divider-solid: var(--fui-color-divider-solid); 17 | --fui-disableable-color-state-active: var(--fui-color-state-active); 18 | --fui-disableable-color-state-hover: var(--fui-color-state-hover); 19 | --fui-disableable-color-foreground-danger: var(--fui-color-status-danger); 20 | --fui-disableable-color-foreground-default: var(--fui-color-foreground-default); 21 | --fui-disableable-color-foreground-light: var(--fui-color-foreground-light); 22 | --fui-disableable-color-foreground-primary: var(--fui-color-foreground-primary); 23 | --fui-disableable-color-foreground-soft: var(--fui-color-foreground-soft); 24 | --fui-disableable-color-foreground-softest: var(--fui-color-foreground-softest); 25 | --fui-disableable-color-foreground-success: var(--fui-color-status-success); 26 | --fui-disableable-color-icon: var(--fui-color-icon); 27 | } 28 | 29 | [class*="fui"].fui-disabled, 30 | [class*="fui"].fui-disabled * { 31 | --fui-disableable-color-background-base: var(--fui-color-disabled-bg); 32 | --fui-disableable-color-background-elevated: var(--fui-color-disabled-bg); 33 | --fui-disableable-color-background-subdued: var(--fui-color-disabled-bg); 34 | --fui-disableable-color-background-white: var(--fui-color-disabled-bg); 35 | --fui-disableable-color-button-primary-bg-destructive: var(--fui-color-disabled-bg); 36 | --fui-disableable-color-button-primary-bg-neutral: var(--fui-color-disabled-bg); 37 | --fui-disableable-color-button-primary-bg-success: var(--fui-color-disabled-bg); 38 | --fui-disableable-color-button-secondary-bg-destructive: var(--fui-color-disabled-bg); 39 | --fui-disableable-color-button-secondary-bg-neutral: var(--fui-color-disabled-bg); 40 | --fui-disableable-color-button-secondary-bg-success: var(--fui-color-disabled-bg); 41 | --fui-disableable-color-button-secondary-border-destructive: #00000000; 42 | --fui-disableable-color-button-secondary-border-neutral: #00000000; 43 | --fui-disableable-color-button-secondary-border-success: #00000000; 44 | --fui-disableable-color-divider-soft: var(--fui-color-disabled-divider); 45 | --fui-disableable-color-divider-solid: var(--fui-color-disabled-divider); 46 | --fui-disableable-color-state-active: #FFFFFF00; 47 | --fui-disableable-color-state-hover: #FFFFFF00; 48 | --fui-disableable-color-foreground-danger: var(--fui-color-disabled-text); 49 | --fui-disableable-color-foreground-default: var(--fui-color-disabled-text); 50 | --fui-disableable-color-foreground-light: var(--fui-color-disabled-text); 51 | --fui-disableable-color-foreground-primary: var(--fui-color-disabled-text); 52 | --fui-disableable-color-foreground-soft: var(--fui-color-disabled-text); 53 | --fui-disableable-color-foreground-softest: var(--fui-color-disabled-text); 54 | --fui-disableable-color-foreground-success: var(--fui-color-disabled-text); 55 | --fui-disableable-color-icon: var(--fui-color-disabled-text); 56 | } -------------------------------------------------------------------------------- /src/css/effects.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fui-shadow-soft-elevation-0: 0px 0px 5px 0px rgba(0, 0, 0, 0.05); 3 | --fui-shadow-soft-elevation-1: 0px 0px 10px -2px rgba(0, 0, 0, 0.15); 4 | --fui-shadow-soft-elevation-2: 0px 0px 0px 0px rgba(0, 0, 0, 0.10), 0px 11px 24px 0px rgba(0, 0, 0, 0.10), 0px 44px 44px 0px rgba(0, 0, 0, 0.09), 0px 99px 59px 0px rgba(0, 0, 0, 0.05), 0px 176px 70px 0px rgba(0, 0, 0, 0.01), 0px 275px 77px 0px rgba(0, 0, 0, 0.00); 5 | --fui-shadow-crisp-down: 0px 2px 0px 0px rgba(0, 0, 0, 0.02); 6 | } -------------------------------------------------------------------------------- /src/css/global-colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fui-color-blue-50: #F0F4FF; 3 | --fui-color-blue-100: #E5EBFE; 4 | --fui-color-blue-200: #CDD9FE; 5 | --fui-color-blue-300: #91ACFD; 6 | --fui-color-blue-650: #6187FA; 7 | --fui-color-blue-750: #3D69F2; 8 | --fui-color-blue-800: #2856E0; 9 | --fui-color-blue-850: #1D4BD6; 10 | --fui-color-blue-950: #152E7A; 11 | --fui-color-blue-1000: #111F49; 12 | 13 | --fui-color-brown-700: #DB850B; 14 | --fui-color-brown-800: #B26C09; 15 | --fui-color-brown-850: #965E0F; 16 | --fui-color-brown-900: #80500D; 17 | --fui-color-brown-950: #66420F; 18 | 19 | --fui-color-green-50: #EDFDF6; 20 | --fui-color-green-100: #D1FAE9; 21 | --fui-color-green-200: #A5F3D2; 22 | --fui-color-green-300: #6EE7B5; 23 | --fui-color-green-600: #36D392; 24 | --fui-color-green-700: #08A464; 25 | --fui-color-green-800: #088752; 26 | --fui-color-green-850: #047244; 27 | --fui-color-green-900: #06603A; 28 | --fui-color-green-950: #064C2F; 29 | 30 | --fui-color-neutral-50: #F5F6F9; 31 | --fui-color-neutral-100: #E9ECF5; 32 | --fui-color-neutral-200: #DFE2EB; 33 | --fui-color-neutral-350: #BDBFC7; 34 | --fui-color-neutral-750: #5C5D61; 35 | --fui-color-neutral-800: #47484D; 36 | --fui-color-neutral-850: #38393D; 37 | --fui-color-neutral-900: #2F2F33; 38 | --fui-color-neutral-950: #171719; 39 | --fui-color-neutral-1000: #0B0B0D; 40 | 41 | --fui-color-purple-50: #F7F5FF; 42 | --fui-color-purple-100: #EDE7FE; 43 | --fui-color-purple-200: #E1D7FE; 44 | --fui-color-purple-300: #C6B4FD; 45 | --fui-color-purple-700: #A689FA; 46 | --fui-color-purple-750: #9370FA; 47 | --fui-color-purple-800: #714CE0; 48 | --fui-color-purple-850: #5F35DE; 49 | --fui-color-purple-900: #4621B5; 50 | --fui-color-purple-950: #3B1D95; 51 | 52 | --fui-color-red-50: #FEF1F1; 53 | --fui-color-red-100: #FEE1E1; 54 | --fui-color-red-200: #FEC8C8; 55 | --fui-color-red-300: #FCA6A6; 56 | --fui-color-red-650: #F87272; 57 | --fui-color-red-750: #EF4343; 58 | --fui-color-red-800: #E02D3C; 59 | --fui-color-red-850: #BA2532; 60 | --fui-color-red-900: #981B25; 61 | --fui-color-red-950: #680D14; 62 | 63 | --fui-color-yellow-50: #FFFAEB; 64 | --fui-color-yellow-100: #FFF5D6; 65 | --fui-color-yellow-200: #FEE9A9; 66 | --fui-color-yellow-300: #FDDA72; 67 | --fui-color-yellow-550: #FBCB3C; 68 | } -------------------------------------------------------------------------------- /src/css/interactable.css: -------------------------------------------------------------------------------- 1 | .fui-interactable, 2 | .fui-interactable * { 3 | cursor: pointer; 4 | position: relative; 5 | } 6 | 7 | .fui-disabled *, 8 | .fui-disabled { 9 | cursor: not-allowed; 10 | } 11 | 12 | [class*="fui"].fui-interactable::after { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | right: 0; 17 | bottom: 0; 18 | content: ""; 19 | background-color: transparent; 20 | pointer-events: none; 21 | } 22 | 23 | [class*="fui"].fui-interactable:hover::after { 24 | background-color: var(--fui-color-state-hover); 25 | } 26 | 27 | [class*="fui"].fui-interactable:active::after { 28 | background-color: var(--fui-color-state-active); 29 | } 30 | 31 | [class*="fui"]:focus-visible { 32 | outline: 2px solid var(--fui-color-state-focus); 33 | outline-offset: 0.1em; 34 | border-radius: var(--fui-radius-sm); 35 | } 36 | 37 | .fui-disabled .fui-interactable::after, 38 | .fui-disabled.fui-interactable::after { 39 | display: none; 40 | } -------------------------------------------------------------------------------- /src/css/layout.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fui-control-height-lg: 44px; 3 | --fui-control-height-md: 34px; 4 | --fui-control-height-sm: 26px; 5 | --fui-radius-lg: 8px; 6 | --fui-radius-md: 6px; 7 | --fui-radius-round: 1000px; 8 | --fui-radius-xlg: 10px; 9 | --fui-radius-sm: 4px; 10 | --fui-radius-xxlg: 12px; 11 | --fui-radius-xs: 2px; 12 | --fui-space-lg: 12px; 13 | --fui-space-md: 8px; 14 | --fui-space-sm: 6px; 15 | --fui-space-xlg: 16px; 16 | --fui-space-xs: 4px; 17 | --fui-space-xxlg: 20px; 18 | --fui-space-xxs: 2px; 19 | --select-min-width: 150px; 20 | --popover-min-width: var(--select-min-width); 21 | --fui-zindex-top: 3; 22 | --fui-zindex-middle: 2; 23 | --fui-zindex-bottom: 1; 24 | --fui-zindex-under: -1; 25 | } -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap'); 3 | @import url('./global-colors.css'); 4 | @import url('./layout.css'); 5 | @import url('./shape.css'); 6 | @import url('./theme-colors.css'); 7 | @import url('./disableable-colors.css'); 8 | @import url('./text-styles.css'); 9 | @import url('./interactable.css'); 10 | @import url('./effects.css'); 11 | 12 | @import url('../components/fui-button/fui-button.css'); 13 | @import url('../components/fui-badge/fui-badge.css'); 14 | @import url('../components/fui-empty/fui-empty.css'); 15 | @import url('../components/fui-status-message/fui-status-message.css'); 16 | @import url('../components/fui-popover/fui-popover.css'); 17 | @import url('../components/fui-select/fui-select.css'); 18 | @import url('../components/fui-checkbox/fui-checkbox.css'); 19 | @import url('../components/fui-radio/fui-radio.css'); 20 | @import url('../components/fui-tooltip/fui-tooltip.css'); 21 | @import url('../components/fui-text-input/fui-text-input.css'); 22 | @import url('../components/fui-switch/fui-switch.css'); 23 | @import url('../components/fui-stepper/fui-stepper.css'); 24 | @import url('../components/fui-toggle/fui-toggle.css'); 25 | @import url('../components/fui-notification/fui-notification.css'); 26 | @import url('../components/fui-modal/fui-modal.css'); 27 | @import url('../components/fui-option-group/fui-option-group.css'); 28 | 29 | /* Box sizing rules */ 30 | [class*="fui"], 31 | [class*="fui"]::before, 32 | [class*="fui"]::after { 33 | box-sizing: border-box; 34 | } 35 | 36 | /* Remove default margin */ 37 | h1[class*="fui"], 38 | h2[class*="fui"], 39 | h3[class*="fui"], 40 | h4[class*="fui"], 41 | p[class*="fui"], 42 | figure[class*="fui"], 43 | blockquote[class*="fui"], 44 | dl[class*="fui"], 45 | dd[class*="fui"] { 46 | margin: 0; 47 | } 48 | 49 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ 50 | ul[role='list'][class*="fui"], 51 | ol[role='list'][class*="fui"] { 52 | list-style: none; 53 | } 54 | 55 | /* Nicely drawn underlines on links */ 56 | a[class*="fui"] { 57 | text-decoration-skip-ink: auto; 58 | } 59 | 60 | /* Inherit fonts for inputs and buttons */ 61 | [class*="fui"], 62 | input[class*="fui"], 63 | button[class*="fui"], 64 | textarea[class*="fui"], 65 | select[class*="fui"] { 66 | font-family: 'Lato', sans-serif; 67 | } 68 | 69 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ 70 | @media (prefers-reduced-motion: reduce) { 71 | 72 | [class*="fui"], 73 | [class*="fui"]::before, 74 | [class*="fui"]::after { 75 | animation-duration: 0.01ms !important; 76 | animation-iteration-count: 1 !important; 77 | transition-duration: 0.01ms !important; 78 | scroll-behavior: auto !important; 79 | } 80 | } 81 | 82 | /* Other general stuff */ 83 | [class*="fui-icon"] { 84 | display: block; 85 | color: var(--fui-disableable-color-icon); 86 | } -------------------------------------------------------------------------------- /src/css/shape.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fui-border-radius-round: 1000px; 3 | --fui-border-radius-xxlg: 12px; 4 | --fui-border-radius-xlg: 10px; 5 | --fui-border-radius-lg: 8px; 6 | --fui-border-radius-md: 6px; 7 | --fui-border-radius-sm: 4px; 8 | --fui-border-radius-xs: 2px; 9 | } -------------------------------------------------------------------------------- /src/css/text-styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fui-font-family: 'Lato', sans-serif; 3 | --h1-bold: 700 44px/48px var(--fui-font-family); 4 | --h2-bold: 700 38px/42px var(--fui-font-family); 5 | --h3-bold: 700 32px/36px var(--fui-font-family); 6 | --h4-bold: 700 28px/33px var(--fui-font-family); 7 | --h5-bold: 700 24px/28px var(--fui-font-family); 8 | --h6-bold: 700 18px/22px var(--fui-font-family); 9 | --h1-regular: 44px/48px var(--fui-font-family); 10 | --h2-regular: 38px/42px var(--fui-font-family); 11 | --h3-regular: 32px/36px var(--fui-font-family); 12 | --h4-regular: 28px/33px var(--fui-font-family); 13 | --h5-regular: 24px/29px var(--fui-font-family); 14 | --h6-regular: 20px/24px var(--fui-font-family); 15 | --running-medium-regular: 16px/25px var(--fui-font-family); 16 | --running-medium-bold: 700 16px/26px var(--fui-font-family); 17 | --running-large-regular: 18px/28px var(--fui-font-family); 18 | --running-large-bold: 700 18px/29px var(--fui-font-family); 19 | --running-small-regular: 14px/22px var(--fui-font-family); 20 | --running-small-bold: 700 14px/24px var(--fui-font-family); 21 | } -------------------------------------------------------------------------------- /src/css/theme-colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fui-color-background-base: #FFFFFFFF; 3 | --fui-color-background-contrast: var(--fui-color-neutral-950); 4 | --fui-color-background-elevated: #FFFFFFFF; 5 | --fui-color-background-opaque: #00000019; 6 | --fui-color-background-subdued: var(--fui-color-neutral-100); 7 | --fui-color-background-white: #FFFFFFFF; 8 | --fui-color-badge-blue-bg: var(--fui-color-blue-100); 9 | --fui-color-badge-blue-border: var(--fui-color-blue-300); 10 | --fui-color-badge-blue-text: var(--fui-color-blue-750); 11 | --fui-color-badge-green-bg: var(--fui-color-green-100); 12 | --fui-color-badge-green-border: var(--fui-color-green-300); 13 | --fui-color-badge-green-text: var(--fui-color-green-700); 14 | --fui-color-badge-neutral-bg: var(--fui-color-neutral-100); 15 | --fui-color-badge-neutral-border: var(--fui-color-neutral-350); 16 | --fui-color-badge-neutral-text: var(--fui-color-neutral-750); 17 | --fui-color-badge-purple-bg: var(--fui-color-purple-100); 18 | --fui-color-badge-purple-border: var(--fui-color-purple-300); 19 | --fui-color-badge-purple-text: var(--fui-color-purple-750); 20 | --fui-color-badge-red-bg: var(--fui-color-red-100); 21 | --fui-color-badge-red-border: var(--fui-color-red-300); 22 | --fui-color-badge-red-text: var(--fui-color-red-750); 23 | --fui-color-badge-yellow-bg: var(--fui-color-yellow-100); 24 | --fui-color-badge-yellow-border: var(--fui-color-yellow-300); 25 | --fui-color-badge-yellow-text: var(--fui-color-brown-700); 26 | --fui-color-button-secondary-bg-destructive: var(--fui-color-red-50); 27 | --fui-color-button-secondary-bg-neutral: var(--fui-color-blue-50); 28 | --fui-color-button-secondary-bg-success: var(--fui-color-green-50); 29 | --fui-color-button-secondary-border-destructive: var(--fui-color-red-100); 30 | --fui-color-button-secondary-border-neutral: var(--fui-color-blue-100); 31 | --fui-color-button-secondary-border-success: var(--fui-color-green-100); 32 | --fui-color-disabled-bg: var(--fui-color-neutral-100); 33 | --fui-color-disabled-divider: var(--fui-color-neutral-200); 34 | --fui-color-disabled-text: var(--fui-color-neutral-350); 35 | --fui-color-divider-soft: var(--fui-color-neutral-200); 36 | --fui-color-divider-solid: var(--fui-color-neutral-350); 37 | --fui-color-status-attention: var(--fui-color-brown-800); 38 | --fui-color-status-attention-subtle: var(--fui-color-yellow-100); 39 | --fui-color-status-danger: var(--fui-color-red-800); 40 | --fui-color-status-danger-subtle: var(--fui-color-red-100); 41 | --fui-color-status-info: var(--fui-color-purple-800); 42 | --fui-color-status-info-subtle: var(--fui-color-purple-100); 43 | --fui-color-status-success: var(--fui-color-green-800); 44 | --fui-color-status-success-subtle: var(--fui-color-green-100); 45 | --fui-color-state-active: #00000026; 46 | --fui-color-state-focus: var(--fui-color-blue-300); 47 | --fui-color-state-hover: #00000019; 48 | --fui-color-surface-base: #FFFFFFFF; 49 | --fui-color-surface-base-soft: var(--fui-color-neutral-50); 50 | --fui-color-surface-subdued: var(--fui-color-neutral-100); 51 | --fui-color-foreground-default: var(--fui-color-neutral-950); 52 | --fui-color-foreground-inverted: var(--fui-color-neutral-50); 53 | --fui-color-foreground-light: #FFFFFFFF; 54 | --fui-color-foreground-opaque: #0000007F; 55 | --fui-color-foreground-primary: var(--fui-color-brand); 56 | --fui-color-foreground-soft: var(--fui-color-neutral-800); 57 | --fui-color-foreground-softest: var(--fui-color-neutral-350); 58 | --fui-color-brand: var(--fui-color-blue-800); 59 | --fui-color-icon: var(--fui-color-neutral-900); 60 | --fui-color-button-primary-bg-destructive: var(--fui-color-red-800); 61 | --fui-color-button-primary-bg-neutral: var(--fui-color-brand); 62 | --fui-color-button-primary-bg-success: var(--fui-color-green-800); 63 | } 64 | 65 | [data-theme="dark"], 66 | [data-theme="dark"] * { 67 | --fui-color-background-base: var(--fui-color-neutral-900); 68 | --fui-color-background-contrast: var(--fui-color-neutral-50); 69 | --fui-color-background-elevated: var(--fui-color-neutral-850); 70 | --fui-color-background-opaque: #FFFFFF19; 71 | --fui-color-background-subdued: var(--fui-color-neutral-1000); 72 | --fui-color-background-white: #FFFFFF; 73 | --fui-color-badge-blue-bg: var(--fui-color-blue-1000); 74 | --fui-color-badge-blue-border: var(--fui-color-blue-800); 75 | --fui-color-badge-blue-text: var(--fui-color-blue-650); 76 | --fui-color-badge-green-bg: var(--fui-color-green-950); 77 | --fui-color-badge-green-border: var(--fui-color-green-800); 78 | --fui-color-badge-green-text: var(--fui-color-green-600); 79 | --fui-color-badge-neutral-bg: var(--fui-color-neutral-900); 80 | --fui-color-badge-neutral-border: var(--fui-color-neutral-800); 81 | --fui-color-badge-neutral-text: var(--fui-color-neutral-350); 82 | --fui-color-badge-purple-bg: var(--fui-color-purple-1000); 83 | --fui-color-badge-purple-border: var(--fui-color-purple-800); 84 | --fui-color-badge-purple-text: var(--fui-color-purple-700); 85 | --fui-color-badge-red-bg: var(--fui-color-red-950); 86 | --fui-color-badge-red-border: var(--fui-color-red-800); 87 | --fui-color-badge-red-text: var(--fui-color-red-650); 88 | --fui-color-badge-yellow-bg: var(--fui-color-brown-950); 89 | --fui-color-badge-yellow-border: var(--fui-color-brown-800); 90 | --fui-color-badge-yellow-text: var(--fui-color-yellow-550); 91 | --fui-color-divider-soft: var(--fui-color-neutral-800); 92 | --fui-color-divider-solid: var(--fui-color-neutral-750); 93 | --fui-color-button-secondary-bg-destructive: var(--fui-color-background-elevated); 94 | --fui-color-button-secondary-bg-neutral: var(--fui-color-background-elevated); 95 | --fui-color-button-secondary-bg-success: var(--fui-color-background-elevated); 96 | --fui-color-button-secondary-border-destructive: var(--fui-color-divider-soft); 97 | --fui-color-button-secondary-border-neutral: var(--fui-color-divider-soft); 98 | --fui-color-button-secondary-border-success: var(--fui-color-divider-soft); 99 | --fui-color-disabled-bg: var(--fui-color-neutral-850); 100 | --fui-color-disabled-divider: var(--fui-color-neutral-800); 101 | --fui-color-disabled-text: var(--fui-color-neutral-750); 102 | --fui-color-status-attention: var(--fui-color-brown-700); 103 | --fui-color-status-attention-subtle: var(--fui-color-brown-950); 104 | --fui-color-status-danger: var(--fui-color-red-750); 105 | --fui-color-status-danger-subtle: var(--fui-color-red-950); 106 | --fui-color-status-info: var(--fui-color-purple-750); 107 | --fui-color-status-info-subtle: var(--fui-color-purple-1000); 108 | --fui-color-status-success: var(--fui-color-green-700); 109 | --fui-color-status-success-subtle: var(--fui-color-green-950); 110 | --fui-color-state-active: #FFFFFF26; 111 | --fui-color-state-focus: var(--fui-color-blue-600); 112 | --fui-color-state-hover: #FFFFFF19; 113 | --fui-color-surface-base: var(--fui-color-neutral-900); 114 | --fui-color-surface-base-soft: var(--fui-color-neutral-950); 115 | --fui-color-surface-subdued: var(--fui-color-neutral-1000); 116 | --fui-color-brand: var(--fui-color-blue-650); 117 | --fui-color-icon: var(--fui-color-neutral-50); 118 | --fui-color-button-primary-bg-destructive: var(--fui-color-red-750); 119 | --fui-color-button-primary-bg-neutral: var(--fui-color-brand); 120 | --fui-color-button-primary-bg-success: var(--fui-color-green-700); 121 | --fui-color-foreground-default: var(--fui-color-neutral-50); 122 | --fui-color-foreground-inverted: var(--fui-color-neutral-950); 123 | --fui-color-foreground-light: var(--fui-color-neutral-50); 124 | --fui-color-foreground-opaque: #FFFFFF7F; 125 | --fui-color-foreground-primary: var(--fui-color-brand); 126 | --fui-color-foreground-soft: var(--fui-color-neutral-350); 127 | --fui-color-foreground-softest: var(--fui-color-neutral-750); 128 | } -------------------------------------------------------------------------------- /src/icons/fui-icon-check-12x12.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconCheck12X12 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-check-16x16.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconCheck16X16 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-check-8x8.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconCheck8X8 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-chevron-down-12x12.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconChevronDown12X12 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-exclamation-mark-16x16.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconExclamationMark16x16 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-exclamation-mark-8x8.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconExclamationMark8x8 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-indeterminate-line-2x10.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconIndeterminateLine2x10 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-minus-12x12.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconMinus12x12 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-minus-8x8.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconMinus8x8 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-placeholder-16x16.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconPlaceholder16X16 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-placeholder-32x32.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconPlaceholder32X32 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-plus-12x12.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { prefix } from '../components/prefix'; 4 | 5 | export default function FuiIconPlus12x12 () { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/icons/fui-icon-plus-8x8.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { prefix } from '../components/prefix'; 4 | 5 | export default function FuiIconPlus8x8 () { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/icons/fui-icon-x-12x12.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconX12x12 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-x-16x16.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconX12x12 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/fui-icon-x-8x8.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { prefix } from '../components/prefix'; 3 | 4 | export default function FuiIconX8x8 () { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/icons.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { StoryObj } from '@storybook/react'; 3 | import FuiIconCheck12X12 from './fui-icon-check-12x12'; 4 | import FuiIconCheck8X8 from './fui-icon-check-8x8'; 5 | import FuiIconChevronDown12X12 from './fui-icon-chevron-down-12x12'; 6 | import FuiIconExclamationMark16x16 from './fui-icon-exclamation-mark-16x16'; 7 | import FuiIconExclamationMark8x8 from './fui-icon-exclamation-mark-8x8'; 8 | import FuiIconMinus8x8 from './fui-icon-minus-8x8'; 9 | import FuiIconPlaceholder16X16 from './fui-icon-placeholder-16x16'; 10 | import FuiIconPlaceholder32X32 from './fui-icon-placeholder-32x32'; 11 | import FuiIconX12x12 from './fui-icon-x-12x12'; 12 | import FuiIconX8x8 from './fui-icon-x-8x8'; 13 | import FuiIconX16x16 from './fui-icon-x-16x16'; 14 | import FuiIconIndeterminateLine2x10 from './fui-icon-indeterminate-line-2x10'; 15 | 16 | const meta = { 17 | title: ' Components/Icons' 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const Check8x8: Story = { render: () => }; 24 | export const Check12x12: Story = { render: () => }; 25 | export const ChevronDown12X12: Story = { render: () => }; 26 | export const ExclamationMark8x8: Story = { render: () => }; 27 | export const ExclamationMark16x16: Story = { render: () => }; 28 | export const Minus8x8: Story = { render: () => }; 29 | export const Placeholder16X16: Story = { render: () => }; 30 | export const Placeholder32X32: Story = { render: () => }; 31 | export const X8x8: Story = { render: () => }; 32 | export const X12x12: Story = { render: () => }; 33 | export const X16x16: Story = { render: () => }; 34 | export const IndeterminateLine2x10: Story = { render: () => }; 35 | -------------------------------------------------------------------------------- /src/illustrations/illustrations.stories.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { type StoryObj } from '@storybook/react'; 4 | import FuiIllustrationCat from './fui-illustration-cat'; 5 | 6 | const meta = { 7 | title: ' Components/Illustrations' 8 | }; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Cat: Story = { render: () => }; 14 | -------------------------------------------------------------------------------- /src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/blocks'; 2 | import { Default as BadgeDefault } from '../components/fui-badge/fui-badge.stories.tsx'; 3 | import { Default as ButtonDefault } from '../components/fui-button/fui-button.stories.tsx'; 4 | import { Default as CheckboxDefault } from '../components/fui-checkbox/fui-checkbox.stories.tsx'; 5 | import { Default as EmptyDefault } from '../components/fui-empty/fui-empty.stories.tsx'; 6 | import { Default as ModalDefault } from '../components/fui-modal/fui-modal.stories.tsx'; 7 | import { Default as NotificationDefault } from '../components/fui-notification/fui-notification.stories.tsx'; 8 | import { Default as PopoverDefault } from '../components/fui-popover/fui-popover.stories.tsx'; 9 | import { Default as RadioDefault } from '../components/fui-radio/fui-radio.stories.tsx'; 10 | import { Default as SelectDefault } from '../components/fui-select/fui-select.stories.tsx'; 11 | import { Success as StatusMessageDefault } from '../components/fui-status-message/fui-status-message.stories.tsx'; 12 | import { Default as StepperDefault } from '../components/fui-stepper/fui-stepper.stories.tsx'; 13 | import { Default as SwitchDefault } from '../components/fui-switch/fui-switch.stories.tsx'; 14 | import { Default as TextInputDefault } from '../components/fui-text-input/fui-text-input.stories.tsx'; 15 | import { Default as ToggleDefault } from '../components/fui-toggle/fui-toggle.stories.tsx'; 16 | import { Default as TooltipDefault } from '../components/fui-tooltip/fui-tooltip.stories.tsx'; 17 | import { Default as OptionGroupDefault } from '../components/fui-option-group/fui-option-group.stories.tsx'; 18 | 19 | 20 | 21 | 48 | 49 |
50 | functional-ui-kit-cover 51 |

Welcome to FUI! let's get started:

52 |

53 | Functional UI Kit is a professionally crafted design system for design & development teams and individuals. We provide core components you would need in every project, focusing on accessibility, development experience and unified designer-developer experience. 54 | 55 | We've made sure that Figma variables and CSS variables work together effortlessly. They share the same names, usage and inheritance structure. This isn't just an extra feature; it's the core approach. 56 | 57 | Each Figma variable has a direct counterpart in CSS, so there's no confusion. Your design ideas stay crystal clear as you move into the development phase. 58 |

59 |
    60 |
  • Website: www.functional-ui-kit.com
  • 61 |
  • Storybook: www.functional-ui.github.io/functional-ui-kit
  • 62 |
  • Github: www.github.com/functional-ui/functional-ui-kit
  • 63 |
  • Npm: www.npmjs.com/package/functional-ui-kit
  • 64 |
65 |
66 |
67 |
68 |
Badge
69 | 70 |
71 |
72 |
Button
73 | 74 |
75 |
76 |
Checkbox
77 | 78 |
79 |
80 |
Empty
81 | 82 |
83 |
84 |
Modal
85 | 86 |
87 |
88 |
Tooltip
89 | 90 |
91 |
92 |
Popover
93 | 94 |
95 |
96 |
Radio
97 | 98 |
99 |
100 |
Select
101 | 102 |
103 |
104 |
StatusMessage
105 | 106 |
107 |
108 |
Stepper
109 | 110 |
111 |
112 |
Switch
113 | 114 |
115 |
116 |
TextInput
117 | 118 |
119 |
120 |
Toggle
121 | 122 |
123 |
124 |
Notification
125 |
126 | 127 |
128 |
129 |
130 |
Option Group
131 | 132 |
133 |
-------------------------------------------------------------------------------- /src/stories/setup.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/blocks'; 2 | import { Default as BadgeDefault } from '../components/fui-badge/fui-badge.stories.tsx'; 3 | 4 | 5 | 6 | 10 | 11 | ### Install 12 | Install the latest version from NPM. 13 | ``` 14 | npm install functional-ui-kit 15 | ``` 16 | 17 | ### Setup CSS 18 | Import the functional-ui-kit CSS file into your project in your css file 19 | ```css 20 | @import 'functional-ui-kit/style'; 21 | 22 | html { 23 | ... 24 | ``` 25 | you can also import the CSS file directly into your main React App file 26 | ```js 27 | import React, { Component } from 'react' 28 | import 'functional-ui-kit/style'; 29 | 30 | class App extends Component { 31 | ... 32 | ``` 33 | 34 | ### Using Components 35 | You can use the components by importing them into your React App file 36 | ```js 37 | import React, { Component } from 'react' 38 | import { FuiBadge } from 'functional-ui-kit/fui-badge'; 39 | 40 | function App() { 41 | return ( 42 |
43 | 44 |
45 | ); 46 | } 47 | ``` -------------------------------------------------------------------------------- /src/stories/theming.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/blocks'; 2 | import { Default as BadgeDefault } from '../components/fui-badge/fui-badge.stories.tsx'; 3 | import { Default as ButtonDefault } from '../components/fui-button/fui-button.stories.tsx'; 4 | 5 | 6 | 7 | 51 | 52 | ### Theming 53 | Theme FUI colors, spacing, text & shape by overriding CSS variables. 54 | Make sure you are overriding the CSS variables only after importing FUI's styles. 55 | The variables operate on an inheritance model, ensuring a cohesive organizational structure. Overriding one variable will have consequences on other variables. 56 | 57 | We recommend concentrating on [Theme Color variables](https://github.com/functional-ui/functional-ui-kit/blob/main/src/css/theme-colors.css), [Global Color variables](https://github.com/functional-ui/functional-ui-kit/blob/main/src/css/global-colors.css) and [Layout variables](https://github.com/functional-ui/functional-ui-kit/blob/main/src/css/layout.css) for effective theming: 58 | 59 | Make sure you apply your style **after Funcional UI Kit's style**, otherwise your overrides will not take affect: 60 | ```css 61 | import 'functional-ui-kit/style'; 62 | import 'your-style'; 63 | ``` 64 | ```css 65 | :root { 66 | --fui-color-badge-blue-bg: var(--fui-color-green-50); 67 | --fui-color-badge-blue-text: var(--fui-color-green-950); 68 | --fui-color-badge-blue-border: var(--fui-color-green-950); 69 | --fui-color-brand: var(--fui-color-purple-700); 70 | --fui-control-height-md: 40px; 71 | --fui-border-radius-md: 20px; 72 | } 73 | 74 | [data-theme="dark"], 75 | [data-theme="dark"] * { 76 | --fui-color-badge-blue-bg: var(--fui-color-green-850); 77 | --fui-color-badge-blue-text: var(--fui-color-green-300); 78 | --fui-color-badge-blue-border: var(--fui-color-green-300); 79 | --fui-color-brand: var(--fui-color-purple-300); 80 | } 81 | ... 82 | ``` 83 | 84 |
85 |
86 |
Badge
87 | 88 |
89 |
90 |
Themed Badge
91 | 92 |
93 |
94 |
Button
95 | 96 |
97 |
98 |
Themed Button
99 | 100 |
101 |
-------------------------------------------------------------------------------- /style/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/style.css" 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "lib": ["dom", "esnext"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "target": "esnext", 11 | "strict": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["**/node_modules", "**/template*"] 16 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import dts from 'vite-plugin-dts'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: { 9 | 'style': resolve(__dirname, 'src/css/main.css'), 10 | 'fui-button': resolve(__dirname, 'src/components/fui-button/fui-button.tsx'), 11 | 'fui-checkbox': resolve(__dirname, 'src/components/fui-checkbox/fui-checkbox.tsx'), 12 | 'fui-popover': resolve(__dirname, 'src/components/fui-popover/fui-popover.tsx'), 13 | 'fui-select': resolve(__dirname, 'src/components/fui-select/fui-select.tsx'), 14 | 'fui-badge': resolve(__dirname, 'src/components/fui-badge/fui-badge.tsx'), 15 | 'fui-radio': resolve(__dirname, 'src/components/fui-radio/fui-radio.tsx'), 16 | 'fui-tooltip': resolve(__dirname, 'src/components/fui-tooltip/fui-tooltip.tsx'), 17 | 'fui-text-input': resolve(__dirname, 'src/components/fui-text-input/fui-text-input.tsx'), 18 | 'fui-switch': resolve(__dirname, 'src/components/fui-switch/fui-switch.tsx'), 19 | 'fui-status-message': resolve(__dirname, 'src/components/fui-status-message/fui-status-message.tsx'), 20 | 'fui-stepper': resolve(__dirname, 'src/components/fui-stepper/fui-stepper.tsx'), 21 | 'fui-toggle': resolve(__dirname, 'src/components/fui-toggle/fui-toggle.tsx'), 22 | 'fui-empty': resolve(__dirname, 'src/components/fui-empty/fui-empty.tsx'), 23 | 'fui-notification': resolve(__dirname, 'src/components/fui-notification/fui-notification.tsx'), 24 | 'fui-modal': resolve(__dirname, 'src/components/fui-modal/fui-modal.tsx'), 25 | 'fui-option-group': resolve(__dirname, 'src/components/fui-option-group/fui-option-group.tsx'), 26 | }, 27 | fileName: '[name]/index', 28 | formats: ['cjs'], 29 | }, 30 | rollupOptions: { 31 | external: ['react', 'react-dom'], 32 | output: { 33 | globals: { 34 | react: 'React', 35 | 'react-dom': 'ReactDOM', 36 | }, 37 | }, 38 | }, 39 | }, 40 | plugins: [dts({ 41 | include: 'src/components/*/*.{ts,tsx}', 42 | exclude: ['src/components/**/*.stories.{ts,tsx}'], 43 | beforeWriteFile: (filePath, content) => ({ 44 | filePath: filePath.replace(/fui(-[a-z]+)+.d.ts/, 'index.d.ts').replace(/components\//, ''), 45 | content, 46 | }), 47 | })], 48 | }); --------------------------------------------------------------------------------