├── .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 |
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 |
19 | Website: www.functional-ui-kit.com
20 | Figma Library: https://www.figma.com/community/file/1338456115232271694
21 | Storybook: https://functional-ui.github.io/functional-ui-kit
22 | Github: www.github.com/functional-ui/functional-ui-kit
23 | Npm: www.npmjs.com/package/functional-ui-kit
24 |
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 |
45 | {props.icon ? {props.icon} : null}
46 | {props.label ? {props.label}
: null}
47 | {props.iconRight ? {props.iconRight} : null}
48 | {props.isLoading &&
}
49 |
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 &&
{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 |
157 | {children}
158 |
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 | {
237 | props.onClick?.(event);
238 | setOpen(false);
239 | }}
240 | />
241 | );
242 | });
243 |
--------------------------------------------------------------------------------
/src/components/fui-radio/fui-radio.css:
--------------------------------------------------------------------------------
1 | .fui-radio {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | gap: var(--fui-space-sm);
6 | color: var(--fui-color-foreground-default);
7 | position: relative;
8 | width: fit-content;
9 | border-radius: var(--fui-border-radius-round);
10 | }
11 |
12 | .fui-radio.fui-focused {
13 | outline: 2px solid;
14 | outline-color: var(--fui-color-state-focus);
15 | color: var(--fui-color-state-focus);
16 | outline-offset: 0.2em;
17 | }
18 |
19 | .fui-radio::after {
20 | margin: -2px;
21 | border-radius: var(--fui-border-radius-round);
22 | }
23 |
24 | .fui-radio-indicator {
25 | position: relative;
26 | border: 2px solid var(--fui-disableable-color-divider-solid);
27 | border-radius: var(--fui-border-radius-round);
28 | height: 18px;
29 | width: 18px;
30 | background-color: var(--fui-disableable-color-background-base);
31 | }
32 |
33 | .fui-radio-indicator.checked {
34 | border-color: var(--fui-disableable-color-foreground-primary);
35 | }
36 |
37 | .fui-radio-indicator.checked::after {
38 | content: '';
39 | position: absolute;
40 | top: 2px;
41 | left: 2px;
42 | width: 10px;
43 | height: 10px;
44 | border-radius: var(--fui-border-radius-round);
45 | background-color: var(--fui-disableable-color-foreground-primary);
46 | }
47 |
48 | .fui-radio-label {
49 | font: var(--running-small-bold);
50 | color: var(--fui-disableable-color-foreground-default);
51 | line-height: 100%;
52 | }
--------------------------------------------------------------------------------
/src/components/fui-radio/fui-radio.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import { FuiRadio } from './fui-radio';
5 |
6 | const meta = {
7 | title: ' Components/Radio',
8 | component: FuiRadio,
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=2511-73454&mode=design&t=jq0JgMhh6dwhuYIm-4'
14 | },
15 | docs: {
16 | description: {
17 | component: 'Radio groups are best for selecting a single option from a short list, while Selects are less efficient due to multiple interactions and hidden options. Radio buttons are preferable for lists of around ten or fewer options, as they require just one quick interaction and ensure all choices are visible and easily comparable. If there isn’t enough space, try a a select instead.'
18 | }
19 | }
20 | },
21 | argTypes: {
22 | checked: {
23 | name: '🔗 checked'
24 | },
25 | disabled: {
26 | name: '🔗 disabled'
27 | },
28 | label: {
29 | name: '🔗 label'
30 | },
31 | className: {
32 | control: {
33 | disable: true
34 | }
35 | },
36 | ariaLabel: {
37 | control: {
38 | disable: true
39 | }
40 | }
41 | }
42 | } satisfies Meta;
43 |
44 | export default meta;
45 | type Story = StoryObj;
46 |
47 | export const Default: Story = {
48 | render: (props) => {
49 | const [checked, setChecked] = React.useState(false);
50 |
51 | React.useEffect(() => {
52 | setChecked(!!props.checked);
53 | }, [props.checked]);
54 |
55 | return (
56 | { setChecked(!checked); }}
60 | />
61 | );
62 | },
63 | args: {
64 | },
65 | name: 'Basic'
66 | };
67 |
68 | export const Disabled: Story = {
69 | render: (props) => {
70 | return (
71 |
72 |
76 |
81 |
86 |
92 |
93 | );
94 | },
95 | args: {
96 | },
97 | };
98 |
99 | export const Group: Story = {
100 | render: () => {
101 | const [value, setValue] = React.useState(0);
102 |
103 | return (
104 |
105 | { setValue(0); }} checked={value === 0} label='Red' />
106 | { setValue(1); }} checked={value === 1} label='Green' />
107 | { setValue(2); }} checked={value === 2} label='Blue' />
108 | { setValue(3); }} checked={value === 3} label='Yellow' />
109 |
110 | );
111 | },
112 | args: {
113 | }
114 | };
115 |
--------------------------------------------------------------------------------
/src/components/fui-radio/fui-radio.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { prefix } from '../prefix';
3 | import classnames from 'classnames';
4 |
5 | export interface FuiRadioProps {
6 | checked?: boolean
7 | disabled?: boolean
8 | label?: string
9 | onClick: () => void
10 |
11 | ariaLabel?: string
12 | className?: string
13 | }
14 |
15 | const compPrefix = `${prefix}-radio`;
16 |
17 | export const FuiRadio = (props: FuiRadioProps) => {
18 | const [focused, setFocused] = React.useState(false);
19 | const ref = React.useRef(null);
20 | const inputId = React.useMemo(() => `${compPrefix}-input-${Math.random().toString(36).substring(7)}`, []);
21 | const labelId = React.useMemo(() => `${compPrefix}-label-${Math.random().toString(36).substring(7)}`, []);
22 |
23 | const indicatorClassNames = classnames(
24 | `${compPrefix}-indicator`,
25 | props.className,
26 | props.checked ? 'checked' : '',
27 | );
28 |
29 | const classNames = classnames(
30 | compPrefix,
31 | {
32 | [`${prefix}-interactable`]: !props.disabled,
33 | [`${prefix}-disabled`]: props.disabled,
34 | [`${prefix}-focused`]: focused
35 | }
36 | );
37 |
38 | const onClick = () => {
39 | if (props.disabled) return;
40 | props.onClick();
41 | ref.current?.focus();
42 | };
43 |
44 | const onSpace = (e: React.KeyboardEvent) => {
45 | if (e.key === ' ') {
46 | e.preventDefault();
47 | props.onClick();
48 | }
49 | };
50 |
51 | return (
52 |
53 |
54 | { setFocused(false); }} onFocus={() => { setFocused(true); }} disabled={props.disabled} onChange={props.onClick} ref={ref} type='radio' style={{ height: 0 }} />
55 |
56 | {props.label &&
57 | {props.label}
58 | }
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/src/components/fui-select/fui-select.css:
--------------------------------------------------------------------------------
1 | .fui-select {
2 | min-width: var(--select-min-width);
3 | font: var(--running-small-bold);
4 | display: flex;
5 | flex-direction: column;
6 | gap: var(--fui-space-sm);
7 | }
8 |
9 | .fui-select .fui-popover {
10 | height: 100%;
11 | box-shadow: none;
12 | border: 0;
13 | background: transparent;
14 | color: unset;
15 | border-radius: unset;
16 | }
17 |
18 | [class*="fui-select"]:focus-visible {
19 | border-radius: var(--fui-radius-md);
20 | outline-offset: -4px;
21 | }
22 |
23 | .fui-select-menu-option:focus-visible {
24 | outline-offset: 4px;
25 | border-radius: var(--fui-radius-xs);
26 | }
27 |
28 | .fui-select-menu-option .fui-interactable::after {
29 | display: none;
30 | }
31 |
32 | .fui-select-native-element {
33 | -webkit-appearance: none;
34 | -moz-appearance: none;
35 | padding: 0px var(--fui-space-md) 0px var(--fui-space-lg);
36 | text-overflow: ellipsis;
37 | border: 0;
38 | height: calc(var(--fui-control-height-md) - 2px);
39 | width: 100%;
40 | color: var(--fui-disableable-color-foreground-default);
41 | font: inherit;
42 | background-color: var(--fui-disableable-color-background-base);
43 | }
44 |
45 |
46 | .fui-select-label {
47 | display: flex;
48 | align-items: flex-start;
49 | align-self: stretch;
50 | color: var(--fui-color-foreground-soft);
51 | font: var(--running-small-bold);
52 | }
53 |
54 | .fui-select-container {
55 | display: flex;
56 | background-color: var(--fui-disableable-color-divider-soft);
57 | gap: 1px;
58 | min-height: 100%;
59 | align-items: center;
60 | align-self: stretch;
61 | border-radius: var(--fui-radius-md);
62 | border: 1px solid var(--fui-color-divider-solid);
63 | position: relative;
64 | overflow: hidden;
65 | }
66 |
67 | .fui-select-container::after {
68 | margin: var(--fui-space-xs);
69 | border-radius: var(--fui-radius-sm);
70 | overflow: visible;
71 | }
72 |
73 | .fui-select-native-element-no-value {
74 | opacity: 0;
75 | }
76 |
77 |
78 | .fui-select-placeholder {
79 | font: var(--running-small-regular);
80 | display: flex;
81 | align-items: center;
82 | overflow: hidden;
83 | color: var(--fui-color-foreground-softest);
84 | text-overflow: ellipsis;
85 | white-space: nowrap;
86 | position: absolute;
87 | height: calc(var(--fui-control-height-md) - 2px);
88 | width: 100%;
89 | background-color: var(--fui-disableable-color-background-base);
90 | padding: 0px var(--fui-space-md) 0px var(--fui-space-lg);
91 | }
92 |
93 | .fui-select-chevron-container {
94 | position: absolute;
95 | display: flex;
96 | width: 34px;
97 | height: calc(var(--fui-control-height-md) - 2px);
98 | right: 0;
99 | justify-content: center;
100 | align-items: center;
101 | }
102 |
103 | .fui-select-chevron-container [class*="fui-icon"] {
104 | color: var(--fui-disableable-color-foreground-primary);
105 | }
106 |
107 | .fui-select-wrapper {
108 | position: relative;
109 | display: flex;
110 | align-self: stretch;
111 | align-items: flex-start;
112 | overflow-x: auto;
113 | flex: 1 0 0;
114 | gap: 1px;
115 | }
116 |
117 | .fui-select .fui-select-clear-value-container::after,
118 | .fui-select .fui-select-custom-chevron-container::after,
119 | .fui-select .fui-select-wrapper::after,
120 | .fui-select .fui-select-clear-container::after {
121 | top: 4px;
122 | left: 4px;
123 | bottom: 4px;
124 | right: 4px;
125 | border-radius: var(--fui-radius-sm);
126 | }
127 |
128 | .fui-select-menu .fui-select-menu-option::after {
129 | border-radius: var(--fui-radius-sm);
130 | width: calc(100% + var(--fui-space-lg));
131 | left: 50%;
132 | transform: translateX(-50%);
133 | }
134 |
135 | .fui-select-custom-chevron-container {
136 | display: flex;
137 | width: 34px;
138 | flex-shrink: 0;
139 | min-height: calc(var(--fui-control-height-md) - 2px);
140 | justify-content: center;
141 | align-items: center;
142 | position: relative;
143 | align-self: stretch;
144 | background-color: var(--fui-disableable-color-background-base);
145 | }
146 |
147 | .fui-select-custom-chevron-container [class*="fui-icon"],
148 | .fui-select-clear-container [class*="fui-icon"],
149 | .fui-select-clear-value-container [class*="fui-icon"] {
150 | color: var(--fui-disableable-color-foreground-primary);
151 | }
152 |
153 | .fui-select-clear-container {
154 | display: flex;
155 | width: 34px;
156 | height: calc(var(--fui-control-height-md) - 2px);
157 | justify-content: center;
158 | align-items: center;
159 | position: relative;
160 | background-color: var(--fui-disableable-color-background-base);
161 | }
162 |
163 | .fui-select-menu {
164 | display: flex;
165 | padding: var(--fui-space-lg) var(--fui-space-xlg);
166 | flex-direction: column;
167 | align-items: flex-start;
168 | background: var(--fui-color-background-base);
169 | border-radius: var(--fui-radius-md);
170 | }
171 |
172 | .fui-select-menu-option {
173 | display: flex;
174 | height: 38px;
175 | gap: var(--fui-space-sm);
176 | align-items: center;
177 | align-self: stretch;
178 | font: var(--running-small-bold);
179 | position: relative;
180 | }
181 |
182 | .fui-select-menu-option .fui-checkbox-wrapper {
183 | width: 100%;
184 | }
185 |
186 | .fui-select-menu-option .fui-checkbox-wrapper::after {
187 | margin: -6px;
188 | }
189 |
190 | .fui-select-menu-group {
191 | display: flex;
192 | flex-direction: column;
193 | min-height: 38px;
194 | align-self: stretch;
195 | }
196 |
197 | .fui-select-menu-group-label {
198 | font: var(--running-small-bold);
199 | color: var(--fui-color-foreground-softest);
200 | }
201 |
202 | .fui-select-menu-option-label {
203 | display: flex;
204 | flex: 1 0 0;
205 | color: var(--fui-disableable-color-foreground-default);
206 | }
207 |
208 | .fui-select-value {
209 | background: var(--fui-disableable-color-background-base);
210 | position: relative;
211 | display: flex;
212 | padding-left: var(--fui-space-lg);
213 | align-items: center;
214 | gap: var(--fui-space-sm);
215 | flex: 1 1 0;
216 | justify-content: space-between;
217 | color: var(--fui-disableable-color-foreground-default);
218 | white-space: nowrap;
219 | font: var(--running-small-bold);
220 | }
221 |
222 | .fui-select-value-label {
223 | height: 100%;
224 | display: flex;
225 | align-items: center;
226 | overflow: hidden;
227 | text-overflow: ellipsis;
228 | }
229 |
230 | .fui-select-clear-value-container {
231 | display: flex;
232 | min-width: 34px;
233 | height: calc(var(--fui-control-height-md) - 2px);
234 | justify-content: center;
235 | align-items: center;
236 | position: relative;
237 | }
238 |
239 | .fui-select-value-prefix-wrapper {
240 | display: flex;
241 | align-items: center;
242 | gap: var(--fui-space-sm);
243 | height: calc(var(--fui-control-height-md) - 2px);
244 | flex-shrink: 1;
245 | width: 100%;
246 | }
247 |
248 |
249 | .fui-select-value-prefix-wrapper.fui-interactable::after {
250 | left: -8px;
251 | right: -8px;
252 | top: 4px;
253 | bottom: 4px;
254 | border-radius: var(--fui-radius-sm);
255 | }
256 |
257 | .fui-select:not(.fui-select-fixed-height) .fui-select-wrapper {
258 | flex-wrap: wrap;
259 | }
--------------------------------------------------------------------------------
/src/components/fui-select/fui-select.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 { FuiSelect } from './fui-select';
5 | import { Clearable } from '../fui-text-input/fui-text-input.stories';
6 |
7 | const meta = {
8 | title: ' Components/Select',
9 | component: FuiSelect,
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=2574-19579&mode=design&t=jq0JgMhh6dwhuYIm-4'
15 | },
16 | docs: {
17 | description: {
18 | component: 'Help people choose single or multiple values from a set of options. Consider select when you have 5 or more options. As an alternative, radio buttons or checkboxes can make the interface cleaner and more accessible.'
19 | }
20 | }
21 | },
22 | argTypes: {
23 | disabled: {
24 | name: '🔗 disabled'
25 | },
26 | type: {
27 | name: '🔗 type'
28 | },
29 | label: {
30 | name: '🔗 label'
31 | },
32 | placeholder: {
33 | name: '🔗 placeholder'
34 | },
35 | clearable: {
36 | name: '🔗 clearable'
37 | },
38 | statusMsg: {
39 | control: 'boolean',
40 | name: '🔗 statusMsg',
41 | mapping: {
42 | false: undefined,
43 | true: { message: 'This is the message of this status', type: 'invalid' }
44 | }
45 | },
46 | fixedHeight: {
47 | name: '🔗 fixedHeight'
48 | },
49 | value: {
50 | name: '🔗 value',
51 | control: {
52 | disable: true
53 | }
54 | },
55 | options: {
56 | name: '🔗 options',
57 | control: {
58 | disable: true
59 | }
60 | },
61 | className: {
62 | control: {
63 | disable: true
64 | }
65 | },
66 | ariaLabel: {
67 | control: {
68 | disable: true
69 | }
70 | }
71 | }
72 | } satisfies Meta;
73 |
74 | export default meta;
75 | type Story = StoryObj;
76 |
77 | export const Default: Story = {
78 | render: (args) => {
79 | const [value, setValue] = React.useState(args.value?.toString());
80 | const [multiValue, setMultiValue] = React.useState([args.value?.toString()]);
81 |
82 | return (
83 |
84 | );
85 | },
86 | name: 'Basic',
87 | args: {
88 | type: 'single-value',
89 | placeholder: 'Select option',
90 | clearable: true,
91 | value: '',
92 | options: [
93 | { label: 'Option 1', value: '1' },
94 | { label: 'Option 2', value: '2' },
95 | { label: 'Option 3', value: '3' },
96 | { label: 'Option 4', value: '4' },
97 | { label: 'Option 5', value: '5' }
98 | ]
99 | }
100 | };
101 |
102 | export const NativeSelect: Story = {
103 | render: (args) => {
104 | const [value, setValue] = React.useState(args.value?.toString());
105 |
106 | return (
107 |
108 | );
109 | },
110 | argTypes: {
111 | type: {
112 | table: {
113 | disable: true
114 | }
115 | },
116 | placeholder: {
117 | table: {
118 | disable: true
119 | }
120 | }
121 | },
122 | args: {
123 | type: 'native',
124 | value: '1',
125 | options: [
126 | { label: 'Group', options: [{ label: 'Option 1', value: '1' }] },
127 | { label: 'Option 2', value: '2' },
128 | { label: 'Option 3', value: '3' },
129 | { label: 'Option 4', value: '4' },
130 | { label: 'Option 5', value: '5' }
131 | ]
132 | }
133 | };
134 |
135 | export const SingleSelectGrouped: Story = {
136 | render: (args) => {
137 | const [value, setValue] = React.useState(args.value?.toString());
138 |
139 | return (
140 |
141 | );
142 | },
143 | argTypes: {
144 | type: {
145 | table: {
146 | disable: true
147 | }
148 | }
149 | },
150 | parameters: {
151 | design: {
152 | type: 'figma',
153 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2556-73873&mode=design&t=jq0JgMhh6dwhuYIm-4'
154 | }
155 | },
156 | args: {
157 | type: 'single-value',
158 | placeholder: 'Select an option',
159 | value: '1',
160 | options: [
161 | { label: 'Group A', options: [{ label: 'Option 1', value: '1a' }, { label: 'Option 2', value: '2a' }, { label: 'Option 3', value: '3a' }] },
162 | { label: 'Group B', options: [{ label: 'Option 1', value: '1b' }, { label: 'Option 2', value: '2b' }, { label: 'Option 3', value: '3b' }] }
163 | ]
164 | }
165 | };
166 |
167 | export const MultiSelectGrouped: Story = {
168 | render: (args) => {
169 | const [value, setValue] = React.useState(args.value);
170 |
171 | return (
172 |
173 | );
174 | },
175 | argTypes: {
176 | type: {
177 | table: {
178 | disable: true
179 | }
180 | }
181 | },
182 | parameters: {
183 | design: {
184 | type: 'figma',
185 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2556-75579&mode=design&t=jq0JgMhh6dwhuYIm-4'
186 | }
187 | },
188 | args: {
189 | type: 'multi-value',
190 | placeholder: 'Select option',
191 | value: ['1'],
192 | options: [
193 | { label: 'Group A', options: [{ label: 'Option 1', value: '1a' }, { label: 'Option 2', value: '2a' }, { label: 'Option 3', value: '3a' }] },
194 | { label: 'Group B', options: [{ label: 'Option 1', value: '1b' }, { label: 'Option 2', value: '2b' }, { label: 'Option 3', value: '3b' }] },
195 | { label: 'Option X', value: 'X' },
196 | { label: 'Option Y', value: 'Y' }
197 | ]
198 | }
199 | };
200 |
201 | export const Disabled: Story = {
202 | render: (args) => {
203 | const [value, setValue] = React.useState(args.value?.toString());
204 | const [multiValue, setMultiValue] = React.useState([args.value?.toString()]);
205 |
206 | return (
207 |
208 | );
209 | },
210 | argTypes: {
211 | disabled: {
212 | table: {
213 | disable: true
214 | }
215 | }
216 | },
217 | args: {
218 | type: 'native',
219 | value: '1',
220 | disabled: true,
221 | options: [
222 | { label: 'Option 1', value: '1' },
223 | { label: 'Option 2', value: '2' },
224 | { label: 'Option 3', value: '3' },
225 | { label: 'Option 4', value: '4' },
226 | { label: 'Option 5', value: '5' }
227 | ]
228 | }
229 | };
230 |
231 | export const CustomOptions: Story = {
232 | parameters: {
233 | docs: {
234 | description: {
235 | story: 'You can customize your options by passing in a `prefix` (on single select) or `suffix` (on single or multi select).\nYou can also use the `groupLabel` prop to group your options.'
236 | }
237 | }
238 | },
239 | render: (args) => {
240 | const [value, setValue] = React.useState(args.value?.toString());
241 | const [multiValue, setMultiValue] = React.useState([args.value?.toString()]);
242 |
243 | return (
244 |
245 | );
246 | },
247 | argTypes: {
248 | options: {
249 | table: {
250 | disable: true
251 | }
252 | }
253 | },
254 | args: {
255 | type: 'single-value',
256 | value: '1',
257 | options: [
258 | { label: 'Option 1', value: '1', prefix: , suffix: },
259 | { label: 'Option 2', value: '2', prefix: , suffix: },
260 | { label: 'Option 3', value: '3', prefix: , suffix: }
261 | ]
262 | }
263 | };
264 |
--------------------------------------------------------------------------------
/src/components/fui-status-message/fui-status-message.css:
--------------------------------------------------------------------------------
1 | .fui-status-message {
2 | font: var(--running-small-regular);
3 | display: flex;
4 | align-items: center;
5 | gap: var(--fui-space-xs);
6 | align-self: stretch;
7 | }
8 |
9 | .fui-status-message-success {
10 | color: var(--fui-color-status-success);
11 | }
12 |
13 | .fui-status-message-success [class*="fui-icon"] {
14 | color: var(--fui-color-status-success);
15 | }
16 |
17 | .fui-status-message-attention {
18 | color: var(--fui-color-status-attention);
19 | }
20 |
21 | .fui-status-message-invalid {
22 | color: var(--fui-color-status-danger);
23 | }
24 |
25 | .fui-status-message-invalid [class*="fui-icon"] {
26 | color: var(--fui-color-status-danger);
27 | }
28 |
29 | .fui-status-message-icon-wrapper-invalid {
30 | width: 14px;
31 | height: 14px;
32 | border-radius: var(--fui-radius-round);
33 | display: flex;
34 | align-items: center;
35 | justify-content: center;
36 | border: 1px solid var(--fui-color-status-danger);
37 | background-color: var(--fui-color-status-danger-subtle);
38 | }
39 |
40 | .fui-status-message-icon-wrapper-success {
41 | width: 14px;
42 | height: 14px;
43 | border-radius: var(--fui-radius-round);
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | border: 1px solid var(--fui-color-status-success);
48 | background-color: var(--fui-color-status-success-subtle);
49 | color: var(--fui-color-status-success);
50 | }
--------------------------------------------------------------------------------
/src/components/fui-status-message/fui-status-message.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { FuiStatusMessage } from './fui-status-message';
4 |
5 | const meta = {
6 | title: ' Components/StatusMessage',
7 | component: FuiStatusMessage,
8 | tags: ['autodocs'],
9 | parameters: {
10 | design: {
11 | type: 'figma',
12 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2511-43025&mode=design&t=jq0JgMhh6dwhuYIm-4'
13 | },
14 | docs: {
15 | description: {
16 | component: 'Offer inline feedback for one of three status types. It is most effective in compact spaces, alongside form controls, or within inline text.'
17 | }
18 | }
19 | },
20 | argTypes: {
21 | type: {
22 | control: 'select',
23 | options: ['invalid', 'attention', 'success'],
24 | name: '🔗 type'
25 | },
26 | message: {
27 | name: '🔗 message'
28 | },
29 | className: {
30 | control: {
31 | disable: true
32 | }
33 | },
34 | ariaLabel: {
35 | control: {
36 | disable: true
37 | }
38 | }
39 | }
40 | } satisfies Meta;
41 |
42 | export default meta;
43 | type Story = StoryObj;
44 |
45 | export const Success: Story = {
46 | args: {
47 | type: 'success',
48 | message: "You're doing something right!"
49 | }
50 | };
51 |
52 | export const Invalid: Story = {
53 | args: {
54 | type: 'invalid',
55 | message: 'Something is clearly wrong here'
56 | }
57 | };
58 |
59 | export const Attention: Story = {
60 | args: {
61 | type: 'attention',
62 | message: 'This requires your attention'
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/src/components/fui-status-message/fui-status-message.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { prefix } from '../prefix';
3 | import FuiIconCheck8X8 from '../../icons/fui-icon-check-8x8';
4 | import FuiIconX8x8 from '../../icons/fui-icon-x-8x8';
5 | import classnames from 'classnames';
6 |
7 | const compPrefix = `${prefix}-status-message`;
8 |
9 | export interface FuiStatusMessageProps {
10 | type: 'invalid' | 'attention' | 'success'
11 | message: string
12 |
13 | ariaLabel?: string
14 | className?: string
15 | }
16 |
17 | export const FuiStatusMessage = ({
18 | ...props
19 | }: FuiStatusMessageProps & React.HtmlHTMLAttributes) => {
20 | const classNames = classnames(compPrefix, `${compPrefix}-${props.type}`, props.className);
21 | const attentionTriangle =
22 |
23 |
24 |
25 | ;
26 | const renderIcon = (type: string) => {
27 | switch (type) {
28 | case 'success':
29 | return
;
30 | case 'attention':
31 | return attentionTriangle;
32 | case 'invalid':
33 | return
;
34 | default:
35 | break;
36 | }
37 | };
38 |
39 | return (
40 | {renderIcon(props.type)} {props.message}
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/fui-stepper/fui-stepper.css:
--------------------------------------------------------------------------------
1 | .fui-stepper-wrapper {
2 | display: flex;
3 | min-height: var(--fui-control-height-md);
4 | flex-direction: column;
5 | align-items: flex-start;
6 | border-radius: var(--fui-radius-md);
7 | }
8 |
9 | .fui-stepper-minus-container.fui-interactable::after,
10 | .fui-stepper-plus-container.fui-interactable::after {
11 | width: 22px;
12 | height: 22px;
13 | top: calc(50% - 11px);
14 | left: calc(50% - 11px);
15 | border-radius: var(--fui-radius-md);
16 | }
17 |
18 | .fui-stepper-minus-container.fui-interactable:focus-visible,
19 | .fui-stepper-plus-container.fui-interactable:focus-visible {
20 | outline-offset: -6px;
21 | }
22 |
23 | .fui-stepper-label {
24 | font: var(--running-medium-bold);
25 | color: var(--fui-disableable-color-foreground-default);
26 | }
27 |
28 | .fui-stepper-container {
29 | display: flex;
30 | height: var(--fui-control-height-md);
31 | justify-content: space-between;
32 | align-items: center;
33 | border-radius: var(--fui-radius-md);
34 | border: 1px solid var(--fui-disableable-color-divider-solid);
35 | background: var(--fui-disableable-color-background-base);
36 | overflow: hidden;
37 | }
38 |
39 | .fui-stepper-minus-container {
40 | display: flex;
41 | width: var(--fui-control-height-md);
42 | height: var(--fui-control-height-md);
43 | justify-content: center;
44 | align-items: center;
45 | border: none;
46 | border-right: 1px solid var(--fui-disableable-color-divider-solid);
47 | position: relative;
48 | flex-shrink: 0;
49 | background: none;
50 | }
51 |
52 | .fui-stepper-value-container {
53 | position: relative;
54 | display: flex;
55 | justify-content: center;
56 | align-items: center;
57 | flex-shrink: 1;
58 | overflow: hidden;
59 | }
60 |
61 | .fui-stepper-input {
62 | border: none;
63 | text-align: center;
64 | font: var(--running-medium-regular);
65 | color: var(--fui-disableable-color-foreground-default);
66 | background: transparent;
67 | width: 100%;
68 | line-height: 32px;
69 | }
70 |
71 | .fui-stepper-input:focus-visible {
72 | outline: none;
73 | }
74 |
75 | .fui-stepper-input::-webkit-outer-spin-button,
76 | .fui-stepper-input::-webkit-inner-spin-button {
77 | -webkit-appearance: none;
78 | margin: 0;
79 | }
80 |
81 | .fui-stepper-input[type=number] {
82 | -moz-appearance: textfield;
83 | }
84 |
85 | .fui-stepper-plus-container {
86 | display: flex;
87 | width: var(--fui-control-height-md);
88 | height: var(--fui-control-height-md);
89 | justify-content: center;
90 | align-items: center;
91 | border: none;
92 | border-left: 1px solid var(--fui-disableable-color-divider-solid);
93 | position: relative;
94 | flex-shrink: 0;
95 | background: none;
96 | }
--------------------------------------------------------------------------------
/src/components/fui-stepper/fui-stepper.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import { FuiStepper, FuiStepperDisabled } from './fui-stepper';
5 |
6 | const meta = {
7 | title: ' Components/Stepper',
8 | component: FuiStepper,
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=2511-80120&mode=design&t=jq0JgMhh6dwhuYIm-4'
14 | },
15 | docs: {
16 | description: {
17 | component: 'Help people make small numeric changes by pressing buttons or typing numbers. The input is also editable for larger value changes since people can directly edit the input field.'
18 | }
19 | }
20 | },
21 | argTypes: {
22 | disabled: {
23 | name: '🔗 disabled',
24 | control: 'select',
25 | options: [undefined, 'minus', 'plus', 'all']
26 | },
27 | status: {
28 | name: '🔗 status',
29 | control: 'boolean',
30 | mapping: {
31 | false: undefined,
32 | true: { type: 'invalid', message: 'Invalid Input' }
33 | }
34 | },
35 | value: {
36 | name: '🔗 value'
37 | },
38 | label: {
39 | name: '🔗 label'
40 | },
41 | ariaLabel: {
42 | control: {
43 | disable: true
44 | }
45 | },
46 | className: {
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 | render: (args) => {
59 | const [value, setValue] = React.useState(args.value);
60 | React.useEffect(() => {
61 | setValue(args.value);
62 | }, [args.value]);
63 | return (
64 |
65 | );
66 | },
67 | name: 'Basic',
68 | args: {
69 | value: 0
70 | }
71 | };
72 |
73 | export const Disabled: Story = {
74 | render: (args) => {
75 | const [value, setValue] = React.useState(args.value);
76 | React.useEffect(() => {
77 | setValue(args.value);
78 | }, [args.value]);
79 | return (
80 |
81 |
82 |
83 |
84 |
85 | );
86 | },
87 | parameters: {
88 | design: {
89 | type: 'figma',
90 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2511-78711&mode=design&t=jq0JgMhh6dwhuYIm-4'
91 | }
92 | },
93 | argTypes: {
94 | disabled: {
95 | table: {
96 | disable: true
97 | }
98 | }
99 | },
100 | args: {
101 | value: 0
102 | }
103 | };
104 |
105 | export const StatusMessage: Story = {
106 | render: (args) => {
107 | const [value, setValue] = React.useState(args.value);
108 | React.useEffect(() => {
109 | setValue(args.value);
110 | }, [args.value]);
111 | return (
112 |
113 | );
114 | },
115 | parameters: {
116 | design: {
117 | type: 'figma',
118 | url: 'https://www.figma.com/file/zHutj6e9DcPngHZTDtAL1u/Functional-UI-Kit?type=design&node-id=2511-78247&mode=design&t=jq0JgMhh6dwhuYIm-4'
119 | }
120 | },
121 | argTypes: {
122 | label: {
123 | table: {
124 | disable: true
125 | }
126 | },
127 | status: {
128 | table: {
129 | disable: true
130 | }
131 | }
132 | },
133 | args: {
134 | value: 0,
135 | label: 'Input Field',
136 | status: { type: 'invalid', message: 'Invalid Value' }
137 | }
138 | };
139 |
--------------------------------------------------------------------------------
/src/components/fui-stepper/fui-stepper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { prefix } from '../prefix';
3 | import { FuiStatusMessage, type FuiStatusMessageProps } from '../fui-status-message/fui-status-message';
4 | import FuiIconMinus12x12 from '../../icons/fui-icon-minus-12x12';
5 | import FuiIconPlus12x12 from '../../icons/fui-icon-plus-12x12';
6 | import classnames from 'classnames';
7 |
8 | const compPrefix = `${prefix}-stepper`;
9 |
10 | export enum FuiStepperDisabled {
11 | Minus = 'minus',
12 | Plus = 'plus',
13 | All = 'all',
14 | };
15 |
16 | export interface FuiStepperProps {
17 | value: number
18 | onChange: (value: number) => void
19 | disabled?: FuiStepperDisabled
20 | label?: string
21 | status?: FuiStatusMessageProps
22 |
23 | className?: string
24 | ariaLabel?: string
25 | }
26 |
27 | export const FuiStepper = (props: FuiStepperProps) => {
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 isIncDisabled = React.useMemo(() => {
32 | return props.disabled && [FuiStepperDisabled.All, FuiStepperDisabled.Plus].includes(props.disabled);
33 | }, [props.disabled]);
34 |
35 | const isDecDisabled = React.useMemo(() => {
36 | return props.disabled && [FuiStepperDisabled.All, FuiStepperDisabled.Minus].includes(props.disabled);
37 | }, [props.disabled]);
38 |
39 | const isAllDisabled = React.useMemo(() => {
40 | return props.disabled && [FuiStepperDisabled.All].includes(props.disabled);
41 | }, [props.disabled]);
42 |
43 | const inc = React.useCallback(() => {
44 | if (isIncDisabled) return;
45 | props.onChange(props.value + 1);
46 | }, [props.value, props.onChange, props.disabled]);
47 |
48 | const dec = React.useCallback(() => {
49 | if (isDecDisabled) return;
50 | props.onChange(props.value - 1);
51 | }, [props.value, props.onChange, props.disabled]);
52 |
53 | const classNames = classnames(
54 | `${compPrefix}-wrapper`,
55 | props.className
56 | );
57 |
58 | const minusButtonClassNames = classnames(
59 | `${compPrefix}-minus-container`,
60 | isDecDisabled ? `${prefix}-disabled` : `${prefix}-interactable`
61 | );
62 |
63 | const plusButtonClassNames = classnames(
64 | `${compPrefix}-plus-container`,
65 | isIncDisabled ? `${prefix}-disabled` : `${prefix}-interactable`
66 | );
67 |
68 | const valueContainerClassNames = classnames(
69 | `${compPrefix}-value-container`,
70 | isAllDisabled ? `${prefix}-disabled` : ''
71 | );
72 |
73 | const containerStyle = React.useMemo(() => {
74 | return { width: 102 + ((props.value.toString().length - 1) * 10) };
75 | }, [props.value]);
76 |
77 | return (
78 |
79 | {props.label &&
{props.label} }
80 | {props.status &&
}
81 |
82 |
83 |
84 |
85 |
86 | { props.onChange(Number(e.target.value)); }} disabled={isAllDisabled} />
87 |
88 |
89 |
90 |
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 |
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 ?
{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 |
74 | {option.prefix}
75 | {option.label}
76 | {option.suffix}
77 |
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 |
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 |
71 |
75 |
79 |
83 |
87 |
91 |
95 |
99 |
103 |
104 |
StatusMessage
105 |
106 |
107 |
108 |
Stepper
109 |
110 |
111 |
115 |
116 |
TextInput
117 |
118 |
119 |
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 |
89 |
90 |
Themed Badge
91 |
92 |
93 |
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 | });
--------------------------------------------------------------------------------