mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-10-26 11:27:18 +00:00
add storybook
This commit is contained in:
parent
25d9cb7fc6
commit
0902003df2
3
.gitignore
vendored
3
.gitignore
vendored
@ -43,3 +43,6 @@ coverage.txt
|
||||
node_modules/
|
||||
|
||||
!/build/gitkeep
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
52
client_v2/.storybook/main.ts
Normal file
52
client_v2/.storybook/main.ts
Normal 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;
|
||||
26
client_v2/.storybook/preview.ts
Normal file
26
client_v2/.storybook/preview.ts
Normal 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;
|
||||
12
client_v2/.storybook/tsconfig.json
Normal file
12
client_v2/.storybook/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@ -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
1165
client_v2/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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']>(
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>>;
|
||||
|
||||
141
client_v2/src/stories/controls/Checkbox.stories.tsx
Normal file
141
client_v2/src/stories/controls/Checkbox.stories.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
119
client_v2/src/stories/controls/Input.stories.tsx
Normal file
119
client_v2/src/stories/controls/Input.stories.tsx
Normal 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" />,
|
||||
},
|
||||
};
|
||||
101
client_v2/src/stories/controls/Radio.stories.tsx
Normal file
101
client_v2/src/stories/controls/Radio.stories.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
169
client_v2/src/stories/controls/Select.stories.tsx
Normal file
169
client_v2/src/stories/controls/Select.stories.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
86
client_v2/src/stories/controls/Switch.stories.tsx
Normal file
86
client_v2/src/stories/controls/Switch.stories.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
92
client_v2/src/stories/controls/Textarea.stories.tsx
Normal file
92
client_v2/src/stories/controls/Textarea.stories.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
44
client_v2/src/stories/theme/Typography.stories.tsx
Normal file
44
client_v2/src/stories/theme/Typography.stories.tsx
Normal 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 />,
|
||||
};
|
||||
9
client_v2/src/stories/types.ts
Normal file
9
client_v2/src/stories/types.ts
Normal 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;
|
||||
81
client_v2/src/stories/ui/Button.stories.tsx
Normal file
81
client_v2/src/stories/ui/Button.stories.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
141
client_v2/src/stories/ui/ConfirmDialog.stories.tsx
Normal file
141
client_v2/src/stories/ui/ConfirmDialog.stories.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
130
client_v2/src/stories/ui/Dropdown.stories.tsx
Normal file
130
client_v2/src/stories/ui/Dropdown.stories.tsx
Normal 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>,
|
||||
},
|
||||
};
|
||||
59
client_v2/src/stories/ui/Icons.module.pcss
Normal file
59
client_v2/src/stories/ui/Icons.module.pcss
Normal 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;
|
||||
}
|
||||
47
client_v2/src/stories/ui/Icons.stories.tsx
Normal file
47
client_v2/src/stories/ui/Icons.stories.tsx
Normal 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 />,
|
||||
};
|
||||
176
client_v2/src/stories/ui/Loader.stories.tsx
Normal file
176
client_v2/src/stories/ui/Loader.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user