diff --git a/web/.eslintrc.js b/web/.eslintrc.js
index 8ea3a0623..79673a7c8 100644
--- a/web/.eslintrc.js
+++ b/web/.eslintrc.js
@@ -3,7 +3,13 @@ module.exports = {
browser: true,
es2021: true,
},
- extends: ['plugin:react/recommended', 'airbnb', 'prettier', 'plugin:@next/next/recommended'],
+ extends: [
+ 'plugin:react/recommended',
+ 'airbnb',
+ 'prettier',
+ 'plugin:@next/next/recommended',
+ 'plugin:storybook/recommended',
+ ],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
@@ -16,21 +22,28 @@ module.exports = {
rules: {
'prettier/prettier': 'error',
'react/react-in-jsx-scope': 'off',
- 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.tsx'] }],
+ 'react/jsx-filename-extension': [
+ 1,
+ {
+ extensions: ['.js', '.jsx', '.tsx'],
+ },
+ ],
'react/jsx-props-no-spreading': 'off',
'react/jsx-no-bind': 'off',
'react/function-component-definition': 'off',
-
'@next/next/no-img-element': 'off',
-
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-console': 'off',
-
'no-use-before-define': [0],
'@typescript-eslint/no-use-before-define': [1],
-
- 'react/jsx-no-target-blank': [1, { allowReferrer: false, enforceDynamicLinks: 'always' }],
+ 'react/jsx-no-target-blank': [
+ 1,
+ {
+ allowReferrer: false,
+ enforceDynamicLinks: 'always',
+ },
+ ],
'import/extensions': [
'error',
'ignorePackages',
diff --git a/web/.storybook/main.js b/web/.storybook/main.js
new file mode 100644
index 000000000..bbe91b517
--- /dev/null
+++ b/web/.storybook/main.js
@@ -0,0 +1,11 @@
+module.exports = {
+ stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ '@storybook/preset-scss',
+ '@storybook/addon-postcss',
+ ],
+ framework: '@storybook/react',
+};
diff --git a/web/.storybook/preview.js b/web/.storybook/preview.js
new file mode 100644
index 000000000..2105d9b21
--- /dev/null
+++ b/web/.storybook/preview.js
@@ -0,0 +1,12 @@
+import 'antd/dist/antd.css';
+import '../styles/globals.scss';
+
+export const parameters = {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+};
diff --git a/web/package.json b/web/package.json
index 098251fe4..9d3957e8c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -6,18 +6,23 @@
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
- "lint": "eslint --ext .js,.ts,.tsx types/ pages/ components/"
+ "lint": "eslint --ext .js,.ts,.tsx types/ pages/ components/ stories/",
+ "storybook": "start-storybook -p 6006",
+ "build-storybook": "build-storybook"
},
"dependencies": {
"@ant-design/icons": "4.7.0",
+ "@storybook/react": "^6.4.22",
"antd": "4.18.9",
+ "autoprefixer": "^10.4.4",
"chart.js": "3.7.0",
"chartkick": "4.1.1",
"classnames": "2.3.1",
"date-fns": "2.28.0",
"lodash": "4.17.21",
"markdown-it": "12.3.2",
- "next": "12.1.0",
+ "next": "^12.1.5",
+ "postcss-flexbugs-fixes": "^5.0.2",
"prop-types": "15.8.1",
"rc-overflow": "1.2.4",
"rc-util": "5.17.0",
@@ -27,20 +32,29 @@
"react-linkify": "1.0.0-alpha",
"react-markdown": "8.0.0",
"react-markdown-editor-lite": "1.3.2",
- "sass": "1.49.7",
"ua-parser-js": "1.0.2"
},
"devDependencies": {
+ "@babel/core": "^7.17.9",
+ "@storybook/addon-actions": "^6.4.22",
+ "@storybook/addon-essentials": "^6.4.22",
+ "@storybook/addon-interactions": "^6.4.22",
+ "@storybook/addon-links": "^6.4.22",
+ "@storybook/addon-postcss": "^2.0.0",
+ "@storybook/preset-scss": "^1.0.3",
+ "@storybook/testing-library": "^0.0.9",
"@types/chart.js": "2.9.35",
"@types/classnames": "2.3.1",
"@types/markdown-it": "12.2.3",
"@types/node": "17.0.0",
"@types/prop-types": "15.7.4",
- "@types/react": "17.0.38",
+ "@types/react": "^18.0.5",
"@types/react-linkify": "1.0.1",
"@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
+ "babel-loader": "^8.2.4",
+ "css-loader": "^5.2.6",
"eslint": "8.8.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-next": "12.0.10",
@@ -50,7 +64,13 @@
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.28.0",
"eslint-plugin-react-hooks": "4.3.0",
+ "eslint-plugin-storybook": "^0.5.10",
+ "html-webpack-plugin": "^5.5.0",
"prettier": "2.5.1",
+ "sass": "^1.50.0",
+ "sass-loader": "^10.1.1",
+ "sb": "^6.4.22",
+ "style-loader": "^2.0.0",
"typescript": "4.5.5"
}
}
\ No newline at end of file
diff --git a/web/postcss.config.js b/web/postcss.config.js
new file mode 100644
index 000000000..99e05ab4a
--- /dev/null
+++ b/web/postcss.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ plugins: ['postcss-flexbugs-fixes', 'autoprefixer'],
+};
diff --git a/web/stories/Dropdown.stories.tsx b/web/stories/Dropdown.stories.tsx
new file mode 100644
index 000000000..46cf854e5
--- /dev/null
+++ b/web/stories/Dropdown.stories.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Menu, Dropdown } from 'antd';
+import { DownOutlined } from '@ant-design/icons';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+
+const menu = (
+
+);
+
+const DropdownExample = () => (
+
+
+
+);
+
+export default {
+ title: 'owncast/Dropdown',
+ component: Dropdown,
+ parameters: {},
+} as ComponentMeta;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const Template: ComponentStory = args => ;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const Basic = Template.bind({});
diff --git a/web/stories/Form.stories.tsx b/web/stories/Form.stories.tsx
new file mode 100644
index 000000000..a7c7c995e
--- /dev/null
+++ b/web/stories/Form.stories.tsx
@@ -0,0 +1,110 @@
+import React, { useState } from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import {
+ Button,
+ Form,
+ Input,
+ Radio,
+ Select,
+ Cascader,
+ DatePicker,
+ InputNumber,
+ TreeSelect,
+ Switch,
+} from 'antd';
+
+const FormExample = () => {
+ const [componentSize, setComponentSize] = useState('default');
+
+ const onFormLayoutChange = ({ size }) => {
+ setComponentSize(size);
+ };
+
+ return (
+
+
+ Small
+ Default
+ Large
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default {
+ title: 'owncast/Form',
+ component: Form,
+ // parameters: {},
+} as ComponentMeta;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const Template: ComponentStory = args => ;
+
+export const Demo = Template.bind({});
diff --git a/web/stories/Introduction.stories.mdx b/web/stories/Introduction.stories.mdx
new file mode 100644
index 000000000..42c4a8714
--- /dev/null
+++ b/web/stories/Introduction.stories.mdx
@@ -0,0 +1,211 @@
+import { Meta } from '@storybook/addon-docs';
+import Code from './assets/code-brackets.svg';
+import Colors from './assets/colors.svg';
+import Comments from './assets/comments.svg';
+import Direction from './assets/direction.svg';
+import Flow from './assets/flow.svg';
+import Plugin from './assets/plugin.svg';
+import Repo from './assets/repo.svg';
+import StackAlt from './assets/stackalt.svg';
+
+
+
+
+
+# Welcome to Storybook
+
+Storybook helps you build UI components in isolation from your app's business logic, data, and context.
+That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
+
+Browse example stories now by navigating to them in the sidebar.
+View their code in the `src/stories` directory to learn how they work.
+We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
+
+Configure
+
+
+
+Learn
+
+
+
+
+ TipEdit the Markdown in{' '}
+ src/stories/Introduction.stories.mdx
+
diff --git a/web/stories/Modal.stories.tsx b/web/stories/Modal.stories.tsx
new file mode 100644
index 000000000..4e0d24229
--- /dev/null
+++ b/web/stories/Modal.stories.tsx
@@ -0,0 +1,47 @@
+import React, { useState } from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Modal, Button } from 'antd';
+
+const Usage = () => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ const showModal = () => {
+ setIsModalVisible(true);
+ };
+
+ const handleOk = () => {
+ setIsModalVisible(false);
+ };
+
+ const handleCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ return (
+ <>
+
+
+ Some contents...
+ Some contents...
+ Some contents...
+
+ >
+ );
+};
+
+export default {
+ title: 'owncast/Modal',
+ component: Modal,
+ parameters: {},
+} as ComponentMeta;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const Template: ComponentStory = args => ;
+
+export const Basic = Template.bind({});
+
+Usage.propTypes = {};
+
+Usage.defaultProps = {};
diff --git a/web/stories/Tabs.stories.tsx b/web/stories/Tabs.stories.tsx
new file mode 100644
index 000000000..043faf023
--- /dev/null
+++ b/web/stories/Tabs.stories.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Tabs, Radio } from 'antd';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+
+const { TabPane } = Tabs;
+
+class TabsExample extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = { size: 'small' };
+ }
+
+ onChange = e => {
+ this.setState({ size: e.target.value });
+ };
+
+ render() {
+ const { size } = this.state;
+ const { type } = this.props;
+
+ return (
+
+
+ Small
+ Default
+ Large
+
+
+
+
+ Content of card tab 1
+
+
+ Content of card tab 2
+
+
+ Content of card tab 3
+
+
+
+ );
+ }
+}
+
+export default {
+ title: 'owncast/Tabs',
+ component: Tabs,
+} as ComponentMeta;
+
+const Template: ComponentStory = args => ;
+
+export const Card = Template.bind({});
+Card.args = { type: 'card' };
+
+export const Basic = Template.bind({});
+Basic.args = { type: '' };
+
+TabsExample.propTypes = {
+ type: PropTypes.string,
+};
+
+TabsExample.defaultProps = {
+ type: '',
+};
diff --git a/web/stories/assets/code-brackets.svg b/web/stories/assets/code-brackets.svg
new file mode 100644
index 000000000..73de94776
--- /dev/null
+++ b/web/stories/assets/code-brackets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/colors.svg b/web/stories/assets/colors.svg
new file mode 100644
index 000000000..17d58d516
--- /dev/null
+++ b/web/stories/assets/colors.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/comments.svg b/web/stories/assets/comments.svg
new file mode 100644
index 000000000..6493a139f
--- /dev/null
+++ b/web/stories/assets/comments.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/direction.svg b/web/stories/assets/direction.svg
new file mode 100644
index 000000000..65676ac27
--- /dev/null
+++ b/web/stories/assets/direction.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/flow.svg b/web/stories/assets/flow.svg
new file mode 100644
index 000000000..8ac27db40
--- /dev/null
+++ b/web/stories/assets/flow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/plugin.svg b/web/stories/assets/plugin.svg
new file mode 100644
index 000000000..29e5c690c
--- /dev/null
+++ b/web/stories/assets/plugin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/repo.svg b/web/stories/assets/repo.svg
new file mode 100644
index 000000000..f386ee902
--- /dev/null
+++ b/web/stories/assets/repo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/stories/assets/stackalt.svg b/web/stories/assets/stackalt.svg
new file mode 100644
index 000000000..9b7ad2743
--- /dev/null
+++ b/web/stories/assets/stackalt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file