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'