diff --git a/.gitignore b/.gitignore index 5a7f5f5..03cc164 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -bazel-bin -bazel-out -bazel-roleypoly -bazel-testlogs -node_modules -.env +bazel-bin +bazel-out +bazel-roleypoly +bazel-testlogs +node_modules +.env docker-compose.yaml #Added by cargo @@ -16,3 +16,5 @@ docker-compose.yaml #already existing elements were commented out #/target + +*.log \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 0000000..f6a1141 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,16 @@ +const path = require("path"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); + +module.exports = { + stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: ["@storybook/addon-links", "@storybook/addon-essentials"], + webpackFinal: async (config, { configType }) => { + config.resolve.plugins = [ + new TsconfigPathsPlugin({ + configFile: path.resolve(__dirname, "../tsconfig.json"), + }), + ]; + + return config; + }, +}; diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 0000000..d2bdc4c --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,6 @@ +import { addons } from "@storybook/addons"; +import { roleypolyTheme } from "./theme"; + +addons.setConfig({ + theme: roleypolyTheme, +}); diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..cc5994e --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,40 @@ + + diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 0000000..5d00c02 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,4 @@ + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, +} \ No newline at end of file diff --git a/.storybook/theme.js b/.storybook/theme.js new file mode 100644 index 0000000..9a57758 --- /dev/null +++ b/.storybook/theme.js @@ -0,0 +1,34 @@ +import { create } from "@storybook/theming"; +import { palette } from "../src/design-system/atoms/colors"; + +export const roleypolyTheme = create({ + base: "dark", + + colorPrimary: palette.green400, + colorSecondary: palette.taupe200, + + // UI + appBg: palette.taupe300, + appContentBg: palette.taupe300, + appBorderColor: palette.taupe100, + appBorderRadius: 0, + + // Typography + fontBase: "system-ui, sans-serif", + fontCode: "monospace", + + // Text colors + textColor: palette.grey600, + textInverseColor: palette.grey100, + + // Toolbar default and active colors + barTextColor: palette.taupe500, + barSelectedColor: palette.taupe600, + barBg: palette.taupe100, + + // Form colors + inputBg: "rgba(0,0,0,0.24)", + inputBorder: palette.taupe100, + inputTextColor: palette.grey600, + inputBorderRadius: 0, +}); diff --git a/WORKSPACE b/WORKSPACE index 7c49991..990cff0 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -26,8 +26,8 @@ http_archive( http_archive( name = "build_bazel_rules_nodejs", - sha256 = "4952ef879704ab4ad6729a29007e7094aef213ea79e9f2e94cbe1c9a753e63ef", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.2.0/rules_nodejs-2.2.0.tar.gz"], + sha256 = "64a71a64ac58b8969bb19b1c9258a973b6433913e958964da698943fb5521d98", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.2.1/rules_nodejs-2.2.1.tar.gz"], ) git_repository( @@ -41,7 +41,9 @@ git_repository( load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install") -node_repositories(package_json = ["//:package.json"]) +node_repositories( + package_json = ["//:package.json"], +) yarn_install( name = "npm", diff --git a/hack/react.bzl b/hack/react.bzl new file mode 100644 index 0000000..67ddb81 --- /dev/null +++ b/hack/react.bzl @@ -0,0 +1,30 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") + +def _render_deps(deps = []): + output_deps = [] + + for dep in deps: + if dep.startswith("//"): + output_deps.append(dep) + else: + output_deps.append("@npm//" + dep) + + return output_deps + +def react_library(name, deps = [], **kwargs): + ts_library( + name = name, + srcs = native.glob( + [ + "*.ts", + "*.tsx", + ], + exclude = native.glob([ + "*.spec.ts*", + "*.story.tsx", + "*.stories.tsx", + ]), + ), + deps = _render_deps(deps), + **kwargs + ) diff --git a/package.json b/package.json index 0be6a57..29b8f67 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "1.0.0", "description": "https://roleypoly.com", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" }, "repository": { "type": "git", @@ -16,14 +18,33 @@ }, "homepage": "https://github.com/roleypoly/roleypoly#readme", "dependencies": { - "@improbable-eng/grpc-web": "0.13.0", - "google-protobuf": "3.13.0" + "chroma-js": "2.1.0", + "next": "^9.5.4", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-icons": "^3.11.0", + "styled-components": "^5.2.0" }, "devDependencies": { + "@babel/core": "^7.11.6", "@bazel/typescript": "^2.2.1", + "@storybook/addon-actions": "^6.0.26", + "@storybook/addon-essentials": "^6.0.26", + "@storybook/addon-links": "^6.0.26", + "@storybook/addons": "^6.0.26", + "@storybook/react": "^6.0.26", + "@storybook/theming": "^6.0.26", + "@types/chroma-js": "2.1.0", + "@types/google-protobuf": "3.7.3", + "@types/styled-components": "5.1.3", + "babel-loader": "^8.1.0", "prettier": "^2.1.2", - "typescript": "^4.0.3", - "@roleypoly/ts-protoc-gen": "^1.0.1-promises.1", - "@types/google-protobuf": "3.7.3" + "react-is": "^16.13.1", + "tsconfig-paths-webpack-plugin": "^3.3.0", + "tslint": "6.1.3", + "tslint-config-prettier": "^1.18.0", + "tslint-config-standard": "^9.0.0", + "tslint-plugin-prettier": "^2.3.0", + "typescript": "^4.0.3" } -} \ No newline at end of file +} diff --git a/src/common/utils/withContext/BUILD.bazel b/src/common/utils/withContext/BUILD.bazel new file mode 100644 index 0000000..cb16e51 --- /dev/null +++ b/src/common/utils/withContext/BUILD.bazel @@ -0,0 +1,11 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "withContext", + deps = [ + "react", + "@types/react", + ], +) diff --git a/src/common/utils/withContext/contextTestHelpers.tsx b/src/common/utils/withContext/contextTestHelpers.tsx new file mode 100644 index 0000000..79678f5 --- /dev/null +++ b/src/common/utils/withContext/contextTestHelpers.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export type ContextShimProps = { + context: React.Context; + children: (data: T) => any; +}; + +export function ContextShim(props: ContextShimProps) { + const context = React.useContext(props.context); + return <>{props.children(context)}; +} diff --git a/src/common/utils/withContext/index.ts b/src/common/utils/withContext/index.ts new file mode 100644 index 0000000..9884653 --- /dev/null +++ b/src/common/utils/withContext/index.ts @@ -0,0 +1,3 @@ +export * from './withContext'; +import * as testHelpers from './contextTestHelpers'; +export { testHelpers }; diff --git a/src/common/utils/withContext/withContext.tsx b/src/common/utils/withContext/withContext.tsx new file mode 100644 index 0000000..41ef631 --- /dev/null +++ b/src/common/utils/withContext/withContext.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; + +export const withContext = ( + Context: React.Context, + Component: React.ComponentType +): React.FunctionComponent => (props) => ( + + {(context) => } + +); diff --git a/src/design-system/atoms/avatar/Avatar.stories.tsx b/src/design-system/atoms/avatar/Avatar.stories.tsx new file mode 100644 index 0000000..8f57bf4 --- /dev/null +++ b/src/design-system/atoms/avatar/Avatar.stories.tsx @@ -0,0 +1,30 @@ +import { text } from "@storybook/addon-knobs"; +import * as React from "react"; +import { Avatar } from "./Avatar"; + +export default { + title: "Atoms/Avatar", + component: Avatar, + argTypes: { + initials: { control: "text" }, + }, + args: { + initials: "KR", + }, +}; + +export const WithInitials = ({ initials, ...rest }) => ( + + {initials} + +); + +export const WithText = ({ initials, ...rest }) => ( + + {initials} + +); +export const Empty = (args) => ; +export const DeliberatelyEmpty = (args) => ( + +); diff --git a/src/design-system/atoms/avatar/Avatar.styled.ts b/src/design-system/atoms/avatar/Avatar.styled.ts new file mode 100644 index 0000000..92505d8 --- /dev/null +++ b/src/design-system/atoms/avatar/Avatar.styled.ts @@ -0,0 +1,46 @@ +import { AvatarProps } from "./Avatar"; +import styled, { css } from "styled-components"; +import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports +import { palette } from "roleypoly/src/design-system/atoms/colors"; + +type ContainerProps = Pick & + Pick; +export const Container = styled.div` + border-radius: 100%; + box-sizing: border-box; + width: ${(props: ContainerProps) => props.size || 48}px; + height: ${(props: ContainerProps) => props.size || 48}px; + min-width: ${(props: ContainerProps) => props.size || 48}px; + min-height: ${(props: ContainerProps) => props.size || 48}px; + display: flex; + justify-content: center; + align-items: center; + color: ${palette.grey100}; + position: relative; + background-color: ${palette.grey500}; + font-weight: bold; + text-align: center; + line-height: 1; + overflow: hidden; + font-size: ${(props: ContainerProps) => props.size}; + ${(props) => + props.deliberatelyEmpty && + css` + border: 4px solid rgba(0, 0, 0, 0.25); + background-color: ${palette.taupe400}; + color: ${palette.taupe600}; + `} +`; + +type ImageProps = Pick; +export const Image = styled.div` + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + border-radius: 100%; +`; diff --git a/src/design-system/atoms/avatar/Avatar.tsx b/src/design-system/atoms/avatar/Avatar.tsx new file mode 100644 index 0000000..edc6a48 --- /dev/null +++ b/src/design-system/atoms/avatar/Avatar.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Container, Image } from './Avatar.styled'; + +export type AvatarProps = { + src?: string; + children?: string | React.ReactNode; + size?: number; + deliberatelyEmpty?: boolean; +}; + +/** Chuldren is recommended to not be larger than 2 uppercase letters. */ +export const Avatar = (props: AvatarProps) => ( + + {props.src && ( + + )} +
+ {props.children || ( + /* needs specifically   to prevent layout issues. */ + <>  + )} +
+
+); diff --git a/src/design-system/atoms/avatar/BUILD.bazel b/src/design-system/atoms/avatar/BUILD.bazel new file mode 100644 index 0000000..6570980 --- /dev/null +++ b/src/design-system/atoms/avatar/BUILD.bazel @@ -0,0 +1,14 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "avatar", + deps = [ + "react", + "styled-components", + "//src/design-system/atoms/colors", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/avatar/avatarUtils.tsx b/src/design-system/atoms/avatar/avatarUtils.tsx new file mode 100644 index 0000000..caddf3c --- /dev/null +++ b/src/design-system/atoms/avatar/avatarUtils.tsx @@ -0,0 +1,7 @@ +export const initialsFromName = (name: string) => + name + .split(' ') + .slice(0, 2) + .map((x) => x[0]) + .join('') + .toUpperCase(); diff --git a/src/design-system/atoms/avatar/index.ts b/src/design-system/atoms/avatar/index.ts new file mode 100644 index 0000000..6cf3c33 --- /dev/null +++ b/src/design-system/atoms/avatar/index.ts @@ -0,0 +1,4 @@ +export * from './Avatar'; + +import * as utils from './avatarUtils'; +export { utils }; diff --git a/src/design-system/atoms/branding/BUILD.bazel b/src/design-system/atoms/branding/BUILD.bazel new file mode 100644 index 0000000..1e47aa8 --- /dev/null +++ b/src/design-system/atoms/branding/BUILD.bazel @@ -0,0 +1,12 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "branding", + deps = [ + "react", + "//src/design-system/atoms/colors", + "@types/react", + ], +) diff --git a/src/design-system/atoms/branding/Branding.stories.tsx b/src/design-system/atoms/branding/Branding.stories.tsx new file mode 100644 index 0000000..7927216 --- /dev/null +++ b/src/design-system/atoms/branding/Branding.stories.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { + Logomark as BrandingLogomark, + Logotype as BrandingLogotype, +} from "./Branding"; +import styled from "styled-components"; + +export default { + title: "Atoms/Branding", +}; + +const Wrapper = styled.div` + background-color: black; + padding: 2em; +`; + +export const Logomark = () => ( + + + +); + +export const Logotype = () => ( + + + +); diff --git a/src/design-system/atoms/branding/Branding.tsx b/src/design-system/atoms/branding/Branding.tsx new file mode 100644 index 0000000..1d07687 --- /dev/null +++ b/src/design-system/atoms/branding/Branding.tsx @@ -0,0 +1,100 @@ +import * as React from "react"; +import { palette } from "roleypoly/src/design-system/atoms/colors"; + +type LogoProps = { + fill: string; + width: number; + height: number; + circleFill: string; + typeFill: string; + style: object; + className: string; +}; + +export const Logotype = ({ + fill = palette.taupe500, + width, + height, + circleFill = palette.taupe200, + typeFill, + style, + className, +}: Partial = {}) => ( + + + + + + + + + + + + +); + +export const Logomark = ({ + fill = palette.taupe500, + width, + height, + circleFill = palette.taupe200, + typeFill, + style, + className, +}: Partial) => ( + + + + + + + + + + + + +); diff --git a/src/design-system/atoms/branding/index.ts b/src/design-system/atoms/branding/index.ts new file mode 100644 index 0000000..e983d1e --- /dev/null +++ b/src/design-system/atoms/branding/index.ts @@ -0,0 +1 @@ +export * from './Branding'; diff --git a/src/design-system/atoms/breakpoints/BUILD.bazel b/src/design-system/atoms/breakpoints/BUILD.bazel new file mode 100644 index 0000000..44c1689 --- /dev/null +++ b/src/design-system/atoms/breakpoints/BUILD.bazel @@ -0,0 +1,14 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "breakpoints", + deps = [ + "react", + "styled-components", + "//src/common/utils/withContext", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/breakpoints/BreakpointProvider.tsx b/src/design-system/atoms/breakpoints/BreakpointProvider.tsx new file mode 100644 index 0000000..4679e62 --- /dev/null +++ b/src/design-system/atoms/breakpoints/BreakpointProvider.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { mediaQueryDefs } from './Breakpoints'; +import { ScreenSize, BreakpointContext } from './Context'; + +const resetScreen: ScreenSize = { + onSmallScreen: false, + onTablet: false, + onDesktop: false, +}; + +export class BreakpointsProvider extends React.Component<{}, ScreenSize> { + public state = { + ...resetScreen, + onSmallScreen: true, + }; + + private mediaQueries: { [key in keyof ScreenSize]: MediaQueryList } = { + onSmallScreen: window.matchMedia( + mediaQueryDefs.onSmallScreen.replace('@media screen and', '') + ), + onTablet: window.matchMedia( + mediaQueryDefs.onTablet.replace('@media screen and', '') + ), + onDesktop: window.matchMedia( + mediaQueryDefs.onDesktop.replace('@media screen and', '') + ), + }; + + componentDidMount() { + Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) => + mediaQuery.addEventListener('change', this.handleMediaEvent) + ); + } + + componentWillUnmount() { + Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) => + mediaQuery.removeEventListener('change', this.handleMediaEvent) + ); + } + + handleMediaEvent = (event: MediaQueryListEvent) => { + console.log('handleMediaEvent', { event }); + this.setState({ + ...resetScreen, + ...this.calculateScreen(), + }); + }; + + calculateScreen = () => { + if (this.mediaQueries.onDesktop.matches) { + return { onDesktop: true }; + } + + if (this.mediaQueries.onTablet.matches) { + return { onTablet: true }; + } + + return { onSmallScreen: true }; + }; + + render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/design-system/atoms/breakpoints/Breakpoints.stories.tsx b/src/design-system/atoms/breakpoints/Breakpoints.stories.tsx new file mode 100644 index 0000000..1421215 --- /dev/null +++ b/src/design-system/atoms/breakpoints/Breakpoints.stories.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { BreakpointDebugTool } from "./DebugTool"; +import { BreakpointsProvider } from "./BreakpointProvider"; + +export default { + title: "Atoms/Breakpoints", + decorators: [(story) => {story()}], + component: BreakpointDebugTool, +}; + +export const DebugTool = () => ; diff --git a/src/design-system/atoms/breakpoints/Breakpoints.ts b/src/design-system/atoms/breakpoints/Breakpoints.ts new file mode 100644 index 0000000..6f77c90 --- /dev/null +++ b/src/design-system/atoms/breakpoints/Breakpoints.ts @@ -0,0 +1,36 @@ +// import {} from 'styled-components'; + +export const breakpoints = { + onTablet: 768, + onDesktop: 1024, +}; + +export const mediaQueryDefs = { + onSmallScreen: `@media screen and (max-width: ${breakpoints.onTablet - 1}px)`, + onTablet: `@media screen and (min-width: ${breakpoints.onTablet}px)`, + onDesktop: `@media screen and (min-width: ${breakpoints.onDesktop}px)`, +}; + +export const onTablet = (...expressions: any) => { + return ` + ${mediaQueryDefs.onTablet} { + ${expressions.join()} + } + `; +}; + +export const onDesktop = (...expressions: any) => { + return ` + ${mediaQueryDefs.onDesktop} { + ${expressions.join()} + } + `; +}; + +export const onSmallScreen = (...expressions: any) => { + return ` + ${mediaQueryDefs.onSmallScreen} { + ${expressions.join()} + } + `; +}; diff --git a/src/design-system/atoms/breakpoints/Context.ts b/src/design-system/atoms/breakpoints/Context.ts new file mode 100644 index 0000000..3c6df5c --- /dev/null +++ b/src/design-system/atoms/breakpoints/Context.ts @@ -0,0 +1,27 @@ +import * as React from "react"; +import { withContext } from "roleypoly/src/common/utils/withContext"; + +export type ScreenSize = { + onSmallScreen: boolean; + onTablet: boolean; + onDesktop: boolean; +}; + +export type BreakpointProps = { + screenSize: ScreenSize; +}; + +const defaultScreenSize: BreakpointProps = { + screenSize: { + onSmallScreen: true, + onDesktop: false, + onTablet: false, + }, +}; + +export const BreakpointContext = React.createContext(defaultScreenSize); + +export const useBreakpointContext = () => React.useContext(BreakpointContext); + +export const withBreakpoints = (Component: React.ComponentType) => + withContext(BreakpointContext, Component as any); diff --git a/src/design-system/atoms/breakpoints/DebugTool.tsx b/src/design-system/atoms/breakpoints/DebugTool.tsx new file mode 100644 index 0000000..9e6cd84 --- /dev/null +++ b/src/design-system/atoms/breakpoints/DebugTool.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { onDesktop, onTablet } from './Breakpoints'; +import { useBreakpointContext } from './Context'; + +const DebuggerPosition = styled.div` + position: fixed; + top: 0; + left: 0; + font-family: monospace; + & > div { + display: flex; + } +`; + +const OnSmallScreen = styled.div` + display: block; +`; + +const OnTablet = styled.div` + display: none; + ${onTablet(`display: block;`)} +`; + +const OnDesktop = styled.div` + display: none; + ${onDesktop`display: block;`} +`; + +const CSSBreakpointDebugger = () => ( +
+ S + T + D +
+); + +const JSBreakpointDebugger = () => { + const { + screenSize: { onTablet, onDesktop, onSmallScreen }, + } = useBreakpointContext(); + + return ( +
+ {onSmallScreen &&
S
} + {onTablet &&
T
} + {onDesktop &&
D
} +
+ ); +}; +export const BreakpointDebugTool = () => ( + + + + +); diff --git a/src/design-system/atoms/breakpoints/index.ts b/src/design-system/atoms/breakpoints/index.ts new file mode 100644 index 0000000..4c89585 --- /dev/null +++ b/src/design-system/atoms/breakpoints/index.ts @@ -0,0 +1,3 @@ +export * from './Breakpoints'; +export * from './Context'; +export * from './BreakpointProvider'; diff --git a/src/design-system/atoms/button/BUILD.bazel b/src/design-system/atoms/button/BUILD.bazel new file mode 100644 index 0000000..68142a3 --- /dev/null +++ b/src/design-system/atoms/button/BUILD.bazel @@ -0,0 +1,16 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "button", + deps = [ + "react", + "styled-components", + "//src/design-system/atoms/colors", + "//src/design-system/atoms/fonts", + "//src/design-system/atoms/typography", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/button/Button.spec.tsx b/src/design-system/atoms/button/Button.spec.tsx new file mode 100644 index 0000000..1f9c192 --- /dev/null +++ b/src/design-system/atoms/button/Button.spec.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { Button } from './Button'; + +it('fires an onClick callback when clicked', () => { + const mock = jest.fn(); + const view = shallow(); + + view.simulate('click'); + expect(mock).toBeCalled(); +}); diff --git a/src/design-system/atoms/button/Button.story.tsx b/src/design-system/atoms/button/Button.story.tsx new file mode 100644 index 0000000..dc32430 --- /dev/null +++ b/src/design-system/atoms/button/Button.story.tsx @@ -0,0 +1,83 @@ +// TODO: port to new story + +import * as React from "react"; +import { atomStories } from "atoms/atoms.story"; +import { Button, ButtonProps } from "./Button"; +import { text as textKnob } from "@storybook/addon-knobs"; +import { FaDiscord } from "react-icons/fa"; +import { styled } from "@storybook/theming"; + +const largeStory = atomStories("Button/Large", module); +const smallStory = atomStories("Button/Small", module); + +const colorModes: NonNullable[] = [ + "primary", + "secondary", + "discord", + "muted", +]; + +const Margin = styled.div` + margin-top: 5px; + display: flex; + align-items: center; +`; + +const storyTemplate = (props: Omit) => () => { + const text = textKnob("Button content", "Example Button"); + + return ( +
+ {colorModes.map((color, i) => ( + + +
+ {color[0].toUpperCase()} + {color.slice(1)} +
+
+ ))} +
+ ); +}; +const storyBuilder = ( + story: typeof largeStory, + size: NonNullable +) => { + story.add( + "Normal", + storyTemplate({ + size, + }) + ); + + story.add( + "Icon", + storyTemplate({ + size, + icon: ( +
+ +
+ ), + }) + ); + + story.add( + "Loading", + storyTemplate({ + size, + icon: ( +
+ +
+ ), + loading: true, + }) + ); +}; + +storyBuilder(largeStory, "large"); +storyBuilder(smallStory, "small"); diff --git a/src/design-system/atoms/button/Button.styled.ts b/src/design-system/atoms/button/Button.styled.ts new file mode 100644 index 0000000..eddd8b7 --- /dev/null +++ b/src/design-system/atoms/button/Button.styled.ts @@ -0,0 +1,106 @@ +import styled, { css } from "styled-components"; +import { text400, text300 } from "roleypoly/src/design-system/atoms/typography"; +import { fontCSS } from "roleypoly/src/design-system/atoms/fonts"; +import { palette } from "roleypoly/src/design-system/atoms/colors"; +import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports + +export const IconContainer = styled.div` + margin-right: 0.6rem; + font-size: 1.75em; +`; + +const base = css` + ${fontCSS} + appearance: none; + display: block; + background-color: ${palette.taupe300}; + color: ${palette.grey500}; + border-radius: 3px; + border: 2px solid rgba(0, 0, 0, 0.55); + transition: all 0.15s ease-in-out; + outline: 0; + position: relative; + user-select: none; + cursor: pointer; + white-space: nowrap; + + ::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: #000; + opacity: 0; + transition: all 0.15s ease-in-out; + } + + :hover { + transform: translateY(-1px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); + } + + :active { + transform: translateY(1px); + box-shadow: 0 0 2px rgba(0, 0, 0, 0.25); + ::after { + opacity: 0.1; + } + } +`; + +const colors = { + primary: css` + background-color: ${palette.green400}; + color: ${palette.taupe100}; + `, + secondary: css``, + discord: css` + background-color: ${palette.discord400}; + border: 2px solid ${palette.discord200}; + `, + muted: css` + border: 2px solid rgba(0, 0, 0, 0.15); + background: none; + :hover { + background-color: ${palette.taupe200}; + } + `, +}; + +const sizes = { + small: css` + ${text300} + padding: 4px 8px; + `, + large: css` + ${text400} + padding: 12px 32px; + width: 100%; + `, +}; + +const modifiers = { + withIcon: css` + display: flex; + align-items: center; + justify-content: center; + `, + withLoading: css` + pointer-events: none; + `, +}; + +export type ButtonComposerOptions = { + size: keyof typeof sizes; + color: keyof typeof colors; + modifiers?: Array; +}; + +export const Button = styled.button` + ${base} + ${(props) => props.size in sizes && sizes[props.size]} + ${(props) => props.color in colors && colors[props.color]} + ${(props) => props.modifiers?.map((m) => modifiers[m])} +`; diff --git a/src/design-system/atoms/button/Button.tsx b/src/design-system/atoms/button/Button.tsx new file mode 100644 index 0000000..69ef82b --- /dev/null +++ b/src/design-system/atoms/button/Button.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { + Button as StyledButton, + IconContainer, + ButtonComposerOptions, +} from './Button.styled'; + +export type ButtonProps = Partial & { + children: React.ReactNode; + icon?: React.ReactNode; + loading?: boolean; + onClick?: () => void; +}; + +export const Button = (props: ButtonProps) => { + const modifiers: ButtonProps['modifiers'] = []; + if (props.loading) { + modifiers.push('withLoading'); + } + + if (props.icon) { + modifiers.push('withIcon'); + } + + return ( + + {props.icon && {props.icon}} +
{props.children}
+
+ ); +}; diff --git a/src/design-system/atoms/button/index.ts b/src/design-system/atoms/button/index.ts new file mode 100644 index 0000000..8b166a8 --- /dev/null +++ b/src/design-system/atoms/button/index.ts @@ -0,0 +1 @@ +export * from './Button'; diff --git a/src/design-system/atoms/colors/BUILD.bazel b/src/design-system/atoms/colors/BUILD.bazel new file mode 100644 index 0000000..b5f9e0d --- /dev/null +++ b/src/design-system/atoms/colors/BUILD.bazel @@ -0,0 +1,15 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "colors", + deps = [ + "chroma-js", + "react", + "styled-components", + "@types/chroma-js", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/colors/colors.stories.tsx b/src/design-system/atoms/colors/colors.stories.tsx new file mode 100644 index 0000000..151f328 --- /dev/null +++ b/src/design-system/atoms/colors/colors.stories.tsx @@ -0,0 +1,164 @@ +import * as React from "react"; +import { palette } from "./colors"; +import styled from "styled-components"; +import chroma from "chroma-js"; +import { AmbientSmall } from "roleypoly/src/design-system/atoms/typography"; + +type RatioList = { + color1: string[]; + color2: string[]; + ratio: string; +}; + +export default { + title: "Atoms/Colors", +}; + +const Swatch = styled.div` + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); + width: 250px; + height: 100px; + margin: 10px; + display: inline-block; + background-color: #fff; + border: 1px solid #fff; +`; + +const SwatchColor = styled.div` + height: 72px; +`; + +const Label = styled.div` + font-size: 12px; + display: flex; + justify-content: space-between; + padding: 6px; + color: ${palette.taupe100}; + p { + margin: 0; + } +`; + +export const Colors = () => { + return ( +
+ {Object.entries(palette).map(([name, color], i) => ( + + + + + ))} +
+ ); +}; + +export const ContrastRatios = () => { + const allRatios = getAllRatios(palette); + + return ( +
+

+ WCAG Contrast Calculations. +
+ Marked in Green is 7.0+ or AAA. + Acceptable for Text. +
+ Marked in Orange is 4.5+ or AA. + Acceptable for UI. +
+ All below 4.5 is unacceptable. +
+ + WCAG Contrast testing disabled for this page. + +

+ + + + Swatch + Ratio + Color 1 + Color 2 + + + + {allRatios.map((ratio, i) => ( + +   +   + {ratio.ratio} + {ratio.color1[0]} + {ratio.color2[0]} + + oh my god my + + + shin how dare you + + + ))} + + +
+ ); +}; + +const ContrastTable = styled.table` + td, + th { + padding: 6px 10px; + } +`; + +const getWCAGStyle = (ratio: number): React.CSSProperties => { + if (ratio >= 7) { + return { color: "green", fontWeight: "bold" }; + } + + if (ratio >= 4.5) { + return { color: "orange", fontWeight: "bold" }; + } + + return {}; +}; + +const getAllRatios = (input: typeof palette) => + Object.entries(input) + .filter(([name]) => !name.startsWith("discord")) + .reduce((acc, [name, color]) => { + return [ + ...acc, + ...Object.entries(palette) + .filter(([name]) => !name.startsWith("discord")) + .map(([matchName, matchColor]) => ({ + color1: [name, color], + color2: [matchName, matchColor], + ratio: chroma.contrast(color, matchColor).toFixed(2), + })), + ]; + }, [] as RatioList[]) + .filter(({ ratio }) => +ratio !== 1) + .sort((a, b) => { + if (+a.ratio > +b.ratio) { + return -1; + } + return 1; + }) + .filter((_, i) => i % 2 === 0); diff --git a/src/design-system/atoms/colors/colors.tsx b/src/design-system/atoms/colors/colors.tsx new file mode 100644 index 0000000..7027c29 --- /dev/null +++ b/src/design-system/atoms/colors/colors.tsx @@ -0,0 +1,45 @@ +import { css, createGlobalStyle } from "styled-components"; +import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports +import chroma from "chroma-js"; + +export const palette = { + taupe100: "#332D2D", + taupe200: "#453E3D", + taupe300: "#5D5352", + taupe400: "#756867", + taupe500: "#AB9B9A", + taupe600: "#EBD6D4", + + discord100: "#23272A", + discord200: "#2C2F33", + discord400: "#7289DA", + discord500: "#99AAB5", + + green400: "#46B646", + + red400: "#E95353", + + gold400: "#EFCF24", + + grey100: "#1C1010", + grey500: "#DBD9D9", + grey600: "#F2EFEF", +}; + +const getPaletteCSS = () => + Object.entries(palette).reduce( + (acc, [key, color]) => ({ ...acc, [`--${key}`]: color }), + {} + ); + +export const colorVars = css(getPaletteCSS()); + +export const GlobalStyleColors = createGlobalStyle` + :root { + ${colorVars} + } +`; + +export const numberToChroma = (colorInt: number) => { + return chroma(colorInt); +}; diff --git a/src/design-system/atoms/colors/index.ts b/src/design-system/atoms/colors/index.ts new file mode 100644 index 0000000..bc9bae4 --- /dev/null +++ b/src/design-system/atoms/colors/index.ts @@ -0,0 +1,2 @@ +export * from "./colors"; +export * as utils from "./withColors"; diff --git a/src/design-system/atoms/colors/withColors.tsx b/src/design-system/atoms/colors/withColors.tsx new file mode 100644 index 0000000..fc6aac3 --- /dev/null +++ b/src/design-system/atoms/colors/withColors.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { colorVars } from './colors'; + +const ColorsContainer = styled.div` + ${colorVars} +`; + +export const withColors = (storyFn: () => React.ReactNode) => ( + {storyFn()} +); diff --git a/src/design-system/atoms/dot-overlay/BUILD.bazel b/src/design-system/atoms/dot-overlay/BUILD.bazel new file mode 100644 index 0000000..d4fc2d8 --- /dev/null +++ b/src/design-system/atoms/dot-overlay/BUILD.bazel @@ -0,0 +1,13 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "dot-overlay", + deps = [ + "react", + "styled-components", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/dot-overlay/DotOverlay.stories.tsx b/src/design-system/atoms/dot-overlay/DotOverlay.stories.tsx new file mode 100644 index 0000000..ea10223 --- /dev/null +++ b/src/design-system/atoms/dot-overlay/DotOverlay.stories.tsx @@ -0,0 +1,9 @@ +import * as React from "react"; +import { DotOverlay } from "./DotOverlay"; + +export default { + title: "Atoms/Dot Overlay", +}; + +export const Dark = () => ; +export const Light = () => ; diff --git a/src/design-system/atoms/dot-overlay/DotOverlay.tsx b/src/design-system/atoms/dot-overlay/DotOverlay.tsx new file mode 100644 index 0000000..0e2b914 --- /dev/null +++ b/src/design-system/atoms/dot-overlay/DotOverlay.tsx @@ -0,0 +1,39 @@ +import styled from "styled-components"; +import * as React from "react"; +import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports + +const dotOverlayBase = styled.div` + opacity: 0.6; + pointer-events: none; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: -10; + background-size: 27px 27px; +`; + +const DotOverlayDark = styled(dotOverlayBase)` + background-image: radial-gradient( + circle, + #332d2d, + #332d2d 1px, + transparent 1px, + transparent + ); +`; + +const DotOverlayLight = styled(dotOverlayBase)` + background-image: radial-gradient( + circle, + #dbd9d9, + #dbd9d9 1px, + transparent 1px, + transparent + ); +`; + +export const DotOverlay = ({ light }: { light?: boolean }) => { + return light ? : ; +}; diff --git a/src/design-system/atoms/dot-overlay/index.ts b/src/design-system/atoms/dot-overlay/index.ts new file mode 100644 index 0000000..f73d022 --- /dev/null +++ b/src/design-system/atoms/dot-overlay/index.ts @@ -0,0 +1 @@ +export * from './DotOverlay'; diff --git a/src/design-system/atoms/fader/BUILD.bazel b/src/design-system/atoms/fader/BUILD.bazel new file mode 100644 index 0000000..d057778 --- /dev/null +++ b/src/design-system/atoms/fader/BUILD.bazel @@ -0,0 +1,13 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "fader", + deps = [ + "react", + "styled-components", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/fader/Fader.stories.tsx b/src/design-system/atoms/fader/Fader.stories.tsx new file mode 100644 index 0000000..a753f68 --- /dev/null +++ b/src/design-system/atoms/fader/Fader.stories.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { FaderOpacity, FaderSlide } from "./Fader"; +import { Button } from "roleypoly/src/design-system/atoms/button"; +import { action } from "@storybook/addon-actions"; + +export default { + title: "Atoms/Fader", + component: FaderSlide, + args: { + isVisible: true, + }, +}; + +export const Opacity = (args) => { + return ( + + + + ); +}; + +export const Slide = (args) => { + return ( + + + + ); +}; diff --git a/src/design-system/atoms/fader/Fader.tsx b/src/design-system/atoms/fader/Fader.tsx new file mode 100644 index 0000000..ea8f868 --- /dev/null +++ b/src/design-system/atoms/fader/Fader.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import styled from "styled-components"; +import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports + +export type FaderProps = { + isVisible: boolean; + children: React.ReactNode; +}; + +const FaderOpacityStyled = styled.div>` + opacity: ${(props) => (props.isVisible ? 1 : 0)}; + pointer-events: ${(props) => (props.isVisible ? "unset" : "none")}; + transition: opacity 0.35s ease-in-out; +`; + +export const FaderOpacity = (props: FaderProps) => { + return ( + + {props.children} + + ); +}; + +const FaderSlideStyled = styled.div>` + max-height: ${(props) => (props.isVisible ? "4em" : "0")}; + pointer-events: ${(props) => (props.isVisible ? "unset" : "none")}; + transition: max-height 0.35s ease-in-out; + overflow: hidden; + transform: translateZ(0); +`; + +export const FaderSlide = (props: FaderProps) => { + return ( + + {props.children} + + ); +}; diff --git a/src/design-system/atoms/fader/index.ts b/src/design-system/atoms/fader/index.ts new file mode 100644 index 0000000..40a7e24 --- /dev/null +++ b/src/design-system/atoms/fader/index.ts @@ -0,0 +1 @@ +export * from './Fader'; diff --git a/src/design-system/atoms/fonts/BUILD.bazel b/src/design-system/atoms/fonts/BUILD.bazel new file mode 100644 index 0000000..513a8a1 --- /dev/null +++ b/src/design-system/atoms/fonts/BUILD.bazel @@ -0,0 +1,14 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "fonts", + deps = [ + "next", + "react", + "styled-components", + "@types/react", + "@types/styled-components", + ], +) diff --git a/src/design-system/atoms/fonts/fonts.stories.tsx b/src/design-system/atoms/fonts/fonts.stories.tsx new file mode 100644 index 0000000..7f6ecb8 --- /dev/null +++ b/src/design-system/atoms/fonts/fonts.stories.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import { UseFontStyled } from "./fonts"; +import styled from "styled-components"; +import { + MediumTitle, + Text as TextBlock, +} from "roleypoly/src/design-system/atoms/typography"; + +const resetFont = (storyFn: () => React.ReactNode) => ( + {storyFn()} +); + +export default { + title: "Atoms/Fonts", + decorators: [resetFont], +}; + +const FontReset = styled.div` + font-family: sans-serif; +`; + +const CorrectlyFontedH2 = (props: { children: React.ReactNode }) => ( + + {props.children} + +); + +const Text = () => ( + <> +

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Et facilis alias + placeat cumque sapiente ad delectus omnis quae. Reiciendis quibusdam + deserunt repellat. Exercitationem modi incidunt autem nemo tempore eaque + soluta. +

+

+ 帯カノ需混モイ一録43旧百12共ドレ能生ホクユ禁度ヨ材図クほはそ護関ラト郵張エノヨ議件クめざ県読れみとぶ論税クょンど慎転リつぎみ松期ほへド. + 縦投記ふで覧速っだせあ過先課フ演無ぎぱべ習併相ーす気6元ゆる領気希ぎ投代ラ我関レ森郎由系堂ず. + 読ケリ夜指ーっトせ認平引ウシ間花ヱクム年6台ぐ山婦ラスエ子著コア掲中ロ像属戸メソユ職諏ルど詐児題たに書希ク幕値長ラそめド. +

+

+ 🔸🐕🔺💱🎊👽🐛 👨📼🕦📞 👱👆🍗👚🌈 🔝🔟🍉🔰🍲🏁🕗 🎡🐉🍲📻🔢🔄 + 💟💲🍻💜💩🔼 🎱🌸📛👫🌻 🗽🕜🐥👕🍈. 🐒🍚🔓📱🏦 🎦🌑🔛💙👣🔚 🔆🗻🌿🎳📲🍯 + 🌞💟🎌🍌 🔪📯🐎💮 👌👭🎋🏉🏰 📓🕃🎂💉🔩 🐟🌇👺🌊🌒 📪👅🍂🍁 🌖🐮🔽🌒📊. + 🔤🍍🌸📷🎴 💏🍌📎👥👉👒 👝💜🔶🍣 💨🗼👈💉💉💰 🍐🕖🌰👝🕓🏊🐕 🏀📅📼📒 + 🐕🌈👋 +

+ +); + +export const Fonts = () => ( + +
+ Unstyled Default + +
+
+ + Main (Source Han Sans Japanese, Source Sans) + + + + +
+
+); diff --git a/src/design-system/atoms/fonts/fonts.tsx b/src/design-system/atoms/fonts/fonts.tsx new file mode 100644 index 0000000..942174c --- /dev/null +++ b/src/design-system/atoms/fonts/fonts.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import Head from "next/head"; +import styled, { css } from "styled-components"; +import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports + +export const InjectTypekitFont = () => { + React.useEffect(() => { + (window as any).Typekit.load(); + }, []); + return ( + + +