mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
feat(design-system): port most of ui atoms to bazel monorepo and new storybook
This commit is contained in:
parent
a5e2fdc7a7
commit
72ea639c5d
108 changed files with 13650 additions and 53 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -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
|
16
.storybook/main.js
Normal file
16
.storybook/main.js
Normal file
|
@ -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;
|
||||
},
|
||||
};
|
6
.storybook/manager.js
Normal file
6
.storybook/manager.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { addons } from "@storybook/addons";
|
||||
import { roleypolyTheme } from "./theme";
|
||||
|
||||
addons.setConfig({
|
||||
theme: roleypolyTheme,
|
||||
});
|
40
.storybook/preview-head.html
Normal file
40
.storybook/preview-head.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
(function (d) {
|
||||
var config = {
|
||||
kitId: "bck0pci",
|
||||
scriptTimeout: 3000,
|
||||
async: true,
|
||||
},
|
||||
h = d.documentElement,
|
||||
t = setTimeout(function () {
|
||||
h.className =
|
||||
h.className.replace(/\bwf-loading\b/g, "") + " wf-inactive";
|
||||
}, config.scriptTimeout),
|
||||
tk = d.createElement("script"),
|
||||
f = false,
|
||||
s = d.getElementsByTagName("script")[0],
|
||||
a;
|
||||
h.className += " wf-loading";
|
||||
tk.src = "https://use.typekit.net/" + config.kitId + ".js";
|
||||
tk.async = true;
|
||||
tk.onload = tk.onreadystatechange = function () {
|
||||
a = this.readyState;
|
||||
if (f || (a && a != "complete" && a != "loaded")) return;
|
||||
f = true;
|
||||
clearTimeout(t);
|
||||
try {
|
||||
Typekit.load(config);
|
||||
} catch (e) {}
|
||||
};
|
||||
s.parentNode.insertBefore(tk, s);
|
||||
})(document);
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: "source-han-sans-japanese", "Source Sans Pro", sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||
color: #f2efef;
|
||||
background-color: #453e3d;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
4
.storybook/preview.js
Normal file
4
.storybook/preview.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
}
|
34
.storybook/theme.js
Normal file
34
.storybook/theme.js
Normal file
|
@ -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,
|
||||
});
|
|
@ -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",
|
||||
|
|
30
hack/react.bzl
Normal file
30
hack/react.bzl
Normal file
|
@ -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
|
||||
)
|
35
package.json
35
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11
src/common/utils/withContext/BUILD.bazel
Normal file
11
src/common/utils/withContext/BUILD.bazel
Normal file
|
@ -0,0 +1,11 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "withContext",
|
||||
deps = [
|
||||
"react",
|
||||
"@types/react",
|
||||
],
|
||||
)
|
11
src/common/utils/withContext/contextTestHelpers.tsx
Normal file
11
src/common/utils/withContext/contextTestHelpers.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export type ContextShimProps<T> = {
|
||||
context: React.Context<T>;
|
||||
children: (data: T) => any;
|
||||
};
|
||||
|
||||
export function ContextShim<T>(props: ContextShimProps<T>) {
|
||||
const context = React.useContext(props.context);
|
||||
return <>{props.children(context)}</>;
|
||||
}
|
3
src/common/utils/withContext/index.ts
Normal file
3
src/common/utils/withContext/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './withContext';
|
||||
import * as testHelpers from './contextTestHelpers';
|
||||
export { testHelpers };
|
10
src/common/utils/withContext/withContext.tsx
Normal file
10
src/common/utils/withContext/withContext.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const withContext = <T, K extends T>(
|
||||
Context: React.Context<T>,
|
||||
Component: React.ComponentType<K>
|
||||
): React.FunctionComponent<K> => (props) => (
|
||||
<Context.Consumer>
|
||||
{(context) => <Component {...props} {...context} />}
|
||||
</Context.Consumer>
|
||||
);
|
30
src/design-system/atoms/avatar/Avatar.stories.tsx
Normal file
30
src/design-system/atoms/avatar/Avatar.stories.tsx
Normal file
|
@ -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 }) => (
|
||||
<Avatar src="https://i.imgur.com/epMSRQH.png" size={48} {...rest}>
|
||||
{initials}
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
export const WithText = ({ initials, ...rest }) => (
|
||||
<Avatar size={48} {...rest}>
|
||||
{initials}
|
||||
</Avatar>
|
||||
);
|
||||
export const Empty = (args) => <Avatar size={48} {...args}></Avatar>;
|
||||
export const DeliberatelyEmpty = (args) => (
|
||||
<Avatar size={48} deliberatelyEmpty={true} {...args}></Avatar>
|
||||
);
|
46
src/design-system/atoms/avatar/Avatar.styled.ts
Normal file
46
src/design-system/atoms/avatar/Avatar.styled.ts
Normal file
|
@ -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<AvatarProps, "size"> &
|
||||
Pick<AvatarProps, "deliberatelyEmpty">;
|
||||
export const Container = styled.div<ContainerProps>`
|
||||
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<AvatarProps, "src">;
|
||||
export const Image = styled.div<ImageProps>`
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
`;
|
28
src/design-system/atoms/avatar/Avatar.tsx
Normal file
28
src/design-system/atoms/avatar/Avatar.tsx
Normal file
|
@ -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) => (
|
||||
<Container size={props.size} deliberatelyEmpty={props.deliberatelyEmpty}>
|
||||
{props.src && (
|
||||
<Image
|
||||
style={{
|
||||
backgroundImage: `url(${props.src})`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{props.children || (
|
||||
/* needs specifically to prevent layout issues. */
|
||||
<> </>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
14
src/design-system/atoms/avatar/BUILD.bazel
Normal file
14
src/design-system/atoms/avatar/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
7
src/design-system/atoms/avatar/avatarUtils.tsx
Normal file
7
src/design-system/atoms/avatar/avatarUtils.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const initialsFromName = (name: string) =>
|
||||
name
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((x) => x[0])
|
||||
.join('')
|
||||
.toUpperCase();
|
4
src/design-system/atoms/avatar/index.ts
Normal file
4
src/design-system/atoms/avatar/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './Avatar';
|
||||
|
||||
import * as utils from './avatarUtils';
|
||||
export { utils };
|
12
src/design-system/atoms/branding/BUILD.bazel
Normal file
12
src/design-system/atoms/branding/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
27
src/design-system/atoms/branding/Branding.stories.tsx
Normal file
27
src/design-system/atoms/branding/Branding.stories.tsx
Normal file
|
@ -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 = () => (
|
||||
<Wrapper>
|
||||
<BrandingLogomark />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export const Logotype = () => (
|
||||
<Wrapper>
|
||||
<BrandingLogotype />
|
||||
</Wrapper>
|
||||
);
|
100
src/design-system/atoms/branding/Branding.tsx
Normal file
100
src/design-system/atoms/branding/Branding.tsx
Normal file
File diff suppressed because one or more lines are too long
1
src/design-system/atoms/branding/index.ts
Normal file
1
src/design-system/atoms/branding/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Branding';
|
14
src/design-system/atoms/breakpoints/BUILD.bazel
Normal file
14
src/design-system/atoms/breakpoints/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
68
src/design-system/atoms/breakpoints/BreakpointProvider.tsx
Normal file
68
src/design-system/atoms/breakpoints/BreakpointProvider.tsx
Normal file
|
@ -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 (
|
||||
<BreakpointContext.Provider value={{ screenSize: { ...this.state } }}>
|
||||
{this.props.children}
|
||||
</BreakpointContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
11
src/design-system/atoms/breakpoints/Breakpoints.stories.tsx
Normal file
11
src/design-system/atoms/breakpoints/Breakpoints.stories.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from "react";
|
||||
import { BreakpointDebugTool } from "./DebugTool";
|
||||
import { BreakpointsProvider } from "./BreakpointProvider";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Breakpoints",
|
||||
decorators: [(story) => <BreakpointsProvider>{story()}</BreakpointsProvider>],
|
||||
component: BreakpointDebugTool,
|
||||
};
|
||||
|
||||
export const DebugTool = () => <BreakpointDebugTool />;
|
36
src/design-system/atoms/breakpoints/Breakpoints.ts
Normal file
36
src/design-system/atoms/breakpoints/Breakpoints.ts
Normal file
|
@ -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()}
|
||||
}
|
||||
`;
|
||||
};
|
27
src/design-system/atoms/breakpoints/Context.ts
Normal file
27
src/design-system/atoms/breakpoints/Context.ts
Normal file
|
@ -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 = <T>(Component: React.ComponentType<T>) =>
|
||||
withContext(BreakpointContext, Component as any);
|
56
src/design-system/atoms/breakpoints/DebugTool.tsx
Normal file
56
src/design-system/atoms/breakpoints/DebugTool.tsx
Normal file
|
@ -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 = () => (
|
||||
<div>
|
||||
<OnSmallScreen style={{ backgroundColor: 'red' }}>S</OnSmallScreen>
|
||||
<OnTablet style={{ backgroundColor: 'green' }}>T</OnTablet>
|
||||
<OnDesktop style={{ backgroundColor: 'blue' }}>D</OnDesktop>
|
||||
</div>
|
||||
);
|
||||
|
||||
const JSBreakpointDebugger = () => {
|
||||
const {
|
||||
screenSize: { onTablet, onDesktop, onSmallScreen },
|
||||
} = useBreakpointContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{onSmallScreen && <div style={{ backgroundColor: 'red' }}>S</div>}
|
||||
{onTablet && <div style={{ backgroundColor: 'green' }}>T</div>}
|
||||
{onDesktop && <div style={{ backgroundColor: 'blue' }}>D</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const BreakpointDebugTool = () => (
|
||||
<DebuggerPosition>
|
||||
<JSBreakpointDebugger />
|
||||
<CSSBreakpointDebugger />
|
||||
</DebuggerPosition>
|
||||
);
|
3
src/design-system/atoms/breakpoints/index.ts
Normal file
3
src/design-system/atoms/breakpoints/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './Breakpoints';
|
||||
export * from './Context';
|
||||
export * from './BreakpointProvider';
|
16
src/design-system/atoms/button/BUILD.bazel
Normal file
16
src/design-system/atoms/button/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
11
src/design-system/atoms/button/Button.spec.tsx
Normal file
11
src/design-system/atoms/button/Button.spec.tsx
Normal file
|
@ -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(<Button onClick={mock}>Button</Button>);
|
||||
|
||||
view.simulate('click');
|
||||
expect(mock).toBeCalled();
|
||||
});
|
83
src/design-system/atoms/button/Button.story.tsx
Normal file
83
src/design-system/atoms/button/Button.story.tsx
Normal file
|
@ -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<ButtonProps["color"]>[] = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"discord",
|
||||
"muted",
|
||||
];
|
||||
|
||||
const Margin = styled.div`
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const storyTemplate = (props: Omit<ButtonProps, "children">) => () => {
|
||||
const text = textKnob("Button content", "Example Button");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{colorModes.map((color, i) => (
|
||||
<Margin key={i}>
|
||||
<Button {...props} color={color}>
|
||||
{text}
|
||||
</Button>
|
||||
<div style={{ marginLeft: "1em" }}>
|
||||
{color[0].toUpperCase()}
|
||||
{color.slice(1)}
|
||||
</div>
|
||||
</Margin>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const storyBuilder = (
|
||||
story: typeof largeStory,
|
||||
size: NonNullable<ButtonProps["size"]>
|
||||
) => {
|
||||
story.add(
|
||||
"Normal",
|
||||
storyTemplate({
|
||||
size,
|
||||
})
|
||||
);
|
||||
|
||||
story.add(
|
||||
"Icon",
|
||||
storyTemplate({
|
||||
size,
|
||||
icon: (
|
||||
<div style={{ position: "relative", top: 3 }}>
|
||||
<FaDiscord />
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
story.add(
|
||||
"Loading",
|
||||
storyTemplate({
|
||||
size,
|
||||
icon: (
|
||||
<div style={{ position: "relative", top: 3 }}>
|
||||
<FaDiscord />
|
||||
</div>
|
||||
),
|
||||
loading: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
storyBuilder(largeStory, "large");
|
||||
storyBuilder(smallStory, "small");
|
106
src/design-system/atoms/button/Button.styled.ts
Normal file
106
src/design-system/atoms/button/Button.styled.ts
Normal file
|
@ -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<keyof typeof modifiers>;
|
||||
};
|
||||
|
||||
export const Button = styled.button<ButtonComposerOptions>`
|
||||
${base}
|
||||
${(props) => props.size in sizes && sizes[props.size]}
|
||||
${(props) => props.color in colors && colors[props.color]}
|
||||
${(props) => props.modifiers?.map((m) => modifiers[m])}
|
||||
`;
|
36
src/design-system/atoms/button/Button.tsx
Normal file
36
src/design-system/atoms/button/Button.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Button as StyledButton,
|
||||
IconContainer,
|
||||
ButtonComposerOptions,
|
||||
} from './Button.styled';
|
||||
|
||||
export type ButtonProps = Partial<ButtonComposerOptions> & {
|
||||
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 (
|
||||
<StyledButton
|
||||
size={props.size || 'large'}
|
||||
color={props.color || 'primary'}
|
||||
modifiers={modifiers}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.icon && <IconContainer>{props.icon}</IconContainer>}
|
||||
<div>{props.children}</div>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
1
src/design-system/atoms/button/index.ts
Normal file
1
src/design-system/atoms/button/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Button';
|
15
src/design-system/atoms/colors/BUILD.bazel
Normal file
15
src/design-system/atoms/colors/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
164
src/design-system/atoms/colors/colors.stories.tsx
Normal file
164
src/design-system/atoms/colors/colors.stories.tsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
{Object.entries(palette).map(([name, color], i) => (
|
||||
<Swatch key={i}>
|
||||
<SwatchColor style={{ backgroundColor: color }} />
|
||||
<Label>
|
||||
<p>{name}</p>
|
||||
<p>
|
||||
<code>var(--{name})</code>
|
||||
</p>
|
||||
</Label>
|
||||
</Swatch>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ContrastRatios = () => {
|
||||
const allRatios = getAllRatios(palette);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>WCAG Contrast Calculations.</b>
|
||||
<br />
|
||||
Marked in <span style={getWCAGStyle(7.1)}>Green</span> is 7.0+ or AAA.
|
||||
Acceptable for Text.
|
||||
<br />
|
||||
Marked in <span style={getWCAGStyle(4.6)}>Orange</span> is 4.5+ or AA.
|
||||
Acceptable for UI.
|
||||
<br />
|
||||
All below 4.5 is unacceptable.
|
||||
<br />
|
||||
<AmbientSmall>
|
||||
WCAG Contrast testing disabled for this page.
|
||||
</AmbientSmall>
|
||||
</p>
|
||||
<ContrastTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}>Swatch</th>
|
||||
<th>Ratio</th>
|
||||
<th>Color 1</th>
|
||||
<th>Color 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allRatios.map((ratio, i) => (
|
||||
<tr key={i}>
|
||||
<td style={{ backgroundColor: ratio.color1[1] }}> </td>
|
||||
<td style={{ backgroundColor: ratio.color2[1] }}> </td>
|
||||
<td style={getWCAGStyle(+ratio.ratio)}>{ratio.ratio}</td>
|
||||
<td>{ratio.color1[0]}</td>
|
||||
<td>{ratio.color2[0]}</td>
|
||||
<td
|
||||
style={{
|
||||
color: ratio.color1[1],
|
||||
backgroundColor: ratio.color2[1],
|
||||
paddingRight: "0.1em",
|
||||
}}
|
||||
>
|
||||
oh my god my
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
color: ratio.color2[1],
|
||||
backgroundColor: ratio.color1[1],
|
||||
paddingLeft: "0.1em",
|
||||
}}
|
||||
>
|
||||
shin how dare you
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</ContrastTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
45
src/design-system/atoms/colors/colors.tsx
Normal file
45
src/design-system/atoms/colors/colors.tsx
Normal file
|
@ -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);
|
||||
};
|
2
src/design-system/atoms/colors/index.ts
Normal file
2
src/design-system/atoms/colors/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./colors";
|
||||
export * as utils from "./withColors";
|
11
src/design-system/atoms/colors/withColors.tsx
Normal file
11
src/design-system/atoms/colors/withColors.tsx
Normal file
|
@ -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) => (
|
||||
<ColorsContainer>{storyFn()}</ColorsContainer>
|
||||
);
|
13
src/design-system/atoms/dot-overlay/BUILD.bazel
Normal file
13
src/design-system/atoms/dot-overlay/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { DotOverlay } from "./DotOverlay";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Dot Overlay",
|
||||
};
|
||||
|
||||
export const Dark = () => <DotOverlay />;
|
||||
export const Light = () => <DotOverlay light />;
|
39
src/design-system/atoms/dot-overlay/DotOverlay.tsx
Normal file
39
src/design-system/atoms/dot-overlay/DotOverlay.tsx
Normal file
|
@ -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 ? <DotOverlayLight /> : <DotOverlayDark />;
|
||||
};
|
1
src/design-system/atoms/dot-overlay/index.ts
Normal file
1
src/design-system/atoms/dot-overlay/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './DotOverlay';
|
13
src/design-system/atoms/fader/BUILD.bazel
Normal file
13
src/design-system/atoms/fader/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
28
src/design-system/atoms/fader/Fader.stories.tsx
Normal file
28
src/design-system/atoms/fader/Fader.stories.tsx
Normal file
|
@ -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 (
|
||||
<FaderOpacity {...args}>
|
||||
<Button onClick={action("onClick")}>Click me!</Button>
|
||||
</FaderOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export const Slide = (args) => {
|
||||
return (
|
||||
<FaderSlide {...args}>
|
||||
<Button onClick={action("onClick")}>Click me!</Button>
|
||||
</FaderSlide>
|
||||
);
|
||||
};
|
38
src/design-system/atoms/fader/Fader.tsx
Normal file
38
src/design-system/atoms/fader/Fader.tsx
Normal file
|
@ -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<Pick<FaderProps, "isVisible">>`
|
||||
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 (
|
||||
<FaderOpacityStyled isVisible={props.isVisible}>
|
||||
{props.children}
|
||||
</FaderOpacityStyled>
|
||||
);
|
||||
};
|
||||
|
||||
const FaderSlideStyled = styled.div<Pick<FaderProps, "isVisible">>`
|
||||
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 (
|
||||
<FaderSlideStyled isVisible={props.isVisible}>
|
||||
{props.children}
|
||||
</FaderSlideStyled>
|
||||
);
|
||||
};
|
1
src/design-system/atoms/fader/index.ts
Normal file
1
src/design-system/atoms/fader/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Fader';
|
14
src/design-system/atoms/fonts/BUILD.bazel
Normal file
14
src/design-system/atoms/fonts/BUILD.bazel
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
66
src/design-system/atoms/fonts/fonts.stories.tsx
Normal file
66
src/design-system/atoms/fonts/fonts.stories.tsx
Normal file
|
@ -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) => (
|
||||
<FontReset>{storyFn()}</FontReset>
|
||||
);
|
||||
|
||||
export default {
|
||||
title: "Atoms/Fonts",
|
||||
decorators: [resetFont],
|
||||
};
|
||||
|
||||
const FontReset = styled.div`
|
||||
font-family: sans-serif;
|
||||
`;
|
||||
|
||||
const CorrectlyFontedH2 = (props: { children: React.ReactNode }) => (
|
||||
<UseFontStyled>
|
||||
<MediumTitle>{props.children}</MediumTitle>
|
||||
</UseFontStyled>
|
||||
);
|
||||
|
||||
const Text = () => (
|
||||
<>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
帯カノ需混モイ一録43旧百12共ドレ能生ホクユ禁度ヨ材図クほはそ護関ラト郵張エノヨ議件クめざ県読れみとぶ論税クょンど慎転リつぎみ松期ほへド.
|
||||
縦投記ふで覧速っだせあ過先課フ演無ぎぱべ習併相ーす気6元ゆる領気希ぎ投代ラ我関レ森郎由系堂ず.
|
||||
読ケリ夜指ーっトせ認平引ウシ間花ヱクム年6台ぐ山婦ラスエ子著コア掲中ロ像属戸メソユ職諏ルど詐児題たに書希ク幕値長ラそめド.
|
||||
</p>
|
||||
<p>
|
||||
🔸🐕🔺💱🎊👽🐛 👨📼🕦📞 👱👆🍗👚🌈 🔝🔟🍉🔰🍲🏁🕗 🎡🐉🍲📻🔢🔄
|
||||
💟💲🍻💜💩🔼 🎱🌸📛👫🌻 🗽🕜🐥👕🍈. 🐒🍚🔓📱🏦 🎦🌑🔛💙👣🔚 🔆🗻🌿🎳📲🍯
|
||||
🌞💟🎌🍌 🔪📯🐎💮 👌👭🎋🏉🏰 📓🕃🎂💉🔩 🐟🌇👺🌊🌒 📪👅🍂🍁 🌖🐮🔽🌒📊.
|
||||
🔤🍍🌸📷🎴 💏🍌📎👥👉👒 👝💜🔶🍣 💨🗼👈💉💉💰 🍐🕖🌰👝🕓🏊🐕 🏀📅📼📒
|
||||
🐕🌈👋
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export const Fonts = () => (
|
||||
<TextBlock>
|
||||
<section>
|
||||
<CorrectlyFontedH2>Unstyled Default</CorrectlyFontedH2>
|
||||
<Text />
|
||||
</section>
|
||||
<section>
|
||||
<CorrectlyFontedH2>
|
||||
Main (Source Han Sans Japanese, Source Sans)
|
||||
</CorrectlyFontedH2>
|
||||
<UseFontStyled>
|
||||
<Text />
|
||||
</UseFontStyled>
|
||||
</section>
|
||||
</TextBlock>
|
||||
);
|
30
src/design-system/atoms/fonts/fonts.tsx
Normal file
30
src/design-system/atoms/fonts/fonts.tsx
Normal file
|
@ -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 (
|
||||
<Head>
|
||||
<link
|
||||
key="typekit-css-preload"
|
||||
rel="preload"
|
||||
href="https://use.typekit.net/bck0pci.js"
|
||||
as="script"
|
||||
/>
|
||||
<script key="typekit-js" src="https://use.typekit.net/bck0pci.js" />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export const fontCSS = css`
|
||||
font-family: "source-han-sans-japanese", "Source Sans Pro", sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||
`;
|
||||
|
||||
export const UseFontStyled = styled.div`
|
||||
${fontCSS}
|
||||
`;
|
1
src/design-system/atoms/fonts/index.ts
Normal file
1
src/design-system/atoms/fonts/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './fonts';
|
14
src/design-system/atoms/halfsies/BUILD.bazel
Normal file
14
src/design-system/atoms/halfsies/BUILD.bazel
Normal file
|
@ -0,0 +1,14 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "halfsies",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/breakpoints",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
13
src/design-system/atoms/halfsies/Halfsies.stories.tsx
Normal file
13
src/design-system/atoms/halfsies/Halfsies.stories.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as React from "react";
|
||||
import { HalfsiesContainer, HalfsiesItem } from "./Halfsies";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Halfsies",
|
||||
};
|
||||
|
||||
export const Container = () => (
|
||||
<HalfsiesContainer>
|
||||
<HalfsiesItem>Lefty doo</HalfsiesItem>
|
||||
<HalfsiesItem>Righty doo</HalfsiesItem>
|
||||
</HalfsiesContainer>
|
||||
);
|
18
src/design-system/atoms/halfsies/Halfsies.tsx
Normal file
18
src/design-system/atoms/halfsies/Halfsies.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import styled, { css } from "styled-components";
|
||||
import { onTablet } from "roleypoly/src/design-system/atoms/breakpoints";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
export const HalfsiesContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const HalfsiesItem = styled.div`
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 100%;
|
||||
${onTablet(css`
|
||||
flex: 1 2 50%;
|
||||
`)}
|
||||
`;
|
1
src/design-system/atoms/halfsies/index.ts
Normal file
1
src/design-system/atoms/halfsies/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Halfsies';
|
13
src/design-system/atoms/hero/BUILD.bazel
Normal file
13
src/design-system/atoms/hero/BUILD.bazel
Normal file
|
@ -0,0 +1,13 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "hero",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
74
src/design-system/atoms/hero/Hero.stories.tsx
Normal file
74
src/design-system/atoms/hero/Hero.stories.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import * as React from "react";
|
||||
import { Hero as HeroComponent } from "./Hero";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Hero",
|
||||
component: HeroComponent,
|
||||
args: {
|
||||
topSpacing: 75,
|
||||
bottomSpacing: 25,
|
||||
},
|
||||
};
|
||||
|
||||
export const Hero = ({ topSpacing, bottomSpacing }) => {
|
||||
return (
|
||||
<StoryWrapper topSpacing={topSpacing} bottomSpacing={bottomSpacing}>
|
||||
<HeroComponent topSpacing={topSpacing} bottomSpacing={bottomSpacing}>
|
||||
<h1>This is it.</h1>
|
||||
</HeroComponent>
|
||||
</StoryWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
type WrapperProps = {
|
||||
children: React.ReactNode;
|
||||
topSpacing: number;
|
||||
bottomSpacing: number;
|
||||
};
|
||||
|
||||
const StoryWrapper = ({
|
||||
topSpacing,
|
||||
bottomSpacing,
|
||||
...props
|
||||
}: WrapperProps) => (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "100vh",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: topSpacing,
|
||||
backgroundColor: "rgba(255,0,0,0.25)",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
topSpacing
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: bottomSpacing,
|
||||
backgroundColor: "rgba(0,0,255,0.25)",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
bottomSpacing
|
||||
</div>
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
33
src/design-system/atoms/hero/Hero.tsx
Normal file
33
src/design-system/atoms/hero/Hero.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
type HeroContainerProps = {
|
||||
topSpacing: number;
|
||||
bottomSpacing: number;
|
||||
};
|
||||
|
||||
type HeroProps = Partial<HeroContainerProps> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const HeroContainer = styled.div<HeroContainerProps>`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
min-height: calc(
|
||||
100vh - ${(props) => props.topSpacing + props.bottomSpacing}px
|
||||
);
|
||||
margin-top: ${(props) => props.topSpacing}px;
|
||||
`;
|
||||
|
||||
export const Hero = (props: HeroProps) => (
|
||||
<HeroContainer
|
||||
topSpacing={props.topSpacing || 0}
|
||||
bottomSpacing={props.bottomSpacing || 0}
|
||||
>
|
||||
{props.children}
|
||||
</HeroContainer>
|
||||
);
|
1
src/design-system/atoms/hero/index.ts
Normal file
1
src/design-system/atoms/hero/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Hero';
|
11
src/design-system/atoms/key-events/BUILD.bazel
Normal file
11
src/design-system/atoms/key-events/BUILD.bazel
Normal file
|
@ -0,0 +1,11 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "key-events",
|
||||
deps = [
|
||||
"react",
|
||||
"@types/react",
|
||||
],
|
||||
)
|
19
src/design-system/atoms/key-events/KeyEvents.ts
Normal file
19
src/design-system/atoms/key-events/KeyEvents.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export const globalOnKeyUp = (
|
||||
key: string[],
|
||||
action: () => any,
|
||||
isActive: boolean = true
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if (isActive && key.includes(event.key)) {
|
||||
action();
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('keyup', onKeyUp);
|
||||
|
||||
return () => document.body.removeEventListener('keyup', onKeyUp);
|
||||
}, [key, action, isActive]);
|
||||
};
|
1
src/design-system/atoms/key-events/index.ts
Normal file
1
src/design-system/atoms/key-events/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './KeyEvents';
|
18
src/design-system/atoms/popover/BUILD.bazel
Normal file
18
src/design-system/atoms/popover/BUILD.bazel
Normal file
|
@ -0,0 +1,18 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "popover",
|
||||
deps = [
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/breakpoints",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/key-events",
|
||||
"//src/design-system/atoms/timings",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
29
src/design-system/atoms/popover/Popover.story.tsx
Normal file
29
src/design-system/atoms/popover/Popover.story.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import * as React from 'react';
|
||||
import { atomStories } from 'atoms/atoms.story';
|
||||
import { Button } from 'atoms/button';
|
||||
import { Popover } from './Popover';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
const story = atomStories('Popover', module);
|
||||
|
||||
story.add('Popover', () => {
|
||||
const canDefocus = boolean('Can Defocus?', true);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 50 }}>
|
||||
<Button size="small" onClick={() => setIsOpen(!isOpen)}>
|
||||
{!isOpen ? 'Open' : 'Close'} me!
|
||||
</Button>
|
||||
<Popover
|
||||
position="top right"
|
||||
active={isOpen}
|
||||
onExit={() => setIsOpen(false)}
|
||||
canDefocus={canDefocus}
|
||||
headContent={<>Hello c:</>}
|
||||
>
|
||||
stuff
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
});
|
91
src/design-system/atoms/popover/Popover.styled.ts
Normal file
91
src/design-system/atoms/popover/Popover.styled.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import {
|
||||
onSmallScreen,
|
||||
onTablet,
|
||||
} from "roleypoly/src/design-system/atoms/breakpoints";
|
||||
import { palette } from "roleypoly/src/design-system/atoms/colors";
|
||||
import { transitions } from "roleypoly/src/design-system/atoms/timings";
|
||||
import styled, { css } from "styled-components";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
type PopoverStyledProps = {
|
||||
active: boolean;
|
||||
preferredWidth?: number;
|
||||
};
|
||||
|
||||
export const PopoverBase = styled.div<PopoverStyledProps>`
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
background-color: ${palette.taupe100};
|
||||
padding: 5px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 3px;
|
||||
z-index: 10;
|
||||
transition: opacity ${transitions.out2in}s ease-in,
|
||||
transform ${transitions.out2in}s ease-in;
|
||||
min-width: ${(props) => props.preferredWidth || 320}px;
|
||||
max-width: 100vw;
|
||||
${(props) =>
|
||||
!props.active &&
|
||||
css`
|
||||
transform: translateY(-2vh);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`}
|
||||
${onSmallScreen(css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
min-width: unset;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`)};
|
||||
`;
|
||||
|
||||
export const DefocusHandler = styled.div<PopoverStyledProps>`
|
||||
background-color: rgba(0, 0, 0, 0.01);
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
${(props) =>
|
||||
!props.active &&
|
||||
css`
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const PopoverHead = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const PopoverHeadCloser = styled.div`
|
||||
flex: 0;
|
||||
font-size: 2em;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
border-radius: 2em;
|
||||
min-width: 1.4em;
|
||||
height: 1.4em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
${onTablet(
|
||||
css`
|
||||
display: none;
|
||||
`
|
||||
)}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
export const PopoverContent = styled.div`
|
||||
padding: 5px;
|
||||
`;
|
42
src/design-system/atoms/popover/Popover.tsx
Normal file
42
src/design-system/atoms/popover/Popover.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
PopoverBase,
|
||||
DefocusHandler,
|
||||
PopoverHead,
|
||||
PopoverHeadCloser,
|
||||
PopoverContent,
|
||||
} from "./Popover.styled";
|
||||
import { globalOnKeyUp } from "roleypoly/src/design-system/atoms/key-events";
|
||||
import { IoMdClose } from "react-icons/io";
|
||||
|
||||
type PopoverProps = {
|
||||
children: React.ReactNode;
|
||||
position: "top left" | "top right" | "bottom left" | "bottom right";
|
||||
active: boolean;
|
||||
canDefocus?: boolean;
|
||||
onExit?: (type: "escape" | "defocus" | "explicit") => void;
|
||||
headContent: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Popover = (props: PopoverProps) => {
|
||||
globalOnKeyUp(["Escape"], () => props.onExit?.("escape"), props.active);
|
||||
return (
|
||||
<>
|
||||
<PopoverBase active={props.active}>
|
||||
<PopoverHead>
|
||||
<PopoverHeadCloser onClick={() => props.onExit?.("explicit")}>
|
||||
<IoMdClose />
|
||||
</PopoverHeadCloser>
|
||||
<div>{props.headContent}</div>
|
||||
</PopoverHead>
|
||||
<PopoverContent>{props.children}</PopoverContent>
|
||||
</PopoverBase>
|
||||
{props.canDefocus && (
|
||||
<DefocusHandler
|
||||
active={props.active}
|
||||
onClick={() => props.onExit?.("defocus")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
1
src/design-system/atoms/popover/index.ts
Normal file
1
src/design-system/atoms/popover/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Popover';
|
18
src/design-system/atoms/role/BUILD.bazel
Normal file
18
src/design-system/atoms/role/BUILD.bazel
Normal file
|
@ -0,0 +1,18 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "role",
|
||||
deps = [
|
||||
"chroma-js",
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"@types/chroma-js",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
13
src/design-system/atoms/role/Role.spec.tsx
Normal file
13
src/design-system/atoms/role/Role.spec.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import { roleCategory } from 'hack/fixtures/storyData';
|
||||
import * as React from 'react';
|
||||
import { Role } from './Role';
|
||||
|
||||
it('fires an OnClick handler when clicked', () => {
|
||||
const onClickMock = jest.fn();
|
||||
const view = shallow(
|
||||
<Role role={roleCategory[0]} selected={true} onClick={onClickMock} />
|
||||
);
|
||||
view.simulate('click');
|
||||
expect(onClickMock).toBeCalledWith(false);
|
||||
});
|
70
src/design-system/atoms/role/Role.story.tsx
Normal file
70
src/design-system/atoms/role/Role.story.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import * as React from 'react';
|
||||
import { atomStories } from 'atoms/atoms.story';
|
||||
import { Role } from './Role';
|
||||
import { roleCategory } from 'hack/fixtures/storyData';
|
||||
import { withColors } from 'atoms/colors/withColors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const story = atomStories('Role', module);
|
||||
|
||||
story.addDecorator(withColors);
|
||||
|
||||
const Demo = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const RoleWithState = (props: any) => {
|
||||
const [selected, updateSelected] = React.useState(false);
|
||||
return (
|
||||
<div style={{ padding: 5 }}>
|
||||
<Role
|
||||
{...props}
|
||||
selected={selected}
|
||||
onClick={(next) => updateSelected(next)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
story.add('Role', () => {
|
||||
return (
|
||||
<Demo>
|
||||
{roleCategory.map((c, idx) => (
|
||||
<RoleWithState key={idx} role={c} />
|
||||
))}
|
||||
</Demo>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Selected', () => (
|
||||
<Demo>
|
||||
{roleCategory.map((c, idx) => (
|
||||
<Role key={idx} role={c} selected={true} />
|
||||
))}
|
||||
</Demo>
|
||||
));
|
||||
|
||||
story.add('Unselected', () => (
|
||||
<Demo>
|
||||
{roleCategory.map((c, idx) => (
|
||||
<Role key={idx} role={c} selected={false} />
|
||||
))}
|
||||
</Demo>
|
||||
));
|
||||
|
||||
story.add('Disabled (Position)', () => (
|
||||
<Demo>
|
||||
{roleCategory.map((c, idx) => (
|
||||
<Role key={idx} role={{ ...c, safety: 1 }} selected={false} disabled />
|
||||
))}
|
||||
</Demo>
|
||||
));
|
||||
|
||||
story.add('Disabled (Dangerous)', () => (
|
||||
<Demo>
|
||||
{roleCategory.map((c, idx) => (
|
||||
<Role key={idx} role={{ ...c, safety: 2 }} selected={false} disabled />
|
||||
))}
|
||||
</Demo>
|
||||
));
|
84
src/design-system/atoms/role/Role.styled.tsx
Normal file
84
src/design-system/atoms/role/Role.styled.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import styled, { css } from "styled-components";
|
||||
import { transitions } from "roleypoly/src/design-system/atoms/timings";
|
||||
import { palette } from "roleypoly/src/design-system/atoms/colors";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
export type StyledProps = {
|
||||
selected: boolean;
|
||||
defaultColor: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const Outer = styled.div<StyledProps>`
|
||||
border-radius: 24px;
|
||||
background-color: ${(props) =>
|
||||
props.selected && !props.defaultColor
|
||||
? "var(--role-color)"
|
||||
: palette.taupe100};
|
||||
color: ${(props) =>
|
||||
props.selected ? "var(--role-contrast)" : palette.grey600};
|
||||
transition: color ${transitions.in2in}s ease-in-out,
|
||||
background-color ${transitions.in2in}s ease-in-out,
|
||||
transform ${transitions.actionable}s ease-in-out,
|
||||
box-shadow ${transitions.actionable}s ease-in-out;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
${(props) =>
|
||||
!props.disabled
|
||||
? css`
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 0 transparent;
|
||||
}
|
||||
`
|
||||
: null};
|
||||
`;
|
||||
|
||||
export const Circle = styled.div<StyledProps>`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 25px;
|
||||
background-color: ${(props) =>
|
||||
props.defaultColor && !props.selected
|
||||
? "transparent"
|
||||
: "var(--role-color)"};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.defaultColor
|
||||
? "var(--role-color)"
|
||||
: props.selected
|
||||
? "var(--role-accent)"
|
||||
: "transparent"};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: border ${transitions.in2in}s ease-in-out,
|
||||
background-color ${transitions.in2in}s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
fill-opacity: ${(props) => (props.selected || props.disabled ? 1 : 0)};
|
||||
transition: fill-opacity ${transitions.in2in}s ease-in-out;
|
||||
fill: ${(props) =>
|
||||
props.disabled && props.defaultColor
|
||||
? "var(--role-color)"
|
||||
: "var(--role-contrast)"};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Text = styled.div`
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
79
src/design-system/atoms/role/Role.tsx
Normal file
79
src/design-system/atoms/role/Role.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import * as React from "react";
|
||||
import * as styled from "./Role.styled";
|
||||
import { FaCheck, FaTimes } from "react-icons/fa";
|
||||
import { numberToChroma } from "roleypoly/src/design-system/atoms/colors";
|
||||
import chroma from "chroma-js";
|
||||
|
||||
type Props = {
|
||||
role: any; // TODO: rpc types
|
||||
selected: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (newState: boolean) => void;
|
||||
tooltipId?: string;
|
||||
};
|
||||
|
||||
export const Role = (props: Props) => {
|
||||
const colorVars = {
|
||||
"--role-color": "white",
|
||||
"--role-contrast": "hsl(0,0,0%)",
|
||||
"--role-accent": "hsl(0,0,70%)",
|
||||
};
|
||||
|
||||
if (props.role.color !== 0) {
|
||||
const baseColor = numberToChroma(props.role.color);
|
||||
const contrastColorUp = baseColor.brighten(5);
|
||||
const contrastColorDown = baseColor.darken(5);
|
||||
const ratio = chroma.contrast(contrastColorDown, baseColor);
|
||||
const contrastColor = ratio > 2 ? contrastColorDown : contrastColorUp;
|
||||
const accentColor = ratio > 2 ? baseColor.darken(2) : baseColor.brighten(2);
|
||||
colorVars["--role-color"] = baseColor.css();
|
||||
colorVars["--role-accent"] = accentColor.css();
|
||||
colorVars["--role-contrast"] = contrastColor.css();
|
||||
}
|
||||
|
||||
const styledProps: styled.StyledProps = {
|
||||
selected: props.selected,
|
||||
defaultColor: props.role.color === 0,
|
||||
disabled: !!props.disabled,
|
||||
};
|
||||
|
||||
const extra = !props.disabled
|
||||
? {}
|
||||
: {
|
||||
"data-tip": disabledReason(props.role),
|
||||
"data-for": props.tooltipId,
|
||||
};
|
||||
|
||||
return (
|
||||
<styled.Outer
|
||||
{...styledProps}
|
||||
style={colorVars as any}
|
||||
onClick={() => !props.disabled && props.onClick?.(!props.selected)}
|
||||
{...extra}
|
||||
>
|
||||
<styled.Circle {...styledProps}>
|
||||
{!props.disabled ? <FaCheck /> : <FaTimes />}
|
||||
</styled.Circle>
|
||||
<styled.Text>{props.role.name}</styled.Text>
|
||||
</styled.Outer>
|
||||
);
|
||||
};
|
||||
|
||||
const disabledReason = (role: any) => {
|
||||
switch (role.safety) {
|
||||
case 1:
|
||||
return `This role is above Roleypoly's own role.`;
|
||||
case 2:
|
||||
const { permissions } = role;
|
||||
let permissionHits: string[] = [];
|
||||
|
||||
(permissions & 0x00000008) === 0x00000008 &&
|
||||
permissionHits.push("Administrator");
|
||||
(permissions & 0x10000000) === 0x10000000 &&
|
||||
permissionHits.push("Manage Roles");
|
||||
|
||||
return `This role has unsafe permissions: ${permissionHits.join(", ")}`;
|
||||
default:
|
||||
return `This role is disabled.`;
|
||||
}
|
||||
};
|
1
src/design-system/atoms/role/index.ts
Normal file
1
src/design-system/atoms/role/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Role';
|
11
src/design-system/atoms/space/BUILD.bazel
Normal file
11
src/design-system/atoms/space/BUILD.bazel
Normal file
|
@ -0,0 +1,11 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "space",
|
||||
deps = [
|
||||
"styled-components",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
6
src/design-system/atoms/space/Space.tsx
Normal file
6
src/design-system/atoms/space/Space.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import styled from "styled-components";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
export const Space = styled.div`
|
||||
height: 15px;
|
||||
`;
|
1
src/design-system/atoms/space/index.ts
Normal file
1
src/design-system/atoms/space/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Space';
|
14
src/design-system/atoms/sparkle/BUILD.bazel
Normal file
14
src/design-system/atoms/sparkle/BUILD.bazel
Normal file
|
@ -0,0 +1,14 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "sparkle",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/colors",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
196
src/design-system/atoms/sparkle/Shapes.tsx
Normal file
196
src/design-system/atoms/sparkle/Shapes.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { CSSProperties } from 'styled-components';
|
||||
import * as React from 'react';
|
||||
|
||||
type SparkleProps = {
|
||||
height: string;
|
||||
strokeColor: string;
|
||||
repeatCount?: number;
|
||||
style?: CSSProperties;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
const Animation = (props: SparkleProps) => (
|
||||
<>
|
||||
<animateTransform
|
||||
fill="freeze"
|
||||
attributeName="transform"
|
||||
type="scale"
|
||||
begin={`${props.delay || 0}s`}
|
||||
additive="sum"
|
||||
dur="1s"
|
||||
repeatCount={props.repeatCount || 'indefinite'}
|
||||
from="-1 -1"
|
||||
to="1 1"
|
||||
/>
|
||||
<animateTransform
|
||||
fill="freeze"
|
||||
attributeName="transform"
|
||||
type="translate"
|
||||
begin={`${props.delay || 0}s`}
|
||||
additive="sum"
|
||||
dur="1s"
|
||||
repeatCount={props.repeatCount || 'indefinite'}
|
||||
from="-1 -1"
|
||||
to="1 1"
|
||||
/>
|
||||
|
||||
<animateTransform
|
||||
// additive="sum"
|
||||
fill="freeze"
|
||||
attributeName="transform"
|
||||
type="scale"
|
||||
to="0 0"
|
||||
dur="0.3s"
|
||||
begin={props.repeatCount || 10}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const SparkleCircle = (props: SparkleProps) => (
|
||||
<svg height={props.height} style={props.style} viewBox="-16 -16 16 16" fill="none">
|
||||
<g transform="translate(-8 -8)">
|
||||
<circle cx="0" cy="0" r="6.5" stroke={props.strokeColor} stroke-width="2">
|
||||
<Animation {...props} />
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SparkleStar = (props: SparkleProps) => (
|
||||
<svg height={props.height} style={props.style} viewBox="-30 -30 30 30" fill="none">
|
||||
<g transform="translate(0 0)">
|
||||
<path
|
||||
d="M15.5 3.23607L18.0289 11.0193L18.2534 11.7102H18.98H27.1637L20.5429 16.5205L19.9551 16.9476L20.1796 17.6385L22.7086 25.4217L16.0878 20.6115L15.5 20.1844L14.9122 20.6115L8.29144 25.4217L10.8204 17.6385L11.0449 16.9476L10.4571 16.5205L3.83631 11.7102H12.02H12.7466L12.9711 11.0193L15.5 3.23607Z"
|
||||
stroke={props.strokeColor}
|
||||
stroke-width="2"
|
||||
>
|
||||
<Animation {...props} />
|
||||
<animateTransform
|
||||
fill="freeze"
|
||||
attributeName="transform"
|
||||
type="translate"
|
||||
begin={`${props.delay || 0}s`}
|
||||
additive="sum"
|
||||
dur="0.5s"
|
||||
repeatCount={props.repeatCount || 'indefinite'}
|
||||
from="30 30"
|
||||
to="0 0"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SparkleCross = (props: SparkleProps) => (
|
||||
<svg height={props.height} style={props.style} viewBox="-15 -15 15 15" fill="none">
|
||||
<g transform="translate(-15 -15)">
|
||||
<path
|
||||
d="M-3.27836e-07 7.5L5 7.5M7.5 4.99999L7.5 2.53319e-06M9.99999 7.49999L15 7.49999M7.5 15L7.5 10"
|
||||
stroke={props.strokeColor}
|
||||
stroke-width="2"
|
||||
>
|
||||
<Animation {...props} />
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const patternBase: CSSProperties = {
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
const shapeMixin: CSSProperties = {
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
export const SparklePatternAlpha = ({ style, ...props }: SparkleProps) => (
|
||||
<div style={patternBase}>
|
||||
<SparkleCircle
|
||||
{...props}
|
||||
height="20%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
delay={0.24}
|
||||
/>
|
||||
<SparkleCross
|
||||
{...props}
|
||||
height="20%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: '45%',
|
||||
left: '15%',
|
||||
}}
|
||||
delay={0.55}
|
||||
/>
|
||||
<SparkleCross
|
||||
{...props}
|
||||
height="10%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: '30%',
|
||||
left: '-15%',
|
||||
transform: 'rotate(30deg)',
|
||||
}}
|
||||
delay={0.33}
|
||||
/>
|
||||
<SparkleStar
|
||||
{...props}
|
||||
height="30%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
bottom: '0%',
|
||||
right: '10%',
|
||||
}}
|
||||
delay={0.75}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SparklePatternBeta = ({ style, ...props }: SparkleProps) => (
|
||||
<div style={patternBase}>
|
||||
<SparkleCircle
|
||||
{...props}
|
||||
height="15%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: '60%',
|
||||
left: '20%',
|
||||
}}
|
||||
delay={0.9}
|
||||
/>
|
||||
<SparkleCross
|
||||
{...props}
|
||||
height="20%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
delay={0.11}
|
||||
/>
|
||||
<SparkleCross
|
||||
{...props}
|
||||
height="15%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: '80%',
|
||||
right: 0,
|
||||
}}
|
||||
delay={0.15}
|
||||
/>
|
||||
<SparkleStar
|
||||
{...props}
|
||||
height="30%"
|
||||
style={{
|
||||
...shapeMixin,
|
||||
top: '20%',
|
||||
left: '30%',
|
||||
transform: 'rotate(30deg)',
|
||||
}}
|
||||
delay={0.6}
|
||||
/>
|
||||
</div>
|
||||
);
|
27
src/design-system/atoms/sparkle/Sparkle.story.tsx
Normal file
27
src/design-system/atoms/sparkle/Sparkle.story.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import { atomStories } from 'atoms/atoms.story';
|
||||
import { SparkleOverlay } from './Sparkle';
|
||||
import { Button } from 'atoms/button';
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
import { Hero } from 'atoms/hero';
|
||||
|
||||
const story = atomStories('Sparkle', module);
|
||||
|
||||
story.add('Example Button', () => {
|
||||
return (
|
||||
<Hero>
|
||||
<SparkleOverlay
|
||||
opacity={number('Effect Opacity', 1, {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.001,
|
||||
range: true,
|
||||
})}
|
||||
size={number('Effect Size', -10)}
|
||||
repeatCount={3}
|
||||
>
|
||||
<Button>Yo check this!</Button>
|
||||
</SparkleOverlay>
|
||||
</Hero>
|
||||
);
|
||||
});
|
55
src/design-system/atoms/sparkle/Sparkle.tsx
Normal file
55
src/design-system/atoms/sparkle/Sparkle.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { palette } from "roleypoly/src/design-system/atoms/colors";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
import { SparklePatternAlpha, SparklePatternBeta } from "./Shapes";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
size?: number;
|
||||
opacity?: number;
|
||||
repeatCount?: number;
|
||||
};
|
||||
|
||||
const SparkleContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type EffectProps = {
|
||||
effectSize: Props["size"];
|
||||
effectOpacity: Props["opacity"];
|
||||
};
|
||||
|
||||
const SparkleEffect = styled.div<EffectProps>`
|
||||
position: absolute;
|
||||
top: ${(props) => props.effectSize}px;
|
||||
bottom: ${(props) => props.effectSize}px;
|
||||
left: ${(props) => props.effectSize}px;
|
||||
right: ${(props) => props.effectSize}px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
z-index: 5;
|
||||
opacity: ${(props) => props.effectOpacity};
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
export const SparkleOverlay = (props: Props) => (
|
||||
<SparkleContainer>
|
||||
<SparkleEffect
|
||||
effectSize={props.size || 0}
|
||||
effectOpacity={props.opacity || 1}
|
||||
>
|
||||
<SparklePatternAlpha
|
||||
repeatCount={props.repeatCount}
|
||||
height="100%"
|
||||
strokeColor={palette.gold400}
|
||||
/>
|
||||
<SparklePatternBeta
|
||||
repeatCount={props.repeatCount}
|
||||
height="100%"
|
||||
strokeColor={palette.gold400}
|
||||
/>
|
||||
</SparkleEffect>
|
||||
{props.children}
|
||||
</SparkleContainer>
|
||||
);
|
1
src/design-system/atoms/sparkle/index.ts
Normal file
1
src/design-system/atoms/sparkle/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Sparkle';
|
16
src/design-system/atoms/tab-view/BUILD.bazel
Normal file
16
src/design-system/atoms/tab-view/BUILD.bazel
Normal file
|
@ -0,0 +1,16 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "tab-view",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/breakpoints",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
41
src/design-system/atoms/tab-view/TabView.spec.tsx
Normal file
41
src/design-system/atoms/tab-view/TabView.spec.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TabView, Tab, TabViewProps } from './TabView';
|
||||
import { TabTitle, TabContent } from './TabView.styled';
|
||||
|
||||
const makeView = (props: Partial<TabViewProps> = {}) =>
|
||||
shallow(
|
||||
<TabView {...props}>
|
||||
{{
|
||||
'Tab 1': <Tab>{() => <div>tab 1</div>}</Tab>,
|
||||
'Tab 2': <Tab>{() => <div>tab 2</div>}</Tab>,
|
||||
}}
|
||||
</TabView>
|
||||
);
|
||||
|
||||
it('renders tab content correctly', () => {
|
||||
const view = makeView();
|
||||
|
||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1');
|
||||
});
|
||||
|
||||
it('automatically picks preselected tab content', () => {
|
||||
const view = makeView({ initialTab: 'Tab 2' });
|
||||
|
||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2');
|
||||
});
|
||||
|
||||
it('automatically uses the first tab when preselected tab is not present', () => {
|
||||
const view = makeView({ initialTab: 'Not a Tab' });
|
||||
|
||||
view.find(TabContent).find('i').simulate('load');
|
||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1');
|
||||
});
|
||||
|
||||
it('changes between tabs when tab is clicked', () => {
|
||||
const view = makeView();
|
||||
|
||||
view.find(TabTitle).at(1).simulate('click');
|
||||
|
||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2');
|
||||
});
|
37
src/design-system/atoms/tab-view/TabView.story.tsx
Normal file
37
src/design-system/atoms/tab-view/TabView.story.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import * as React from 'react';
|
||||
import { atomStories } from 'atoms/atoms.story';
|
||||
import { TabView, Tab } from './TabView';
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
|
||||
const story = atomStories('Tab View', module);
|
||||
|
||||
story.add('Multiple Tabs', () => (
|
||||
<TabView>
|
||||
{{
|
||||
'Tab 1': <Tab>{() => <div>tab 1</div>}</Tab>,
|
||||
'Tab 2': <Tab>{() => <div>tab 2</div>}</Tab>,
|
||||
}}
|
||||
</TabView>
|
||||
));
|
||||
|
||||
story.add('Single Tab', () => (
|
||||
<TabView>
|
||||
{{
|
||||
'Tab 1': <Tab>{() => <div>tab 1</div>}</Tab>,
|
||||
}}
|
||||
</TabView>
|
||||
));
|
||||
|
||||
story.add('Many Tabs', () => {
|
||||
const amount = number('Tab Count', 10);
|
||||
|
||||
const tabs = [...'0'.repeat(amount)].reduce(
|
||||
(acc, _, idx) => ({
|
||||
...acc,
|
||||
[`Tab ${idx + 1}`]: <Tab>{() => <div>tab {idx + 1}</div>}</Tab>,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
return <TabView>{tabs}</TabView>;
|
||||
});
|
43
src/design-system/atoms/tab-view/TabView.styled.ts
Normal file
43
src/design-system/atoms/tab-view/TabView.styled.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import styled, { css } from "styled-components";
|
||||
import { palette } from "roleypoly/src/design-system/atoms/colors";
|
||||
import { transitions } from "roleypoly/src/design-system/atoms/timings";
|
||||
import { onTablet } from "roleypoly/src/design-system/atoms/breakpoints";
|
||||
import * as _ from "styled-components"; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
export const TabViewStyled = styled.div``;
|
||||
|
||||
export const TabTitleRow = styled.div`
|
||||
display: flex;
|
||||
border-bottom: 1px solid ${palette.taupe100};
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const TabTitle = styled.div<{ selected: boolean }>`
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0.7em 1em;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: border-color ${transitions.in2out}s ease-in-out,
|
||||
color ${transitions.in2out}s ease-in-out;
|
||||
cursor: pointer;
|
||||
color: ${palette.taupe500};
|
||||
${(props) =>
|
||||
props.selected
|
||||
? css`
|
||||
color: unset;
|
||||
border-bottom-color: ${palette.taupe500};
|
||||
`
|
||||
: css`
|
||||
&:hover {
|
||||
border-bottom-color: ${palette.taupe300};
|
||||
color: unset;
|
||||
}
|
||||
`};
|
||||
${onTablet(css`
|
||||
padding: 0.45em 1em;
|
||||
`)}
|
||||
`;
|
||||
|
||||
export const TabContent = styled.div``;
|
48
src/design-system/atoms/tab-view/TabView.tsx
Normal file
48
src/design-system/atoms/tab-view/TabView.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import * as React from 'react';
|
||||
import { TabTitleRow, TabContent, TabViewStyled, TabTitle } from './TabView.styled';
|
||||
|
||||
export type TabViewProps = {
|
||||
children: { [title: string]: React.ReactNode };
|
||||
initialTab?: string;
|
||||
};
|
||||
|
||||
type TabProps = {
|
||||
children: () => React.ReactNode;
|
||||
};
|
||||
|
||||
export const TabView = (props: TabViewProps) => {
|
||||
const tabNames = Object.keys(props.children);
|
||||
|
||||
if (tabNames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [currentTab, setCurrentTab] = React.useState<keyof TabViewProps['children']>(
|
||||
props.initialTab ?? tabNames[0]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabViewStyled>
|
||||
<TabTitleRow>
|
||||
{tabNames.map((tabName, idx) => (
|
||||
<TabTitle
|
||||
selected={currentTab === tabName}
|
||||
onClick={() => setCurrentTab(tabName)}
|
||||
key={`tab${tabName}${idx}`}
|
||||
>
|
||||
{tabName}
|
||||
</TabTitle>
|
||||
))}
|
||||
</TabTitleRow>
|
||||
<TabContent>
|
||||
{props.children[currentTab] || (
|
||||
<i onLoad={() => setCurrentTab(tabNames[0])}>
|
||||
Tabs were misconfigured, resetting to zero.
|
||||
</i>
|
||||
)}
|
||||
</TabContent>
|
||||
</TabViewStyled>
|
||||
);
|
||||
};
|
||||
|
||||
export const Tab = (props: TabProps) => <div>{props.children()}</div>;
|
1
src/design-system/atoms/tab-view/index.ts
Normal file
1
src/design-system/atoms/tab-view/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './TabView';
|
14
src/design-system/atoms/text-input/BUILD.bazel
Normal file
14
src/design-system/atoms/text-input/BUILD.bazel
Normal file
|
@ -0,0 +1,14 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "text-input",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/colors",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
36
src/design-system/atoms/text-input/TextInput.stories.tsx
Normal file
36
src/design-system/atoms/text-input/TextInput.stories.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react";
|
||||
import { TextInput, TextInputWithIcon } from "./TextInput";
|
||||
import { SmallTitle } from "roleypoly/src/design-system/atoms/typography";
|
||||
import { FiKey } from "react-icons/fi";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Text Input",
|
||||
argTypes: {
|
||||
placeholder: { control: "text" },
|
||||
},
|
||||
args: {
|
||||
placeholder: "Fill me in!",
|
||||
},
|
||||
};
|
||||
|
||||
export const Common = (args) => (
|
||||
<div>
|
||||
<SmallTitle>TextInput</SmallTitle>
|
||||
<div>
|
||||
<TextInput {...args} />
|
||||
</div>
|
||||
<div>
|
||||
<TextInput {...args} disabled />
|
||||
</div>
|
||||
<SmallTitle>TextInputWithIcon</SmallTitle>
|
||||
<div>
|
||||
<TextInputWithIcon icon={<FiKey />} {...args} />
|
||||
</div>
|
||||
<div>
|
||||
<TextInputWithIcon icon={<FiKey />} {...args} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<TextInputWithIcon icon={<FiKey />} {...args} type="password" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
83
src/design-system/atoms/text-input/TextInput.tsx
Normal file
83
src/design-system/atoms/text-input/TextInput.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { palette } from "roleypoly/src/design-system/atoms/colors";
|
||||
|
||||
const StyledTextInput = styled.input`
|
||||
appearance: none;
|
||||
border: 1px solid ${palette.taupe200};
|
||||
border-radius: 3px;
|
||||
line-height: 163%;
|
||||
padding: 12px 16px;
|
||||
font-size: 1.2rem;
|
||||
background-color: ${palette.taupe300};
|
||||
color: ${palette.grey600};
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: 97vw;
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
border-color: ${palette.grey100};
|
||||
box-shadow: 1px 0 3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
[disabled],
|
||||
:disabled {
|
||||
cursor: not-allowed;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:hover:not([disabled]) {
|
||||
border-color: ${palette.grey100};
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${palette.taupe500};
|
||||
}
|
||||
`;
|
||||
|
||||
type TextInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
_override?: React.Component;
|
||||
};
|
||||
|
||||
export const TextInput = (props: TextInputProps) => {
|
||||
const { ...rest } = props;
|
||||
return <StyledTextInput {...rest} />;
|
||||
};
|
||||
|
||||
const StyledTextInputWithIcon = styled(StyledTextInput)`
|
||||
padding-left: 36px;
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const IconInputContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type TextInputWithIconProps = TextInputProps & {
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
export const TextInputWithIcon = (props: TextInputWithIconProps) => {
|
||||
const { icon, ...rest } = props;
|
||||
return (
|
||||
<IconInputContainer>
|
||||
<IconContainer>{icon}</IconContainer>
|
||||
<StyledTextInputWithIcon {...rest}></StyledTextInputWithIcon>
|
||||
</IconInputContainer>
|
||||
);
|
||||
};
|
1
src/design-system/atoms/text-input/index.ts
Normal file
1
src/design-system/atoms/text-input/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './TextInput';
|
7
src/design-system/atoms/timings/BUILD.bazel
Normal file
7
src/design-system/atoms/timings/BUILD.bazel
Normal file
|
@ -0,0 +1,7 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "timings",
|
||||
)
|
1
src/design-system/atoms/timings/index.ts
Normal file
1
src/design-system/atoms/timings/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './timings';
|
6
src/design-system/atoms/timings/timings.ts
Normal file
6
src/design-system/atoms/timings/timings.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const transitions = {
|
||||
in2in: 0.3,
|
||||
out2in: 0.15,
|
||||
in2out: 0.25,
|
||||
actionable: 0.07,
|
||||
};
|
11
src/design-system/atoms/typist/BUILD.bazel
Normal file
11
src/design-system/atoms/typist/BUILD.bazel
Normal file
|
@ -0,0 +1,11 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "timings",
|
||||
deps = [
|
||||
"react",
|
||||
"@types/react",
|
||||
],
|
||||
)
|
31
src/design-system/atoms/typist/Typist.spec.tsx
Normal file
31
src/design-system/atoms/typist/Typist.spec.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { mount } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Typist } from './Typist';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
it('correctly cycles through provided lines', () => {
|
||||
const lines = ['abcdef', 'ghijkl'];
|
||||
act(() => {
|
||||
let view = mount(<Typist charTimeout={100} resetTimeout={10000} lines={lines} />);
|
||||
|
||||
jest.advanceTimersByTime(100 * lines[0].length);
|
||||
view = view.update();
|
||||
expect(view.text()).toBe(lines[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly cycles through provided characters in a line', () => {
|
||||
const lines = ['abcdef'];
|
||||
|
||||
act(() => {
|
||||
let view = mount(<Typist charTimeout={1} resetTimeout={100} lines={lines} />);
|
||||
|
||||
Array(...lines[0]).forEach((_, idx) => {
|
||||
view = view.update();
|
||||
expect(view.text()).toBe(lines[0].slice(0, idx));
|
||||
jest.advanceTimersByTime(1);
|
||||
});
|
||||
});
|
||||
});
|
16
src/design-system/atoms/typist/Typist.stories.tsx
Normal file
16
src/design-system/atoms/typist/Typist.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import * as React from "react";
|
||||
import { Typist } from "./Typist";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Typist",
|
||||
component: Typist,
|
||||
args: {
|
||||
charTimeout: 75,
|
||||
resetTimeout: 2000,
|
||||
lines: ["hello world", "and again", "a third", "story time!"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Looping = (args) => {
|
||||
return <Typist {...args} />;
|
||||
};
|
37
src/design-system/atoms/typist/Typist.tsx
Normal file
37
src/design-system/atoms/typist/Typist.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import * as React from "react";
|
||||
|
||||
type TypistProps = {
|
||||
resetTimeout: number;
|
||||
charTimeout: number;
|
||||
lines: string[];
|
||||
};
|
||||
|
||||
export const Typist = (props: TypistProps) => {
|
||||
const [outputText, setOutputText] = React.useState("");
|
||||
const [currentLine, setCurrentLine] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fullLine = props.lines[currentLine];
|
||||
|
||||
if (outputText === fullLine) {
|
||||
const timeout = setTimeout(() => {
|
||||
setOutputText("");
|
||||
setCurrentLine((currentLine + 1) % props.lines.length);
|
||||
}, props.resetTimeout);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setOutputText(fullLine.slice(0, outputText.length + 1));
|
||||
}, props.charTimeout);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [currentLine, outputText]);
|
||||
|
||||
return <>{outputText}</>;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue