diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 00000000..689d29fb --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,32 @@ +import type { FrameworkOptions, StorybookConfig } from 'storybook-solidjs-vite' + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/addon-a11y', + '@storybook/addon-themes' + ], + framework: { + name: 'storybook-solidjs-vite', + options: { + builder: { + viteConfigPath: './app.config.ts' + } + } as FrameworkOptions + }, + docs: { + autodocs: 'tag' + }, + previewHead: (head) => ` + ${head} + + ` +} +export default config diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 00000000..a09ca9bd --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,34 @@ +import { withThemeByClassName } from '@storybook/addon-themes' +import '../src/styles/_imports.scss' + +const preview = { + parameters: { + themes: { + default: 'light', + list: [ + { name: 'light', class: '', color: '#f8fafc' }, + { name: 'dark', class: 'dark', color: '#0f172a' } + ] + }, + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/ + } + } + } +} + +export default preview + +export const decorators = [ + withThemeByClassName({ + themes: { + light: '', + dark: 'dark' + }, + defaultTheme: 'light', + parentSelector: 'body' + }) +] diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts new file mode 100644 index 00000000..91a12ca8 --- /dev/null +++ b/.storybook/test-runner.ts @@ -0,0 +1,22 @@ +import type { TestRunnerConfig } from '@storybook/test-runner' +import { checkA11y, injectAxe } from 'axe-playwright' + +/* + * See https://storybook.js.org/docs/react/writing-tests/test-runner#test-hook-api-experimental + * to learn more about the test-runner hooks API. + */ +const a11yConfig: TestRunnerConfig = { + async preRender(page) { + await injectAxe(page) + }, + async postRender(page) { + await checkA11y(page, '#storybook-root', { + detailedReport: true, + detailedReportOptions: { + html: true + } + }) + } +} + +module.exports = a11yConfig diff --git a/app.config.ts b/app.config.ts index 17f1372d..f8a88c67 100644 --- a/app.config.ts +++ b/app.config.ts @@ -36,7 +36,8 @@ export default defineConfig({ devOverlay: true, build: { chunkSizeWarningLimit: 1024, - target: 'esnext' + target: 'esnext', + sourcemap: true }, vite: { envPrefix: 'PUBLIC_', diff --git a/package.json b/package.json index 41f04d97..33ca5223 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "fix": "npx @biomejs/biome check . --fix && stylelint **/*.{scss,css} --fix", "format": "npx @biomejs/biome format src/. --write", "postinstall": "npm run codegen && npx patch-package", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "storybook": "storybook dev -p 6006", + "storybook:test": "test-storybook", + "build-storybook": "storybook build" }, "devDependencies": { "@authorizerdev/authorizer-js": "^2.0.3", @@ -37,6 +40,19 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.14.3", "@solidjs/start": "^1.0.6", + "@storybook/addon-a11y": "^8.2.9", + "@storybook/addon-actions": "^8.2.9", + "@storybook/addon-controls": "^8.2.9", + "@storybook/addon-essentials": "^8.2.9", + "@storybook/addon-interactions": "^8.2.9", + "@storybook/addon-links": "^8.2.9", + "@storybook/addon-themes": "^8.2.9", + "@storybook/addon-viewport": "^8.2.9", + "@storybook/blocks": "^8.2.9", + "@storybook/html": "^8.2.9", + "@storybook/react": "^8.2.9", + "@storybook/test-runner": "^0.19.1", + "@storybook/testing-library": "^0.2.2", "@tiptap/core": "^2.6.6", "@tiptap/extension-blockquote": "^2.6.6", "@tiptap/extension-bold": "^2.6.6", @@ -68,9 +84,10 @@ "@tiptap/extension-youtube": "^2.6.6", "@types/cookie": "^0.6.0", "@types/cookie-signature": "^1.1.2", - "@types/node": "^22.5.0", + "@types/node": "^22.5.2", "@types/throttle-debounce": "^5.0.2", "@urql/core": "^5.0.6", + "axe-playwright": "^2.0.2", "bootstrap": "^5.3.3", "clsx": "^2.1.1", "cookie": "^0.6.0", @@ -83,29 +100,32 @@ "i18next-http-backend": "^2.6.1", "i18next-icu": "^2.3.0", "intl-messageformat": "^10.5.14", - "javascript-time-ago": "^2.5.10", + "javascript-time-ago": "^2.5.11", "patch-package": "^8.0.0", "prosemirror-history": "^1.4.1", "prosemirror-trailing-node": "^2.0.9", - "prosemirror-view": "^1.34.0", + "prosemirror-view": "^1.34.1", "rollup-plugin-visualizer": "^5.12.0", "sass": "1.76.0", - "solid-js": "^1.8.21", + "solid-js": "^1.8.22", "solid-popper": "^0.3.0", "solid-tiptap": "0.7.0", "solid-transition-group": "^0.2.3", - "stylelint": "^16.8.2", + "storybook": "^8.2.9", + "storybook-solidjs": "^1.0.0-beta.2", + "storybook-solidjs-vite": "^1.0.0-beta.2", + "stylelint": "^16.9.0", "stylelint-config-recommended": "^14.0.1", "stylelint-config-standard-scss": "^13.1.0", "stylelint-order": "^6.0.4", - "stylelint-scss": "^6.5.0", - "swiper": "^11.1.10", + "stylelint-scss": "^6.5.1", + "swiper": "^11.1.12", "throttle-debounce": "^5.0.2", "tslib": "^2.7.0", "typescript": "^5.5.4", "typograf": "^7.4.1", "uniqolor": "^1.1.1", - "vinxi": "^0.4.1", + "vinxi": "^0.4.2", "vite-plugin-mkcert": "^1.17.6", "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-sass-dts": "^1.3.25", diff --git a/src/components/atoms/Button/Button.module.scss b/src/components/atoms/Button/Button.module.scss new file mode 100644 index 00000000..495c3466 --- /dev/null +++ b/src/components/atoms/Button/Button.module.scss @@ -0,0 +1,195 @@ +.button { + border-radius: 2px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + + &.primary { + background: var(--background-color-invert); + color: var(--default-color-invert); + + &:hover { + color: #ccc; + } + + &:active { + color: #9fa1a7; + } + } + + &.secondary { + border: 1px solid #f7f7f7; + background: #f7f7f7; + color: #141414; + + &:hover { + background: #000; + color: #fff; + } + } + + &.danger { + border: 3px solid var(--danger-color); + background: var(--background-color); + color: var(--danger-color); + + &:hover { + background: var(--danger-color); + color: #fff; + } + } + + &.inline { + font-weight: 700; + font-size: 16px; + line-height: 21px; + color: #696969; + + &:hover, + &:active { + text-decoration: underline; + color: #141414; + } + } + + &.light { + font-weight: inherit; + padding: 0; + } + + &.outline, + &.bordered { + border: 3px solid #f2f2f2; + border-radius: 1.2em; + cursor: pointer; + font-weight: bold; + margin-right: 0.8em; + min-width: auto !important; + padding: 0; + transition: + border-color 0.3s, + background-color 0.3s, + color 0.3s; + + &:hover, + &:active { + background: var(--link-color); + border-color: var(--link-color); + color: var(--link-hover-color); + + :global(.icon) { + filter: var(--icon-filter-hover); + } + } + + :global(.icon) { + margin: 0 -0.5em; + filter: var(--icon-filter); + transition: filter 0.3s; + } + } + + &.bordered { + border-radius: 2px; + border: 2px solid #000; + font-size: 16px; + font-weight: 500; + } + + &:disabled, + &:disabled:hover { + cursor: default; + color: #9fa1a7; + background: #f6f6f6; + } + + &.loading, + &.loading:hover { + background: #f6f6f6; + } + + &.L { + height: 56px; + min-width: 80px; + font-size: 20px; + padding: 16px 20px; + } + + &.M { + height: 40px; + min-width: 64px; + font-size: 17px; + padding: 8px 16px; + } + + &.S { + height: 32px; + min-width: 53px; + font-size: 15px; + padding: 1rem 1.2rem; + } + + &.subscribeButton { + aspect-ratio: auto; + background-color: #000; + border: 2px solid #000; + border-radius: 0.8rem; + color: #fff; + float: none; + padding-bottom: 0.6rem; + padding-top: 0.6rem; + width: 9em; + + .icon { + img { + filter: invert(1); + } + } + + &:hover { + background: #fff; + color: #000; + + .icon img { + filter: invert(0) !important; + } + + .buttonSubscribeLabel { + display: none; + } + + .buttonSubscribeLabelHovered { + display: block; + } + } + + .buttonSubscribeLabelHovered { + display: none; + } + + img { + vertical-align: text-top; + } + } + + &.followed { + background: #fff; + color: #000; + + .icon img { + filter: invert(0); + } + + &:hover { + background: #000; + color: #fff; + + .icon img { + filter: invert(1) !important; + } + } + } +} \ No newline at end of file diff --git a/src/components/atoms/Button/Button.stories.tsx b/src/components/atoms/Button/Button.stories.tsx new file mode 100644 index 00000000..f41cd338 --- /dev/null +++ b/src/components/atoms/Button/Button.stories.tsx @@ -0,0 +1,15 @@ +// src/components/atoms/Button/Button.stories.tsx +import type { Meta } from '@storybook/html' +import { Button } from './Button' + +// Примените корректную типизацию для Storybook +const meta: Meta = { + title: 'Atoms/Button', + argTypes: { + label: { control: 'text' }, + primary: { control: 'boolean' }, + onClick: { action: 'clicked' } + } +} + +export default meta diff --git a/src/components/atoms/Button/Button.tsx b/src/components/atoms/Button/Button.tsx new file mode 100644 index 00000000..11434e0b --- /dev/null +++ b/src/components/atoms/Button/Button.tsx @@ -0,0 +1,50 @@ +import type { JSX } from 'solid-js' + +import { clsx } from 'clsx' + +import styles from './Button.module.scss' + +export type ButtonVariant = 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline' | 'danger' +type Props = { + title?: string + value: string | JSX.Element + size?: 'S' | 'M' | 'L' + variant?: ButtonVariant + type?: 'submit' | 'button' + loading?: boolean + disabled?: boolean + onClick?: (event?: MouseEvent) => void + class?: string + ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void) + isSubscribeButton?: boolean +} + +export const Button = (props: Props) => { + return ( + + ) +} diff --git a/src/components/atoms/Button/index.ts b/src/components/atoms/Button/index.ts new file mode 100644 index 00000000..4d0a670f --- /dev/null +++ b/src/components/atoms/Button/index.ts @@ -0,0 +1 @@ +export { Button } from './Button'