add storybook

This commit is contained in:
Ildar Kamalov 2025-07-23 12:41:47 +03:00
parent 25d9cb7fc6
commit 0902003df2
35 changed files with 2781 additions and 645 deletions

3
.gitignore vendored
View File

@ -43,3 +43,6 @@ coverage.txt
node_modules/
!/build/gitkeep
*storybook.log
storybook-static

View File

@ -7,7 +7,7 @@ indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 180
max_line_length = 120
[*.md]
max_line_length = off

View File

@ -7,7 +7,8 @@
"prettier",
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
"plugin:storybook/recommended"
],
"parser": "@typescript-eslint/parser",
"env": {
@ -84,4 +85,4 @@
}
]
}
}
}

View File

@ -0,0 +1,52 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
import * as path from 'path';
// Get the project root directory
const projectRoot = path.resolve(process.cwd());
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-webpack5-compiler-swc', '@storybook/addon-docs'],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
webpackFinal: async (config) => {
// Modify existing CSS rules to support PCSS files
if (config.module?.rules) {
config.module.rules.forEach((rule) => {
if (rule && typeof rule !== 'string' && rule.test) {
// Find CSS rules and extend them to handle PCSS
if (rule.test instanceof RegExp) {
// Extend CSS test to include PCSS files
if (rule.test.test('.css')) {
rule.test = /\.(css|pcss)$/;
}
// Extend CSS module test to include PCSS modules
if (rule.test.toString().includes('module') && rule.test.test('module.css')) {
rule.test = /\.module\.(css|pcss)$/;
}
}
}
});
}
// Resolve panel alias to match tsconfig.json paths
if (config.resolve?.alias) {
config.resolve.alias = {
...config.resolve.alias,
panel: path.resolve(projectRoot, 'src'),
};
} else {
config.resolve = {
...config.resolve,
alias: {
panel: path.resolve(projectRoot, 'src'),
},
};
}
return config;
},
};
export default config;

View File

@ -0,0 +1,26 @@
import type { Preview } from '@storybook/react-webpack5';
import React from 'react';
import { Icons } from '../src/common/ui/Icons';
import '../src/index.pcss';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
(Story) => React.createElement(
React.Fragment,
null,
React.createElement(Icons),
React.createElement(Story)
),
],
};
export default preview;

View File

@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["node"],
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["../src/**/*", "*.ts", "*.tsx"]
}

View File

@ -1,133 +1,141 @@
{
"name": "dashboard",
"version": "1.0.0",
"private": true,
"scripts": {
"build-dev": "cross-env NODE_ENV=development BUILD_ENV=dev webpack --config webpack.dev.js",
"build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
"watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
"lint": "eslint --ext .ts,.tsx src",
"lint:fix": "eslint --ext .ts,.tsx src --fix",
"test": "vitest --run",
"test:watch": "vitest --watch",
"test:e2e": "npx playwright test tests/e2e",
"test:e2e:interactive": "npx playwright test --ui",
"test:e2e:debug": "npx playwright test --debug",
"test:e2e:codegen": "npx playwright codegen",
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch"
},
"type": "module",
"dependencies": {
"@adguard/translate": "^1.0.2",
"@nivo/line": "^0.64.0",
"axios": "^0.19.2",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"countries-and-timezones": "^3.6.0",
"date-fns": "^1.29.0",
"i18next": "^19.6.2",
"i18next-browser-languagedetector": "^4.2.0",
"ipaddr.js": "^1.9.1",
"js-yaml": "^3.14.0",
"lodash": "^4.17.19",
"nanoid": "^3.1.9",
"popper.js": "^1.16.1",
"prop-types": "^15.8.1",
"qs": "^6.14.0",
"query-string": "^6.13.1",
"rc-dialog": "^10.0.0",
"rc-dropdown": "^4.2.1",
"react": "^16.13.1",
"react-click-outside": "^3.0.1",
"react-dom": "^16.13.1",
"react-hook-form": "^7.54.0",
"react-i18next": "^11.7.2",
"react-modal": "^3.11.2",
"react-popper-tooltip": "^2.11.1",
"react-redux": "^7.2.0",
"react-redux-loading-bar": "^4.6.0",
"react-router-dom": "^5.2.0",
"react-router-hash-link": "^1.2.2",
"react-select": "^5.3.2",
"react-table": "^6.11.4",
"react-transition-group": "^4.4.5",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
"ts-migrate": "^0.1.35",
"url-polyfill": "^1.1.12"
},
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/plugin-transform-class-properties": "^7.24.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1",
"@babel/plugin-transform-object-rest-spread": "^7.24.5",
"@babel/plugin-transform-optional-chaining": "^7.24.5",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
"@playwright/test": "1.50.1",
"@types/lodash": "^4.17.4",
"@types/node": "^22.13.10",
"@types/react": "^17.0.80",
"@types/react-dom": "^18.3.0",
"@types/react-redux": "^7.1.33",
"@types/react-router-dom": "^5.3.3",
"@types/react-table": "^7.7.20",
"@types/redux-actions": "^2.6.5",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.2",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.0",
"jscodeshift": "^0.15.2",
"mini-css-extract-plugin": "^2.9.0",
"path": "^0.12.7",
"postcss": "^8.5.6",
"postcss-custom-media": "^11.0.6",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.1.1",
"postcss-nested": "^7.0.2",
"prettier": "^3.2.5",
"react-hot-loader": "^4.13.1",
"style-loader": "^4.0.0",
"stylelint": "^16.5.0",
"ts-loader": "^9.5.1",
"url-loader": "^4.1.1",
"user-agent-data-types": "^0.4.2",
"vitest": "^3.1.1",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
},
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
">1%",
"last 4 chrome version",
"last 4 firefox version",
"last 4 safari version",
"firefox esr",
"not ie < 9"
]
}
"name": "dashboard",
"version": "1.0.0",
"private": true,
"scripts": {
"build-dev": "cross-env NODE_ENV=development BUILD_ENV=dev webpack --config webpack.dev.js",
"build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
"watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
"lint": "eslint --ext .ts,.tsx src",
"lint:fix": "eslint --ext .ts,.tsx src --fix",
"test": "vitest --run",
"test:watch": "vitest --watch",
"test:e2e": "npx playwright test tests/e2e",
"test:e2e:interactive": "npx playwright test --ui",
"test:e2e:debug": "npx playwright test --debug",
"test:e2e:codegen": "npx playwright codegen",
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"type": "module",
"dependencies": {
"@adguard/translate": "^1.0.2",
"@nivo/line": "^0.64.0",
"axios": "^0.19.2",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"countries-and-timezones": "^3.6.0",
"date-fns": "^1.29.0",
"i18next": "^19.6.2",
"i18next-browser-languagedetector": "^4.2.0",
"ipaddr.js": "^1.9.1",
"js-yaml": "^3.14.0",
"lodash": "^4.17.19",
"nanoid": "^3.1.9",
"popper.js": "^1.16.1",
"prop-types": "^15.8.1",
"qs": "^6.14.0",
"query-string": "^6.13.1",
"rc-dialog": "^10.0.0",
"rc-dropdown": "^4.2.1",
"react": "^16.13.1",
"react-click-outside": "^3.0.1",
"react-dom": "^16.13.1",
"react-hook-form": "^7.54.0",
"react-i18next": "^11.7.2",
"react-modal": "^3.11.2",
"react-popper-tooltip": "^2.11.1",
"react-redux": "^7.2.0",
"react-redux-loading-bar": "^4.6.0",
"react-router-dom": "^5.2.0",
"react-router-hash-link": "^1.2.2",
"react-select": "^5.3.2",
"react-table": "^6.11.4",
"react-transition-group": "^4.4.5",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
"ts-migrate": "^0.1.35",
"url-polyfill": "^1.1.12"
},
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/plugin-transform-class-properties": "^7.24.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1",
"@babel/plugin-transform-object-rest-spread": "^7.24.5",
"@babel/plugin-transform-optional-chaining": "^7.24.5",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
"@playwright/test": "1.50.1",
"@storybook/addon-docs": "^9.0.18",
"@storybook/addon-onboarding": "^9.0.18",
"@storybook/addon-webpack5-compiler-swc": "^3.0.0",
"@storybook/react-webpack5": "^9.0.18",
"@types/lodash": "^4.17.4",
"@types/node": "^22.13.10",
"@types/react": "^17.0.80",
"@types/react-dom": "^18.3.0",
"@types/react-redux": "^7.1.33",
"@types/react-router-dom": "^5.3.3",
"@types/react-table": "^7.7.20",
"@types/redux-actions": "^2.6.5",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-storybook": "^9.0.18",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.0",
"jscodeshift": "^0.15.2",
"mini-css-extract-plugin": "^2.9.0",
"path": "^0.12.7",
"postcss": "^8.5.6",
"postcss-custom-media": "^11.0.6",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.1.1",
"postcss-nested": "^7.0.2",
"prettier": "^3.2.5",
"react-hot-loader": "^4.13.1",
"storybook": "^9.0.18",
"style-loader": "^4.0.0",
"stylelint": "^16.5.0",
"ts-loader": "^9.5.1",
"url-loader": "^4.1.1",
"user-agent-data-types": "^0.4.2",
"vitest": "^3.1.1",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
},
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
">1%",
"last 4 chrome version",
"last 4 firefox version",
"last 4 safari version",
"firefox esr",
"not ie < 9"
]
}
}

1165
client_v2/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -47,9 +47,9 @@
height: 24px;
transition: color var(--t2);
color: var(--default-gray-icons);
&_active {
transition: none;
color: var(--default-product-icon);
}
}
.active {
transition: none;
color: var(--default-product-icon);
}

View File

@ -41,13 +41,10 @@ export const Checkbox = ({
{plusStyle ? (
<Icon
icon={checked ? 'checkbox_minus' : 'checkbox_plus'}
className={cn(s.icon, { [s.icon_active]: checked })}
className={cn(s.icon, { [s.active]: checked })}
/>
) : (
<Icon
icon={checked ? 'checkbox_checked' : 'checkbox_unchecked'}
className={cn(s.icon, { [s.icon_active]: checked })}
/>
<Icon icon={checked ? 'checkbox_on' : 'checkbox_off'} className={cn(s.icon, { [s.active]: checked })} />
)}
</div>
{children && (

View File

@ -25,11 +25,11 @@
.icon {
color: var(--default-gray-icons);
}
&_active {
transition: none;
color: var(--default-product-icon);
}
.active {
transition: none;
color: var(--default-product-icon);
}
.input:disabled + .handler .icon {

View File

@ -35,7 +35,7 @@ export const Radio = <T extends number | string | boolean = string>({
<div className={s.handler}>
<Icon
icon={value === o.value ? 'radio_on' : 'radio_off'}
className={cn(s.icon, { [s.icon_active]: value === o.value })}
className={cn(s.icon, { [s.active]: value === o.value })}
/>
</div>
<div className={s.text}>{o.text}</div>

View File

@ -20,10 +20,11 @@ export const Textarea = ({
className,
maxLength,
errorMessage,
disabled,
}: Props) => (
<div className={s.textareaWrapper}>
{label && (
<label className={s.textareaLabel} htmlFor={id}>
<label className={s.label} htmlFor={id}>
{label}
</label>
)}
@ -37,7 +38,8 @@ export const Textarea = ({
onChange={onChange}
wrap={wrap}
maxLength={maxLength}
disabled={disabled}
/>
{errorMessage && <div className={s.error}>{errorMessage}</div>}
{errorMessage && <div className={s.errorMessage}>{errorMessage}</div>}
</div>
);

View File

@ -29,6 +29,18 @@
&.error {
border-color: var(--default-error-icon);
}
&:disabled {
background-color: var(--pressed-page-background);
color: var(--default-description-text);
cursor: default;
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
box-shadow: 0 0 0 100px var(--pressed-page-background) inset;
}
}
}
.label {
@ -39,7 +51,7 @@
color: var(--default-description-text);
}
.error {
.errorMessage {
margin-top: 8px;
font-size: 14px;
color: var(--default-error-icon);

View File

@ -2,14 +2,10 @@ import React, { ComponentProps, ReactNode } from 'react';
import cn from 'clsx';
import s from './Button.module.pcss';
import { IconType } from '../Icons';
import { Icon } from '../Icon';
export type ButtonProps = ComponentProps<'button'> & {
size?: 'small' | 'medium' | 'big';
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
icon?: IconType;
iconClassName?: string;
children?: ReactNode;
leftAddon?: ReactNode;
rightAddon?: ReactNode;

View File

@ -3,8 +3,8 @@ import React, { memo } from 'react';
import './Icons.pcss';
export const ICONS = {
checkbox_unchecked: 'checkbox_unchecked',
checkbox_checked: 'checkbox_checked',
checkbox_off: 'checkbox_off',
checkbox_on: 'checkbox_on',
checkbox_plus: 'checkbox_plus',
checkbox_minus: 'checkbox_minus',
radio_on: 'radio_on',
@ -33,7 +33,7 @@ export const ICON_VALUES: IconType[] = Object.values(ICONS);
export const Icons = memo(() => (
<svg xmlns="http://www.w3.org/2000/svg" className="icons">
<symbol id="checkbox_unchecked" viewBox="0 0 24 24" fill="none" fillRule="evenodd" clipRule="evenodd">
<symbol id="checkbox_off" viewBox="0 0 24 24" fill="none" fillRule="evenodd" clipRule="evenodd">
<path
d="M21 3H3V21H21V3Z"
stroke="currentColor"
@ -43,7 +43,7 @@ export const Icons = memo(() => (
/>
</symbol>
<symbol id="checkbox_checked" viewBox="0 0 24 24" fillRule="evenodd" clipRule="evenodd">
<symbol id="checkbox_on" viewBox="0 0 24 24" fillRule="evenodd" clipRule="evenodd">
<path
d="m22 22v-20h-20v20zm-4.4309-12.5115c.2698-.3143.2337-.7878-.0806-1.05759s-.7878-.23371-1.0576.08059l-5.4763 6.3798-3.41909-3.4869c-.29-.2957-.76485-.3004-1.06061-.0104-.29575.29-.30042.7649-.01041 1.0606l4.56351 4.6541z"
fill="currentColor"

View File

@ -120,14 +120,6 @@ export const Menu = ({ headerMenu }: Props) => {
</div>
</nav>
<div className={s.referenceWrapper}>
{process.env.NODE_ENV === 'development' && (
<div className={cn(s.menuLinkWrapper)}>
<Link className={cn(s.menuLink, { [s.activeLink]: isActive(Paths.Expo) })} to={RoutePath.Expo}>
<Icon className={s.linkIcon} icon="faq" />
<span className={theme.common.textOverflow}>Components</span>
</Link>
</div>
)}
<div className={cn(s.menuLinkWrapper)}>
<a target="_blank" rel="noopener noreferrer" className={s.menuLink} href="">
<Icon className={s.linkIcon} icon="logout" />

View File

@ -13,7 +13,6 @@ import { setHtmlLangAttr, setUITheme } from '../../helpers/helpers';
import { changeLanguage, getDnsStatus, getTimerStatus } from '../../actions';
import { RootState } from '../../initialState';
import Expo from '../Expo';
import s from './styles.module.pcss';
@ -25,14 +24,6 @@ type RouteConfig = {
const ROUTES: RouteConfig[] = [];
if (process.env.NODE_ENV === 'development') {
ROUTES.push({
path: '/expo',
component: Expo,
exact: true,
});
}
const App = () => {
const dispatch = useDispatch();
const { language, isCoreRunning, processing, theme } = useSelector<RootState, RootState['dashboard']>(

View File

@ -1,396 +0,0 @@
import React, { useState } from 'react';
import { withTranslation } from 'react-i18next';
import cn from 'clsx';
import { ICON_VALUES } from 'panel/common/ui/Icons';
import { Switch, Select, Textarea, Input, Radio, Checkbox } from 'panel/common/controls';
import { Dialog, ConfirmDialog, Link, Dropdown, Breadcrumbs, Button, Icon } from 'panel/common/ui';
import { CustomMultiValue } from 'panel/common/controls/Select';
import theme from 'panel/lib/theme';
import { RoutePath } from 'panel/components/Routes/Paths';
import styles from './styles.module.pcss';
// List of all CSS variable names from light.css
export const COLOR_VARIABLES = [
'--default-page-background',
'--hovered-page-background',
'--pressed-page-background',
'--fills-backgrounds-page-background-additional',
'--default-cards-background',
'--default-popup-background',
'--default-footer-background',
'--default-item-divider',
'--default-main-text',
'--disabled-main-text',
'--default-description-text',
'--default-labels',
'--default-input-background',
'--default-active-input-stroke',
'--default-inactive-input-stroke',
'--default-placeholder',
'--default-input-on-card-background',
'--disabled-input-on-card-background',
'--default-dropdown-menu-background',
'--hovered-dropdown-menu-background',
'--pressed-dropdown-menu-background',
'--default-main-button',
'--hovered-main-button',
'--pressed-main-button',
'--disabled-main-button',
'--default-primary-button-text',
'--disabled-primary-button-text',
'--default-primary-button-icon',
'--default-secondary-button',
'--hovered-secondary-button',
'--pressed-secondary-button',
'--disabled-secondary-button',
'--default-secondary-button-stroke',
'--disabled-secondary-button-stroke',
'--default-secondary-card-button',
'--hovered-secondary-card-button',
'--pressed-secondary-card-button',
'--disabled-secondary-card-button',
'--default-danger-button',
'--hovered-danger-button',
'--pressed-danger-button',
'--disabled-danger-button',
'--default-link',
'--hovered-link',
'--pressed-link',
'--visited-link',
'--default-attention-link',
'--hovered-attention-link',
'--pressed-attention-link',
'--disabled-attention-link',
'--default-error-link',
'--default-product-icon',
'--default-black-icons',
'--default-gray-icons',
'--disabled-gray-icons',
'--default-error-icon',
'--stroke-icons-white-icons-default',
'--stroke-icons-tertiary-icon-disabled',
'--stroke-icons-secondary-icon-disabled',
'--default-stats-background',
'--default-red-stat',
'--modal-iframe-overlay',
'--modal-overlay',
'--default-notifications-attention',
'--default-logo-key-color',
'--default-loaders-background',
'--default-loaders-background-dark',
'--default-loaders-primary',
'--default-text-toplines-main',
'--disabled-text-toplines-main',
'--default-fills-toplines-adblocker',
'--default-fills-toplines-vpn',
'--default-breadcrumbs',
'--fills-toplines-topline-background',
'--text-toplines-topline-title',
'--text-toplines-topline-description',
'--text-toplines-topline-button-text-default',
'--fills-toplines-topline-background-image',
'--fills-toplines-topline-button-default',
'--fills-toplines-topline-button-hovered',
'--fills-toplines-topline-button-pressed',
'--stroke-toplines-button-stroke-default',
'--stroke-toplines-button-stroke-hovered',
'--stroke-toplines-button-stroke-pressed',
'--stroke-toplines-close-icon-default',
'--stroke-toplines-close-icon-hovered',
'--stroke-toplines-close-icon-pressed',
'--fills-backgrounds-recent-activity',
'--fills-switch-on-default',
'--fills-switch-on-hovered',
'--fills-switch-on-disabled',
'--fills-switch-off-default',
'--fills-switch-off-hovered',
'--fills-switch-off-disabled',
'--fills-switch-knob',
'--fills-switch-knob-disabled',
];
const Expo = () => {
const [checked, setChecked] = useState(false);
const [switchChecked, setSwitchChecked] = useState(false);
const [inputValue, setInputValue] = useState('');
const [textareaValue, setTextareaValue] = useState('');
const [radioValue, setRadioValue] = useState('1');
const [dialogOpen, setDialogOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
return (
<div className={theme.layout.container}>
<h1 className={cn(theme.title.h3, styles.title)}>Components</h1>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Checkbox</h3>
<div className={styles.contolsList}>
<Checkbox onChange={(e) => setChecked(e.target.checked)} checked={checked}>
<span className={styles.label}>Test checkbox</span>
</Checkbox>
<Checkbox onChange={(e) => setChecked(e.target.checked)} checked={checked} disabled>
<span className={styles.label}>Disabled checkbox</span>
</Checkbox>
</div>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Switch</h3>
<div className={styles.contolsList}>
<Switch handleChange={(e) => setSwitchChecked(e.target.checked)} checked={switchChecked} id="switch">
Test switch
</Switch>
<Switch
handleChange={(e) => setSwitchChecked(e.target.checked)}
checked={switchChecked}
id="switch2"
disabled>
Disabled switch
</Switch>
</div>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Radio</h3>
<Radio
handleChange={(v) => setRadioValue(v)}
value={radioValue}
options={[
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
{ text: 'Option 3', value: '3' },
]}
/>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Buttons</h3>
<div className={styles.buttonsContainer}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="primary" size="small">
Small
</Button>
<Button variant="primary" size="medium">
Medium
</Button>
<Button variant="primary" size="big">
Big
</Button>
<Button variant="primary" leftAddon={<Icon icon="lang" />} rightAddon={<Icon icon="lang" />}>
Button with icon
</Button>
<Button variant="primary" disabled>
Disabled button
</Button>
<Button variant="secondary" disabled>
Disabled secondary button
</Button>
</div>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Input</h3>
<Input
id="input1"
type="text"
value={inputValue}
label="Label"
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter text"
suffixIcon={<Icon icon="lang" />}
prefixIcon={<Icon icon="lang" />}
/>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Textarea</h3>
<Textarea
id="textarea"
value={textareaValue}
onChange={(e) => setTextareaValue(e.target.value)}
placeholder="Enter text"
label="Label"
/>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Dialog</h3>
<Button variant="primary" size="small" onClick={() => setDialogOpen(true)} style={{ marginBottom: 8 }}>
Open Dialog
</Button>
{dialogOpen && (
<Dialog visible title="Dialog Title" onClose={() => setDialogOpen(false)}>
<div className={theme.dialog.body}>Dialog content goes here.</div>
</Dialog>
)}
<h3 className={cn(theme.title.h5, styles.subtitle)}>ConfirmDialog</h3>
<Button variant="primary" size="small" onClick={() => setConfirmOpen(true)} style={{ marginBottom: 8 }}>
Open ConfirmDialog
</Button>
{confirmOpen && (
<ConfirmDialog
title="Decrease log rotation interval?"
text="This will delete all logs older than 6 hours"
onClose={() => setConfirmOpen(false)}
onConfirm={() => setConfirmOpen(false)}
buttonVariant="danger"
buttonText="Yes, decrease"
cancelText="Cancel"
/>
)}
<h3 className={cn(theme.title.h5, styles.subtitle)}>Link</h3>
<Link to={RoutePath.SettingsPage} className={theme.link.link}>
Go to settings
</Link>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Dropdown</h3>
<Dropdown
position="bottomLeft"
trigger="click"
menu={
<div className={theme.dropdown.menu}>
<button type="button" className={theme.dropdown.item}>
Item 1
</button>
<button type="button" className={theme.dropdown.item}>
Item 2
</button>
</div>
}>
<span className={theme.dropdown.text}>Open Dropdown</span>
</Dropdown>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Breadcrumbs</h3>
<Breadcrumbs
parentLinks={[
{ path: RoutePath.Dashboard, title: 'Dashboard' },
{ path: RoutePath.Logs, title: 'Logs' },
]}
currentTitle="Current Page"
/>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Select</h3>
<div className={styles.selectExamples}>
<div className={styles.selectExample}>
<h4 className={styles.exampleTitle}>Basic Select</h4>
<Select
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
]}
onChange={(selected) => console.log('Selected:', selected)}
placeholder="Select value"
/>
</div>
<div className={styles.selectExample}>
<h4 className={styles.exampleTitle}>Multi-select</h4>
<Select
isMulti
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
]}
components={{ MultiValue: CustomMultiValue }}
onChange={(selected) => console.log('Selected:', selected)}
placeholder="Select multiple options"
/>
</div>
<div className={styles.selectExample}>
<h4 className={styles.exampleTitle}>Disabled</h4>
<Select
isDisabled={true}
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]}
onChange={(selected) => console.log('Selected:', selected)}
placeholder="Disabled select"
/>
</div>
<div className={styles.selectExample}>
<h4 className={styles.exampleTitle}>Clearable</h4>
<Select
isClearable={true}
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
]}
onChange={(selected) => console.log('Selected:', selected)}
placeholder="Clearable select"
/>
</div>
<div className={styles.selectExample}>
<h4 className={styles.exampleTitle}>Group Options</h4>
<Select<string>
options={[
{
label: 'Group 1',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
],
},
{
label: 'Group 2',
options: [
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
],
},
]}
onChange={(selected) => console.log('Selected:', selected)}
placeholder="Grouped options"
/>
</div>
</div>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Icons</h3>
<div className={styles.iconsContainer}>
{ICON_VALUES.map((icon) => (
<div key={icon} className={styles.iconItem}>
<Icon icon={icon} className={styles.icon} />
<div className={styles.iconName}>{icon}</div>
</div>
))}
</div>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Typography (theme.title & theme.text)</h3>
<div className={styles.typographySection}>
<div className={styles.typographyBlock}>
<div className={styles.typographyHeading}>Title classes:</div>
<div className={styles.typographyList}>
<div className={theme.title.h0}>.h0 Title Example</div>
<div className={theme.title.h1}>.h1 Title Example</div>
<div className={theme.title.h2}>.h2 Title Example</div>
<div className={theme.title.h3}>.h3 Title Example</div>
<div className={theme.title.h4}>.h4 Title Example</div>
<div className={theme.title.h5}>.h5 Title Example</div>
<div className={theme.title.h6}>.h6 Title Example</div>
</div>
</div>
<div className={styles.typographyBlock}>
<div className={styles.typographyHeading}>Text classes:</div>
<div className={styles.typographyList}>
<div className={theme.text.t1}>.t1 Text Example</div>
<div className={theme.text.t2}>.t2 Text Example</div>
<div className={theme.text.t3}>.t3 Text Example</div>
<div className={theme.text.t4}>.t4 Text Example</div>
</div>
</div>
</div>
<h3 className={cn(theme.title.h5, styles.subtitle)}>Colors</h3>
<ul className={styles.colorsContainer}>
{COLOR_VARIABLES.map((varName) => (
<li key={varName} className={styles.colorSwatch}>
<div className={styles.colorBox} style={{ background: `var(${varName})` }} />
<div className={styles.colorName}>{varName}</div>
</li>
))}
</ul>
</div>
);
};
export default withTranslation()(Expo);

View File

@ -184,3 +184,25 @@
gap: 8px;
flex-direction: column;
}
.deprecationNotice {
padding: 24px;
border: 2px solid #ff9800;
border-radius: 8px;
background-color: #fff8e1;
margin: 24px 0;
}
.storyBookInstructions {
background-color: #f5f5f5;
padding: 16px 24px;
border-radius: 4px;
margin: 16px 0;
}
.storyBookInstructions code {
background-color: #e0e0e0;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}

View File

@ -18,7 +18,6 @@ export enum RoutePath {
CustomRules = 'CustomRules',
BlockedServices = 'BlockedServices',
UserRules = 'UserRules',
Expo = 'Expo',
}
export const Paths: Record<RoutePath, string> = {
@ -36,7 +35,6 @@ export const Paths: Record<RoutePath, string> = {
CustomRules: pathBuilder('custom_rules'),
BlockedServices: pathBuilder('blocked_services'),
UserRules: pathBuilder('user_rules'),
Expo: pathBuilder('expo'),
};
export type LinkParams = Partial<Record<string, string | number>>;

View File

@ -0,0 +1,141 @@
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Checkbox } from '../../common/controls';
const meta: Meta<typeof Checkbox> = {
title: 'Controls/Checkbox',
component: Checkbox,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
checked: {
control: 'boolean',
description: 'Whether the checkbox is checked',
},
disabled: {
control: 'boolean',
description: 'Whether the checkbox is disabled',
},
children: {
control: 'text',
description: 'Label text for the checkbox',
},
className: {
control: 'text',
description: 'CSS class name for the checkbox wrapper',
},
labelClassName: {
control: 'text',
description: 'CSS class name for the label text',
},
overflow: {
control: 'boolean',
description: 'Whether to apply text overflow styling to the label',
},
plusStyle: {
control: 'boolean',
description: 'Use plus/minus icons instead of check/uncheck icons',
},
id: {
control: 'text',
description: 'HTML id attribute for the checkbox input',
},
name: {
control: 'text',
description: 'HTML name attribute for the checkbox input',
},
onChange: { action: 'changed' },
onClick: { action: 'clicked' },
},
};
export default meta;
type Story = StoryObj<typeof Checkbox>;
const CheckboxWithState = (args: any) => {
const [checked, setChecked] = useState(args.checked || false);
return (
<Checkbox
{...args}
checked={checked}
onChange={(e) => {
setChecked(e.target.checked);
args.onChange?.(e);
}}
/>
);
};
export const Default: Story = {
render: CheckboxWithState,
args: {
children: 'Default checkbox',
id: 'default-checkbox',
},
};
export const Checked: Story = {
render: CheckboxWithState,
args: {
children: 'Checked checkbox',
checked: true,
id: 'checked-checkbox',
},
};
export const Disabled: Story = {
render: CheckboxWithState,
args: {
children: 'Disabled checkbox',
disabled: true,
id: 'disabled-checkbox',
},
};
export const DisabledChecked: Story = {
render: CheckboxWithState,
args: {
children: 'Disabled checked checkbox',
checked: true,
disabled: true,
id: 'disabled-checked-checkbox',
},
};
export const PlusStyle: Story = {
render: CheckboxWithState,
args: {
children: 'Plus/minus style checkbox',
plusStyle: true,
id: 'plus-style-checkbox',
},
};
export const PlusStyleChecked: Story = {
render: CheckboxWithState,
args: {
children: 'Plus/minus style checked',
plusStyle: true,
checked: true,
id: 'plus-style-checked-checkbox',
},
};
export const WithOverflow: Story = {
render: CheckboxWithState,
args: {
children: 'This is a very long label text that should demonstrate the overflow behavior when the text is too long to fit in the available space',
overflow: true,
id: 'overflow-checkbox',
},
};
export const WithoutLabel: Story = {
render: CheckboxWithState,
args: {
id: 'no-label-checkbox',
},
};

View File

@ -0,0 +1,119 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Input } from '../../common/controls';
import { Icon } from 'panel/common/ui';
const meta: Meta<typeof Input> = {
title: 'Controls/Input',
component: Input,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
description: 'Label text displayed above the input field',
},
placeholder: {
control: 'text',
description: 'Placeholder text shown when input is empty',
},
value: {
control: 'text',
description: 'Current value of the input field',
},
disabled: {
control: 'boolean',
description: 'Whether the input is disabled and cannot be interacted with',
},
error: {
control: 'boolean',
description: 'Whether the input is in an error state',
},
errorMessage: {
control: 'text',
description: 'Error message displayed below the input when in error state',
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Size variant of the input (small, medium, large)',
},
borderless: {
control: 'boolean',
description: 'Whether to remove the input border styling',
},
invalid: {
control: 'boolean',
description: 'Whether the input is in an invalid state (visual styling)',
},
maxLength: {
control: 'number',
description: 'Maximum number of characters allowed in the input',
},
type: {
control: 'select',
options: ['text', 'password', 'email', 'number', 'tel', 'url'],
description: 'HTML input type attribute',
},
autoFocus: {
control: 'boolean',
description: 'Whether the input should automatically focus when mounted',
},
prefixIcon: { control: false },
suffixIcon: { control: false },
onChange: { action: 'changed' },
onBlur: { action: 'blurred' },
onFocus: { action: 'focused' },
},
};
export default meta;
type Story = StoryObj<typeof Input>;
export const Default: Story = {
args: {
placeholder: 'Enter text...',
},
};
export const WithLabel: Story = {
args: {
label: 'Input Label',
placeholder: 'Enter text...',
},
};
export const WithValue: Story = {
args: {
label: 'Input with Value',
value: 'Example text',
},
};
export const Disabled: Story = {
args: {
label: 'Disabled Input',
placeholder: 'Cannot edit this field',
disabled: true,
},
};
export const WithError: Story = {
args: {
label: 'Input with error',
value: 'Invalid value',
error: true,
errorMessage: 'This field has an error',
},
};
export const WithIcons: Story = {
args: {
label: 'Input with icons',
prefixIcon: <Icon icon="check" />,
suffixIcon: <Icon icon="dot" />,
},
};

View File

@ -0,0 +1,101 @@
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Radio } from '../../common/controls';
const meta: Meta<typeof Radio> = {
title: 'Controls/Radio',
component: Radio,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
options: {
control: false,
description: 'Array of radio options with text and value properties',
},
value: {
control: false,
description: 'Currently selected value',
},
disabled: {
control: 'boolean',
description: 'Whether all radio options are disabled',
},
className: {
control: 'text',
description: 'CSS class name for individual radio items',
},
wrapClass: {
control: 'text',
description: 'CSS class name for the radio group wrapper',
},
handleChange: { action: 'changed' },
},
};
export default meta;
type Story = StoryObj<typeof Radio>;
const RadioWithState = (args: any) => {
const [value, setValue] = useState(args.value);
return (
<Radio
{...args}
value={value}
handleChange={(newValue) => {
setValue(newValue);
args.handleChange?.(newValue);
}}
/>
);
};
export const Default: Story = {
render: RadioWithState,
args: {
options: [
{ text: 'Option 1', value: 'option1' },
{ text: 'Option 2', value: 'option2' },
{ text: 'Option 3', value: 'option3' },
],
value: 'option1',
},
};
export const Disabled: Story = {
render: RadioWithState,
args: {
options: [
{ text: 'Option 1', value: 'option1' },
{ text: 'Option 2', value: 'option2' },
{ text: 'Option 3', value: 'option3' },
],
value: 'option2',
disabled: true,
},
};
export const NumberValues: Story = {
render: RadioWithState,
args: {
options: [
{ text: 'Small (1)', value: 1 },
{ text: 'Medium (2)', value: 2 },
{ text: 'Large (3)', value: 3 },
],
value: 2,
},
};
export const BooleanValues: Story = {
render: RadioWithState,
args: {
options: [
{ text: 'Yes', value: true },
{ text: 'No', value: false },
],
value: true,
},
};

View File

@ -0,0 +1,169 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Select } from '../../common/controls';
const meta: Meta<typeof Select> = {
title: 'Controls/Select',
component: Select,
parameters: {
layout: 'padded',
docs: {
story: {
height: '250px',
},
},
},
tags: ['autodocs'],
argTypes: {
options: {
control: false,
description: 'Array of options or option groups to display in the select',
},
value: {
control: false,
description: 'Currently selected value(s)',
},
components: {
control: false,
description: 'Custom components to override default select components',
},
formatGroupLabel: {
control: false,
description: 'Function to format group labels',
},
isDisabled: {
control: 'boolean',
description: 'Whether the select is disabled and cannot be interacted with',
},
isMulti: {
control: 'boolean',
description: 'Allow multiple selections to be made',
},
isClearable: {
control: 'boolean',
description: 'Allow clearing the current selection with a clear button',
},
isSearchable: {
control: 'boolean',
description: 'Whether the select options can be searched/filtered',
},
isLoading: {
control: 'boolean',
description: 'Show loading indicator in the select',
},
size: {
control: 'select',
options: ['auto', 'small', 'medium', 'big', 'big-limit', 'responsive'],
description: 'Size variant of the select component',
},
height: {
control: 'select',
options: ['small', 'medium', 'big', 'big-mobile'],
description: 'Height variant of the select component',
},
menuSize: {
control: 'select',
options: ['small', 'medium', 'big', 'large'],
description: 'Size variant of the dropdown menu',
},
menuPlacement: {
control: 'select',
options: ['top', 'bottom', 'auto'],
description: 'Placement of the dropdown menu relative to the select',
},
borderless: {
control: 'boolean',
description: 'Whether to remove border styling from the select',
},
closeMenuOnSelect: {
control: 'boolean',
description: 'Whether to close the menu after selecting an option',
},
onMenuOpen: { action: 'menu opened' },
onMenuClose: { action: 'menu closed' },
},
};
export default meta;
type Story = StoryObj<typeof Select>;
export const Default: Story = {
args: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
],
placeholder: 'Select an option',
},
};
export const WithDefaultValue: Story = {
args: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
],
},
};
export const Disabled: Story = {
args: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
],
isDisabled: true,
placeholder: 'Disabled select',
},
};
export const Clearable: Story = {
args: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
],
isClearable: true,
placeholder: 'Clearable select',
},
};
export const MultiSelect: Story = {
args: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
],
isMulti: true,
placeholder: 'Select multiple options',
},
};
export const GroupedOptions: Story = {
args: {
options: [
{
label: 'Group 1',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
],
},
{
label: 'Group 2',
options: [
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
],
},
],
placeholder: 'Grouped options',
},
};

View File

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Switch } from '../../common/controls';
const SwitchWrapper = (props: any) => {
const [checked, setChecked] = useState(props.checked || false);
const handleChange = (value: boolean) => {
setChecked(value);
props.onChange?.(value);
};
return <Switch {...props} checked={checked} onChange={handleChange} />;
};
const meta: Meta<typeof Switch> = {
title: 'Controls/Switch',
component: SwitchWrapper,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
id: {
control: 'text',
description: 'Unique identifier for the switch input element',
},
checked: {
control: 'boolean',
description: 'Whether the switch is in the checked/on state',
},
disabled: {
control: 'boolean',
description: 'Whether the switch is disabled and cannot be toggled',
},
children: {
control: 'text',
description: 'Label content displayed next to the switch',
},
className: {
control: 'text',
description: 'Additional CSS class for the switch container',
},
labelClassName: {
control: 'text',
description: 'Additional CSS class for the label text',
},
wrapperClassName: {
control: 'text',
description: 'Additional CSS class for the wrapper element',
},
handleChange: { action: 'changed' },
},
};
export default meta;
type Story = StoryObj<typeof Switch>;
export const Default: Story = {
args: {
children: 'Default Switch',
},
};
export const Checked: Story = {
args: {
children: 'Checked Switch',
checked: true,
},
};
export const Disabled: Story = {
args: {
children: 'Disabled Switch',
disabled: true,
},
};
export const DisabledChecked: Story = {
args: {
children: 'Disabled Checked Switch',
disabled: true,
checked: true,
},
};

View File

@ -0,0 +1,92 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Textarea } from '../../common/controls';
const meta: Meta<typeof Textarea> = {
title: 'Controls/Textarea',
component: Textarea,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
description: 'Label text displayed above the textarea field',
},
placeholder: {
control: 'text',
description: 'Placeholder text shown when textarea is empty',
},
value: {
control: 'text',
description: 'Current value of the textarea field',
},
disabled: {
control: 'boolean',
description: 'Whether the textarea is disabled and cannot be interacted with',
},
errorMessage: {
control: 'text',
description: 'Error message displayed below the textarea when in error state',
},
rows: {
control: 'number',
description: 'Number of visible text lines for the textarea',
},
cols: {
control: 'number',
description: 'Visible width of the textarea in characters',
},
maxLength: {
control: 'number',
description: 'Maximum number of characters allowed in the textarea',
},
autoFocus: {
control: 'boolean',
description: 'Whether the textarea should automatically focus when mounted',
},
onChange: { action: 'changed' },
onBlur: { action: 'blurred' },
onFocus: { action: 'focused' },
},
};
export default meta;
type Story = StoryObj<typeof Textarea>;
export const Default: Story = {
args: {
placeholder: 'Enter text...',
},
};
export const WithLabel: Story = {
args: {
label: 'Label',
placeholder: 'Enter text...',
},
};
export const WithValue: Story = {
args: {
label: 'Textarea with value',
value: 'Example text',
},
};
export const Disabled: Story = {
args: {
label: 'Disabled textarea',
placeholder: 'Cannot edit this field',
disabled: true,
},
};
export const WithError: Story = {
args: {
label: 'Textarea with error',
value: 'Invalid value',
errorMessage: 'This field has an error',
},
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import theme from '../../lib/theme';
const Typography = () => {
return (
<div>
<div>
<div className={theme.title.h0}>.h0 Title Example</div>
<div className={theme.title.h1}>.h1 Title Example</div>
<div className={theme.title.h2}>.h2 Title Example</div>
<div className={theme.title.h3}>.h3 Title Example</div>
<div className={theme.title.h4}>.h4 Title Example</div>
<div className={theme.title.h5}>.h5 Title Example</div>
<div className={theme.title.h6}>.h6 Title Example</div>
</div>
<div>
<div>
<div className={theme.text.t1}>.t1 Text Example</div>
<div className={theme.text.t2}>.t2 Text Example</div>
<div className={theme.text.t3}>.t3 Text Example</div>
<div className={theme.text.t4}>.t4 Text Example</div>
</div>
</div>
</div>
);
};
const meta: Meta<typeof Typography> = {
title: 'Theme/Typography',
component: Typography,
parameters: {
layout: 'padded',
},
};
export default meta;
type Story = StoryObj<typeof Typography>;
export const TypographyStyles: Story = {
render: () => <Typography />,
};

View File

@ -0,0 +1,9 @@
/**
* Helper type utilities for Storybook stories
*/
/**
* Type assertion helper to suppress TypeScript errors in story args
* Use this when the component props and Storybook's typing don't align perfectly
*/
export const asStoryArgs = <T>(args: any): T => args as T;

View File

@ -0,0 +1,81 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Button } from '../../common/ui';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'ghost'],
description: 'Button variant',
},
size: {
control: 'select',
options: ['small', 'medium', 'big'],
description: 'Button size',
},
disabled: {
control: 'boolean',
description: 'Disabled state',
},
onClick: { action: 'clicked' },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
export const Danger: Story = {
args: {
variant: 'danger',
children: 'Danger Button',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
children: 'Ghost Button',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled Button',
},
};
export const Small: Story = {
args: {
size: 'small',
children: 'Small Button',
},
};
export const Big: Story = {
args: {
size: 'big',
children: 'Big Button',
},
};

View File

@ -0,0 +1,141 @@
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { ConfirmDialog } from '../../common/ui';
import { Button } from '../../common/ui';
const meta: Meta<typeof ConfirmDialog> = {
title: 'UI/ConfirmDialog',
component: ConfirmDialog,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
title: {
control: 'text',
description: 'Dialog title',
},
text: {
control: 'text',
description: 'Dialog body text',
},
buttonText: {
control: 'text',
description: 'Confirm button text',
},
cancelText: {
control: 'text',
description: 'Cancel button text',
},
buttonVariant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'ghost'],
description: 'Confirm button variant',
},
submitId: {
control: 'text',
description: 'HTML id for the confirm button',
},
cancelId: {
control: 'text',
description: 'HTML id for the cancel button',
},
wrapClassName: {
control: 'text',
description: 'CSS class name for the dialog wrapper',
},
customFooter: {
control: false,
description: 'Custom footer content to replace default buttons',
},
onClose: { action: 'closed' },
onConfirm: { action: 'confirmed' },
},
};
export default meta;
type Story = StoryObj<typeof ConfirmDialog>;
const ConfirmDialogWithTrigger = (args: any) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
{isOpen && (
<ConfirmDialog
{...args}
onClose={() => {
setIsOpen(false);
args.onClose?.();
}}
onConfirm={() => {
setIsOpen(false);
args.onConfirm?.();
}}
/>
)}
</div>
);
};
export const Default: Story = {
render: ConfirmDialogWithTrigger,
args: {
title: 'Confirm Action',
text: 'Are you sure you want to perform this action?',
buttonText: 'Confirm',
cancelText: 'Cancel',
},
};
export const DeleteConfirmation: Story = {
render: ConfirmDialogWithTrigger,
args: {
title: 'Delete Item',
text: 'This action cannot be undone. Are you sure you want to delete this item?',
buttonText: 'Delete',
cancelText: 'Cancel',
buttonVariant: 'danger',
},
};
export const WithoutTitle: Story = {
render: ConfirmDialogWithTrigger,
args: {
text: 'Are you sure you want to continue?',
buttonText: 'Yes',
cancelText: 'No',
},
};
export const WithoutText: Story = {
render: ConfirmDialogWithTrigger,
args: {
title: 'Confirm',
buttonText: 'OK',
cancelText: 'Cancel',
},
};
export const LongContent: Story = {
render: ConfirmDialogWithTrigger,
args: {
title: 'Important Notice',
text: 'This is a longer confirmation dialog with more detailed information. It explains the consequences of the action and provides additional context to help the user make an informed decision. The text can span multiple lines and contain important details about what will happen when the user confirms the action.',
buttonText: 'I Understand',
cancelText: 'Cancel',
},
};
export const CustomButtons: Story = {
render: ConfirmDialogWithTrigger,
args: {
title: 'Save Changes',
text: 'You have unsaved changes. What would you like to do?',
buttonText: 'Save',
cancelText: 'Discard',
buttonVariant: 'primary',
},
};

View File

@ -0,0 +1,130 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Dropdown } from '../../common/ui';
import theme from 'panel/lib/theme';
const meta: Meta<typeof Dropdown> = {
title: 'UI/Dropdown',
component: Dropdown,
parameters: {
layout: 'centered',
docs: {
story: {
height: '200px',
},
},
},
tags: ['autodocs'],
argTypes: {
trigger: {
control: 'select',
options: ['click', 'hover'],
description: 'How the dropdown is triggered',
},
position: {
control: 'select',
options: ['bottomLeft', 'bottomCenter', 'bottomRight', 'topLeft', 'topCenter', 'topRight'],
description: 'Position of the dropdown overlay',
},
disabled: {
control: 'boolean',
description: 'Whether the dropdown is disabled',
},
noIcon: {
control: 'boolean',
description: 'Hide the dropdown arrow icon',
},
widthAuto: {
control: 'boolean',
description: 'Auto width for the overlay',
},
flex: {
control: 'boolean',
description: 'Use flex layout',
},
autoClose: {
control: 'boolean',
description: 'Auto close dropdown after timeout',
},
disableAnimation: {
control: 'boolean',
description: 'Disable dropdown animation',
},
minOverlayWidthMatchTrigger: {
control: 'boolean',
description: 'Match overlay width to trigger width',
},
className: {
control: 'text',
description: 'CSS class for the dropdown wrapper',
},
overlayClassName: {
control: 'text',
description: 'CSS class for the dropdown overlay',
},
menu: {
control: false,
description: 'Dropdown menu content',
},
children: {
control: false,
description: 'Dropdown trigger content',
},
onOpenChange: { action: 'openChanged' },
},
};
export default meta;
type Story = StoryObj<typeof Dropdown>;
const actionMenu = (
<div className={theme.dropdown.menu}>
<div className={theme.dropdown.item}>Edit</div>
<div className={theme.dropdown.item}>Duplicate</div>
<div className={theme.dropdown.item}>Delete</div>
</div>
);
export const Default: Story = {
args: {
trigger: 'click',
menu: actionMenu,
children: <div className={theme.dropdown.trigger}>Click me</div>,
},
};
export const HoverTrigger: Story = {
args: {
trigger: 'hover',
menu: actionMenu,
children: <div className={theme.dropdown.trigger}>Hover me</div>,
},
};
export const WithoutIcon: Story = {
args: {
trigger: 'click',
menu: actionMenu,
noIcon: true,
children: <div className={theme.dropdown.trigger}>No arrow icon</div>,
},
};
export const DifferentPositions: Story = {
args: {
trigger: 'click',
menu: actionMenu,
position: 'topLeft',
children: <div className={theme.dropdown.trigger}>Top Left Position</div>,
},
};
export const AutoClose: Story = {
args: {
trigger: 'click',
menu: actionMenu,
autoClose: true,
children: <div className={theme.dropdown.trigger}>Auto Close (1s delay)</div>,
},
};

View File

@ -0,0 +1,59 @@
.container {
font-family: var(--font-family-system);
padding: 20px;
}
.title {
font-size: 24px;
font-weight: 600;
margin-bottom: 24px;
color: var(--default-main-text);
}
.description {
color: var(--default-description-text);
margin-bottom: 20px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
margin-top: 16px;
}
.iconItem {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
border: 1px solid var(--default-item-divider);
border-radius: 8px;
background-color: var(--default-page-background);
transition: all 0.2s ease;
cursor: pointer;
}
.iconItem:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.iconContainer {
width: 24px;
height: 24px;
margin-bottom: 8px;
color: var(--default-gray-icons);
display: flex;
align-items: center;
justify-content: center;
}
.iconName {
font-size: 12px;
text-align: center;
color: var(--default-description-text);
font-family: var(--font-family-monospace);
word-break: break-word;
line-height: 1.2;
}

View File

@ -0,0 +1,47 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { ICON_VALUES } from '../../common/ui/Icons';
import { Icon } from '../../common/ui';
import s from './Icons.module.pcss';
const Icons = () => {
const handleIconClick = (iconName: string) => {
navigator.clipboard.writeText(iconName);
};
return (
<div className={s.container}>
<h2 className={s.title}>Icon Library</h2>
<p className={s.description}>
Click any icon to copy its name to clipboard. Total: {ICON_VALUES.length} icons
</p>
<div className={s.grid}>
{ICON_VALUES.map((icon) => (
<div key={icon} className={s.iconItem} onClick={() => handleIconClick(icon)}>
<div className={s.iconContainer}>
<Icon icon={icon} />
</div>
<div className={s.iconName}>{icon}</div>
</div>
))}
</div>
</div>
);
};
const meta: Meta<typeof Icons> = {
title: 'UI/Icons',
component: Icons,
parameters: {
layout: 'padded',
},
};
export default meta;
type Story = StoryObj<typeof Icons>;
export const IconGallery: Story = {
render: () => <Icons />,
};

View File

@ -0,0 +1,176 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Loader, InlineLoader, Button } from '../../common/ui';
const meta: Meta<typeof Loader> = {
title: 'UI/Loader',
component: Loader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
color: {
control: 'color',
description: 'Color of the loader icon',
},
className: {
control: 'text',
description: 'CSS class name for the loader icon',
},
overlay: {
control: 'boolean',
description: 'Whether to show the loader with an overlay background',
},
overlayClassName: {
control: 'text',
description: 'CSS class name for the overlay wrapper',
},
icon: {
control: 'text',
description: 'Icon type to use for the loader (defaults to "loader")',
},
},
};
export default meta;
type Story = StoryObj<typeof Loader>;
export const Default: Story = {
args: {},
};
export const WithColor: Story = {
args: {
color: '#007bff',
},
};
export const WithOverlay: Story = {
args: {
overlay: true,
},
render: (args) => (
<div
style={{
position: 'relative',
width: '300px',
height: '200px',
background: '#f5f5f5',
border: '1px solid #ddd',
}}>
<div style={{ padding: '20px' }}>
<h3>Content behind overlay</h3>
<p>This content should be covered by the loader overlay.</p>
</div>
<Loader {...args} />
</div>
),
};
export const CustomIcon: Story = {
args: {
icon: 'refresh' as any,
},
};
export const CustomClassName: Story = {
args: {
className: 'custom-loader-class',
},
};
export const ColoredOverlay: Story = {
args: {
overlay: true,
color: '#28a745',
},
render: (args) => (
<div
style={{
position: 'relative',
width: '300px',
height: '200px',
background: '#f8f9fa',
border: '1px solid #dee2e6',
}}>
<div style={{ padding: '20px' }}>
<h3>Loading Content</h3>
<p>Please wait while we load your data...</p>
</div>
<Loader {...args} />
</div>
),
};
// InlineLoader stories
const InlineLoaderMeta: Meta<typeof InlineLoader> = {
title: 'UI/InlineLoader',
component: InlineLoader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
className: {
control: 'text',
description: 'CSS class name for the inline loader',
},
icon: {
control: 'text',
description: 'Icon type to use for the loader (defaults to "loader")',
},
},
};
export { InlineLoaderMeta as InlineLoaderStories };
type InlineLoaderStory = StoryObj<typeof InlineLoader>;
export const InlineDefault: InlineLoaderStory = {
args: {},
};
export const InlineInText: InlineLoaderStory = {
render: (args) => (
<div>
<p>
Loading data <InlineLoader {...args} /> please wait...
</p>
</div>
),
};
export const InlineInButton: InlineLoaderStory = {
render: (args) => (
<Button variant="primary" size="medium" leftAddon={<InlineLoader {...args} />}>
Loading...
</Button>
),
};
export const InlineCustomIcon: InlineLoaderStory = {
args: {
icon: 'refresh' as any,
},
};
export const InlineMultiple: InlineLoaderStory = {
render: (args) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>Small:</span>
<InlineLoader {...args} className="small-loader" />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>Default:</span>
<InlineLoader {...args} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>Large:</span>
<InlineLoader {...args} className="large-loader" />
</div>
</div>
),
};