feat: Add majority of design system components, build system fixes (#11)

* feat(design-system): pre-port of roleypoly/ui

* feat(design-system): port molecules

* chore(design-system): prettier

* feat(design-system): add intro card and MDX components

* fix(common/utils): hack fixtures test data moved to design-system, update accordingly

* chore: document protoReflection.ts

* fix(design-system): some molecules missed the magic fuckery

* ci: keep going on bazel test failures

* fix(design-system): server masthead molecule missed the magic fuckery

* chore: fix ts paths

* chore: fix docker publisher

* chore: fix docker publisher names

* chore(discord-bot): fix publisher

* chore(discord-bot): fix publisher
This commit is contained in:
41666 2020-10-14 22:33:01 -04:00 committed by GitHub
parent c41fcabfd0
commit 89f237cf22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
133 changed files with 2795 additions and 278 deletions

View file

@ -25,7 +25,7 @@ jobs:
- name: Test
run: |
"${GITHUB_WORKSPACE}/bin/bazel" test \
-c opt \
-k -c opt \
--stamp \
--workspace_status_command hack/workspace_status.sh --\
//src/... //hack/... -//hack/dev-container/...

View file

@ -1,3 +1,10 @@
import { roleypolyTheme } from './theme';
import { mdxComponents } from '../src/design-system/atoms/typography/mdx';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
docs: {
theme: roleypolyTheme,
components: mdxComponents,
},
};

View file

@ -9,7 +9,7 @@ export const roleypolyTheme = create({
// UI
appBg: palette.taupe300,
appContentBg: palette.taupe300,
appContentBg: palette.taupe200,
appBorderColor: palette.taupe100,
appBorderRadius: 0,

View file

@ -1,12 +0,0 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "fixtures",
deps = [
"//src/rpc/discord",
"//src/rpc/platform",
"//src/rpc/shared",
],
)

View file

@ -1,237 +0,0 @@
import { Member } from 'roleypoly/src/rpc/discord';
import { Category, GuildData, GuildEnumeration } from 'roleypoly/src/rpc/platform';
import {
DiscordUser,
Guild,
GuildRoles,
Role,
RoleypolyUser,
} from 'roleypoly/src/rpc/shared';
export const roleCategory: Role.AsObject[] = [
{
id: 'aaa',
permissions: 0,
name: 'She/Her',
color: 0xffc0cb,
position: 1,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'bbb',
permissions: 0,
name: 'He/Him',
color: 0xc0ebff,
position: 2,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'ccc',
permissions: 0,
name: 'They/Them',
color: 0xc0ffd5,
position: 3,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'ddd',
permissions: 0,
name: 'Reee',
color: 0xff0000,
position: 4,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'eee',
permissions: 0,
name: 'black but actually bravely default',
color: 0x000000,
position: 5,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'fff',
permissions: 0,
name: 'b̻͌̆̽ͣ̃ͭ̊l͚̥͙̔ͨ̊aͥć͕k͎̟͍͕ͥ̋ͯ̓̈̉̋i͛̄̔͂̚̚҉̳͈͔̖̼̮ṣ̤̗̝͊̌͆h͈̭̰͔̥̯ͅ',
color: 0x1,
position: 6,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'unsafe1',
permissions: 0,
name: 'too high',
color: 0xff0088,
position: 7,
managed: false,
safety: Role.RoleSafety.HIGHERTHANBOT,
},
{
id: 'unsafe2',
permissions: 0x00000008 | 0x10000000,
name: 'too strong',
color: 0x00ff88,
position: 8,
managed: false,
safety: Role.RoleSafety.DANGEROUSPERMISSIONS,
},
];
export const mockCategory: Category.AsObject = {
id: 'aaa',
name: 'Mock',
rolesList: roleCategory.map((x) => x.id),
hidden: false,
type: Category.CategoryType.MULTI,
position: 0,
};
export const roleCategory2: Role.AsObject[] = [
{
id: 'ddd2',
permissions: 0,
name: 'red',
color: 0xff0000,
position: 9,
managed: false,
safety: Role.RoleSafety.SAFE,
},
{
id: 'eee2',
permissions: 0,
name: 'green',
color: 0x00ff00,
position: 10,
managed: false,
safety: Role.RoleSafety.SAFE,
},
];
export const mockCategorySingle: Category.AsObject = {
id: 'bbb',
name: 'Mock Single 岡野',
rolesList: roleCategory2.map((x) => x.id),
hidden: false,
type: Category.CategoryType.SINGLE,
position: 0,
};
export const guildRoles: GuildRoles.AsObject = {
id: 'aaa',
rolesList: [...roleCategory, ...roleCategory2],
};
export const roleWikiData = {
aaa: 'Typically used by feminine-identifying people',
bbb: 'Typically used by masculine-identifying people',
ccc: 'Typically used to refer to all people as a singular neutral.',
};
export const guild: Guild.AsObject = {
name: 'emoji megaporium',
id: 'aaa',
icon:
'https://cdn.discordapp.com/icons/421896162539470888/3372fd895ed913b55616c5e49cd50e60.png?size=256',
ownerid: 'bbb',
membercount: 23453,
splash: '',
};
export const guildMap: { [x: string]: Guild.AsObject } = {
'emoji megaporium': guild,
Roleypoly: {
name: 'Roleypoly',
id: 'aaa',
icon:
'https://cdn.discordapp.com/icons/203493697696956418/ff08d36f5aee1ff48f8377b65d031ab0.png?size=256',
ownerid: 'bbb',
membercount: 23453,
splash: '',
},
'chamber of secrets': {
name: 'chamber of secrets',
id: 'aaa',
icon: '',
ownerid: 'bbb',
membercount: 23453,
splash: '',
},
Eclipse: {
name: 'Eclipse',
id: 'aaa',
icon:
'https://cdn.discordapp.com/icons/408821059161423873/49dfdd8b2456e2977e80a8b577b19c0d.png?size=256',
ownerid: 'bbb',
membercount: 23453,
splash: '',
},
};
export const guildData: GuildData.AsObject = {
id: 'aaa',
message: 'henlo worl!!',
categoriesList: [mockCategory, mockCategorySingle],
entitlementsList: [],
};
export const user: DiscordUser.AsObject = {
id: '123',
username: 'okano cat',
discriminator: '3266',
avatar:
'https://cdn.discordapp.com/avatars/62601275618889728/b1292bb974557337702cb941fc038085.png',
bot: false,
};
export const member: Member.AsObject = {
guildid: 'aaa',
rolesList: ['aaa', 'eee', 'unsafe2', 'ddd2'],
nick: 'okano cat',
user: user,
};
export const rpUser: RoleypolyUser.AsObject = {
discorduser: user,
};
export const guildEnum: GuildEnumeration.AsObject = {
guildsList: [
{
id: 'aaa',
guild: guildMap['emoji megaporium'],
member,
data: guildData,
roles: guildRoles,
},
{
id: 'bbb',
guild: guildMap['Roleypoly'],
member: {
...member,
rolesList: ['unsafe2'],
},
data: guildData,
roles: guildRoles,
},
{
id: 'ccc',
guild: guildMap['chamber of secrets'],
member,
data: guildData,
roles: guildRoles,
},
{
id: 'ddd',
guild: guildMap['Eclipse'],
member,
data: guildData,
roles: guildRoles,
},
],
};

View file

@ -14,7 +14,7 @@ def render_deps(deps = []):
"@npm//@improbable-eng/grpc-web",
])
has_added_grpc_deps = True
elif dep.startswith("//"):
elif dep.startswith("//") or dep.startswith("@npm//"):
output_deps.append(dep)
else:
output_deps.append("@npm//" + dep)

View file

@ -30,6 +30,7 @@
"react": "16.13.1",
"react-dom": "16.13.1",
"react-icons": "^3.11.0",
"react-tooltip": "^4.2.10",
"styled-components": "^5.2.0"
},
"devDependencies": {

View file

@ -19,7 +19,7 @@ react_library(
jest_test(
src = ":utils",
deps = [
"//hack/fixtures",
"//src/design-system/shared-types",
"//src/rpc/shared",
],
)

View file

@ -1,6 +1,6 @@
import { hasPermission, permissions, hasPermissionOrAdmin } from './hasPermission';
import { Role } from 'roleypoly/src/rpc/shared';
import { guildRoles } from 'roleypoly/hack/fixtures/storyData';
import { guildRoles } from 'roleypoly/src/design-system/shared-types/storyData';
const roles: Role.AsObject[] = [
{

View file

@ -1,5 +1,5 @@
import { DiscordUser } from 'roleypoly/src/rpc/shared';
import { user } from 'roleypoly/hack/fixtures/storyData';
import { user } from 'roleypoly/src/design-system/shared-types/storyData';
import { AsObjectToProto } from './protoReflection';
it('converts a RoleypolyUser.AsObject back to protobuf', () => {

View file

@ -1,26 +1,43 @@
import * as pbjs from 'google-protobuf';
// Protobuf Message itself
type GenericObject<T extends pbjs.Message> = T;
// Message's "setter" call
type ProtoFunction<T extends pbjs.Message, U extends ReturnType<T['toObject']>> = (
value: U[keyof U]
) => void;
/**
* AsObjectToProto does the opposite of ProtoMessage.toObject().
* This function turns regular JS objects back into their source protobuf message type,
* with the help us copious amounts of reflection.
* @param protoClass A protobuf message class
* @param input A JS object that corresponds to the protobuf message class.
*/
export const AsObjectToProto = <T extends pbjs.Message>(
protoClass: { new (): T },
input: ReturnType<T['toObject']>
): GenericObject<T> => {
// First, we create the message itself
const proto = new protoClass();
// We want the keys from the message, this will give us the setter names we need.
const protoKeys = Object.getOwnPropertyNames((proto as any).__proto__);
// Loop over the input data keys
for (let inputKey in input) {
// As we loop, find the setter function for the key
const setCallName = protoKeys.find(
(key) => `set${inputKey.toLowerCase()}` === key.toLowerCase()
) as keyof typeof proto;
// If we encounter a key without a place to go, we silently ignore it.
if (!setCallName) {
continue;
}
// But, if it all succeeds, we call the setter with the JS object's value.
((proto[setCallName] as unknown) as ProtoFunction<T, typeof input>)(
input[inputKey]
);

View file

@ -0,0 +1,24 @@
import { Meta } from '@storybook/addon-docs/blocks';
import { Logotype } from 'roleypoly/src/design-system/atoms/branding';
import { Space } from 'roleypoly/src/design-system/atoms/space';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
<Meta title="Roleypoly Design System" />
<Logotype height="4em" circleFill={palette.taupe100} />
<Space />
# Rapid UI
#### Roleypoly Design System
This is a tool for Roleypoly developers to design and show off UI
components as they build them.
If you're interested in helping build Roleypoly, [please visit the GitHub project.][roleypoly]
All components here follow the [Atomic Design System][atomic], and might be used in any number of Roleypoly UI systems, not limited to
just the end user web application.
[roleypoly]: https://github.com/roleypoly/roleypoly
[atomic]: https://bradfrost.com/blog/post/atomic-web-design/

View file

@ -8,7 +8,7 @@ The Roleypoly Design System (rapid) is an atomic design system to help rapidly a
**Please follow hermeticity considerations.**
This package cannot reference RPC types, as they do not exist in the outside world. Storybook is the core component of this, and Storybook doesn't know how to find RPC types at CI build time, as Bazel is also not present.
This package cannot reference RPC types, as they do not exist in the outside world. Storybook is the core component of this, and Storybook doesn't know how to find RPC types at CI build time, as Bazel is also not present. If you are worried about RPC types being compatible, please write a unit test and include the RPC types then.
You need:

View file

@ -1,4 +1,5 @@
load("//:hack/react.bzl", "react_library")
load("//:hack/jest.bzl", "jest_test")
package(default_visibility = ["//visibility:public"])
@ -14,3 +15,7 @@ react_library(
"@types/styled-components",
],
)
jest_test(
src = ":tab-view",
)

View file

@ -4,7 +4,7 @@ load("//:hack/jest.bzl", "jest_test")
package(default_visibility = ["//visibility:public"])
react_library(
name = "timings",
name = "typist",
deps = [
"react",
"@types/react",
@ -12,5 +12,5 @@ react_library(
)
jest_test(
src = ":timings",
src = ":typist",
)

View file

@ -6,6 +6,8 @@ react_library(
name = "typography",
deps = [
"styled-components",
"//src/design-system/atoms/colors",
"//src/design-system/atoms/timings",
"@types/styled-components",
],
)

View file

@ -0,0 +1,33 @@
import styled from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
import {
AccentTitle,
LargeTitle,
Link,
MediumTitle,
SmallTitle,
Text,
text600,
text700,
text800,
text900,
} from './typography';
export const mdxComponents = {
h1: styled.h1`
${text900}
`,
h2: styled.h2`
${text800}
`,
h3: styled.h3`
${text700}
`,
h4: styled.h4`
${text600}
`,
p: styled.p`
${Text}
`,
a: Link,
};

View file

@ -107,3 +107,12 @@ export const Spacing = () => (
})}
</div>
);
export const Link = () => (
<typography.Link
target="_blank"
href="https://images.boredomfiles.com/wp-content/uploads/sites/5/2016/03/fox-door-5.png"
>
Here is a link &lt;3
</typography.Link>
);

View file

@ -1,4 +1,6 @@
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 * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
const reset = css`
@ -86,3 +88,12 @@ export const AmbientLarge = styled.span`
export const AmbientSmall = styled.span`
${text100}
`;
export const Link = styled.a`
color: ${palette.taupe500};
text-decoration: none;
transition: color ${transitions.actionable}s ease-in-out;
&:hover {
color: ${palette.taupe600};
}
`;

View file

@ -0,0 +1,16 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "demo-discord",
deps = [
"react",
"styled-components",
"//src/design-system/atoms/colors",
"//src/design-system/atoms/typist",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,8 @@
import * as React from 'react';
import { DemoDiscord } from './DemoDiscord';
export default {
title: 'Molecules/Role Demos',
};
export const Discord = () => <DemoDiscord />;

View file

@ -0,0 +1,68 @@
import styled, { keyframes } from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
import { palette } from 'roleypoly/src/design-system/atoms/colors';
export const Base = styled.div`
background-color: ${palette.discord100};
border: solid 1px rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding: 10px;
user-select: none;
`;
export const Timestamp = styled.span`
padding: 0 5px;
font-size: 0.7em;
opacity: 0.3;
`;
export const TextParts = styled.span`
padding: 0 5px;
`;
export const Username = styled(TextParts)`
&:hover {
text-decoration: underline;
cursor: pointer;
}
`;
export const InputBox = styled.div`
margin-top: 10px;
background-color: ${palette.discord200};
padding: 7px 10px;
border-radius: 3px;
`;
const lineBlink = keyframes`
0% {
opacity: 1;
}
40% {
opacity: 1;
}
60% {
opacity: 0;
}
100% {
opacity: 0;
}
`;
export const Line = styled.div`
background-color: ${palette.grey600};
width: 1px;
height: 1.5em;
display: inline-block;
position: absolute;
right: -5px;
animation: ${lineBlink} 0.5s ease-in-out infinite alternate-reverse;
`;
export const InputTextAlignment = styled.div`
position: relative;
display: inline-block;
`;

View file

@ -0,0 +1,53 @@
import * as React from 'react';
import {
Base,
Timestamp,
TextParts,
Username,
InputBox,
Line,
InputTextAlignment,
} from './DemoDiscord.styled';
import { demoData } from 'roleypoly/src/design-system/shared-types/demoData';
import { Typist } from 'roleypoly/src/design-system/atoms/typist';
export const DemoDiscord = () => {
const time = new Date();
const timeString = time.toTimeString();
const [easterEggCount, setEasterEggCount] = React.useState(0);
return (
<Base>
<Timestamp>
{time.getHours() % 12}:{timeString.slice(3, 5)}&nbsp;
{time.getHours() <= 12 ? 'AM' : 'PM'}
</Timestamp>
<Username onClick={() => setEasterEggCount(easterEggCount + 1)}>
okano&nbsp;cat
</Username>
<TextParts>
{easterEggCount >= 15
? `NYAAAAAAA${'A'.repeat(easterEggCount - 15)}`
: easterEggCount >= 11
? `I'm.. I'm gonna...`
: easterEggCount >= 10
? `S-senpai... Be careful...`
: easterEggCount >= 5
? `H-hey... Stop that..`
: `Hey, I'd like some roles!`}
</TextParts>
<InputBox>
<InputTextAlignment>
&nbsp;
<Typist
resetTimeout={2000}
charTimeout={75}
lines={demoData.map((role) => `.iam ${role.name}`)}
/>
<Line />
</InputTextAlignment>
</InputBox>
</Base>
);
};

View file

@ -0,0 +1 @@
export * from './DemoDiscord';

View file

@ -0,0 +1,15 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "demo-picker",
deps = [
"react",
"styled-components",
"//src/design-system/atoms/role",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,8 @@
import * as React from 'react';
import { DemoPicker } from './DemoPicker';
export default {
title: 'Molecules/Role Demos',
};
export const Picker = () => <DemoPicker />;

View file

@ -0,0 +1,46 @@
import * as React from 'react';
import { Role } from 'roleypoly/src/design-system/atoms/role';
import { Role as RPCRole } from 'roleypoly/src/design-system/shared-types';
import styled from 'styled-components';
import { demoData } from 'roleypoly/src/design-system/shared-types/demoData';
const Container = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
align-content: center;
height: 95px;
`;
const RoleWrap = styled.div`
padding: 2.5px;
display: inline-block;
`;
export const DemoPicker = () => {
const [selectedStates, setSelectedStates] = React.useState<
{
[key in RPCRole['id']]: boolean;
}
>(demoData.reduce((acc, role) => ({ ...acc, [role.id]: false }), {}));
return (
<Container>
{demoData.map((role) => (
<RoleWrap key={`role${role.id}`}>
<Role
role={role}
selected={selectedStates[role.id]}
onClick={() => {
setSelectedStates({
...selectedStates,
[role.id]: !selectedStates[role.id],
});
}}
/>
</RoleWrap>
))}
</Container>
);
};

View file

@ -0,0 +1 @@
export * from './DemoPicker';

View file

@ -0,0 +1,17 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "footer",
deps = [
"react",
"react-icons",
"styled-components",
"//src/design-system/atoms/colors",
"//src/design-system/atoms/timings",
"//src/design-system/atoms/typography",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,94 @@
import * as React from 'react';
type FlagsProps = {
width?: number | string;
height?: number | string;
};
export const Flags = (props: FlagsProps) => (
<svg width={props.width} height={props.height} viewBox="0 0 3372 900" version="1.1">
<defs>
<rect id="path-3" x="1772" y="0" width="1600" height="900" rx="100"></rect>
</defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="Rectangle-5"></g>
<g id="Trans">
<rect
id="Rectangle"
fill="#55CDFC"
x="0"
y="0"
width="1600"
height="900"
rx="100"
></rect>
<rect
id="Rectangle-2"
fill="#F7A8B8"
x="0"
y="170"
width="1600"
height="560"
></rect>
<rect
id="Rectangle-3"
fill="#FFFFFF"
x="0"
y="350"
width="1600"
height="200"
></rect>
</g>
<mask id="mask-4" fill="white">
<use href="#path-3"></use>
</mask>
<g id="Rectangle-5"></g>
<g id="Geyy" mask="url(#mask-4)">
<g transform="translate(1772.000000, 0.000000)" id="Rectangle-4">
<rect
fill="#F9238B"
x="0"
y="0"
width="1600"
height="151.006711"
></rect>
<rect
fill="#FB7B04"
x="0"
y="150"
width="1600"
height="151.006711"
></rect>
<rect
fill="#FFCA66"
x="0"
y="300"
width="1600"
height="151.006711"
></rect>
<rect
fill="#00B289"
x="0"
y="450"
width="1600"
height="151.006711"
></rect>
<rect
fill="#5A38B5"
x="0"
y="598.993289"
width="1600"
height="151.006711"
></rect>
<rect
fill="#B413F5"
x="0"
y="748.993289"
width="1600"
height="151.006711"
></rect>
</g>
</g>
</g>
</svg>
);

View file

@ -0,0 +1,9 @@
import * as React from 'react';
import { Footer as FooterComponent } from './Footer';
export default {
title: 'Molecules',
component: FooterComponent,
};
export const Footer = (args) => <FooterComponent {...args} />;

View file

@ -0,0 +1,31 @@
import styled from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
export const FooterWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
text-align: center;
a {
color: ${palette.taupe500};
text-decoration: none;
transition: color ${transitions.actionable}s ease-in-out;
&:hover {
color: ${palette.taupe600};
}
}
`;
export const HoverColor = styled.div`
opacity: 0.3;
filter: saturate(0);
transition: all ${transitions.in2in}s ease-in-out;
&:hover {
opacity: 1;
filter: none;
}
`;

View file

@ -0,0 +1,27 @@
import * as React from 'react';
import { FooterWrapper, HoverColor } from './Footer.styled';
import { AmbientLarge } from 'roleypoly/src/design-system/atoms/typography';
import { FaHeart } from 'react-icons/fa';
import { Flags } from './Flags';
const year = new Date().getFullYear();
export const Footer = () => (
<FooterWrapper>
<AmbientLarge>
<div>
&copy; {year} Roleypoly &ndash; Made with{' '}
<FaHeart size={'0.8em'} color={'#fe4365'} />
&nbsp;in Raleigh, NC
</div>
<div>
<a href="https://discord.gg/Xj6rK3E">Discord/Support</a> &ndash;&nbsp;
<a href="https://patreon.com/kata">Patreon</a> &ndash;&nbsp;
<a href="https://github.com/roleypoly">GitHub</a>
</div>
<HoverColor>
<Flags height={'1em'} />
</HoverColor>
</AmbientLarge>
</FooterWrapper>
);

View file

@ -0,0 +1 @@
export * from './Footer';

View file

@ -0,0 +1,21 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "guild-nav",
deps = [
"next",
"react",
"react-icons",
"react-tooltip",
"styled-components",
"//src/common/utils",
"//src/design-system/atoms/colors",
"//src/design-system/atoms/timings",
"//src/design-system/molecules/nav-slug",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,21 @@
import * as React from 'react';
import { GuildNav } from './GuildNav';
import { guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
import { PopoverBase } from 'roleypoly/src/design-system/atoms/popover/Popover.styled';
export default {
title: 'Molecules/Guild Nav',
component: GuildNav,
};
export const HasGuilds = () => (
<PopoverBase active>
<GuildNav guildEnumeration={guildEnum} />
</PopoverBase>
);
export const NoGuilds = () => (
<PopoverBase active>
<GuildNav guildEnumeration={{ guildsList: [] }} />
</PopoverBase>
);

View file

@ -0,0 +1,22 @@
import styled from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
export const GuildNavItem = styled.a`
display: flex;
align-items: center;
transition: border ${transitions.in2in}s ease-in-out;
border: 1px solid transparent;
border-radius: 3px;
box-sizing: border-box;
margin: 5px;
user-select: none;
color: unset;
text-decoration: none;
&:hover {
border-color: ${palette.taupe300};
cursor: pointer;
}
`;

View file

@ -0,0 +1,62 @@
import Link from 'next/link';
import * as React from 'react';
import { GoStar, GoZap } from 'react-icons/go';
import ReactTooltip from 'react-tooltip';
import { hasPermission, permissions } from 'roleypoly/src/common/utils/hasPermission';
import { sortBy } from 'roleypoly/src/common/utils/sortBy';
import {
GuildEnumeration,
PresentableGuild,
Role,
Guild,
} from 'roleypoly/src/design-system/shared-types';
import { NavSlug } from 'roleypoly/src/design-system/molecules/nav-slug';
import { GuildNavItem } from './GuildNav.styled';
type Props = {
guildEnumeration: GuildEnumeration;
};
const tooltipId = 'guildnav';
const Badges = (props: { guild: PresentableGuild }) => {
return React.useMemo(() => {
if (!props.guild.member) {
return null;
}
const roles = props.guild.member.rolesList
.map((id) => {
if (!props.guild.roles) {
return undefined;
}
return props.guild.roles.rolesList.find((role) => role.id === id);
})
.filter((x) => !!x) as Role[];
if (hasPermission(roles, permissions.ADMINISTRATOR)) {
return <GoStar data-tip="Administrator" data-for={tooltipId} />;
}
if (hasPermission(roles, permissions.MANAGE_ROLES)) {
return <GoZap data-tip="Role Editor" data-for={tooltipId} />;
}
return null;
}, [props.guild]);
};
export const GuildNav = (props: Props) => (
<div>
{sortBy(props.guildEnumeration.guildsList, 'id').map((guild) => (
<Link href={`/s/${guild.id}`} passHref>
<GuildNavItem>
<NavSlug guild={guild.guild || null} key={guild.id} />
<Badges guild={guild} />
</GuildNavItem>
</Link>
))}
<ReactTooltip id={tooltipId} />
</div>
);

View file

@ -0,0 +1 @@
export * from './GuildNav';

View file

@ -0,0 +1,16 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "nav-slug",
deps = [
"react",
"react-icons",
"styled-components",
"//src/design-system/atoms/avatar",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,11 @@
import * as React from 'react';
import { NavSlug } from './NavSlug';
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
export default {
title: 'Molecules/Server Slug',
component: NavSlug,
};
export const Empty = () => <NavSlug guild={null} />;
export const Example = () => <NavSlug guild={guild} />;

View file

@ -0,0 +1,17 @@
import styled from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
export const SlugContainer = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
padding: 5px;
`;
export const SlugName = styled.div`
padding: 0 10px;
position: relative;
top: -1px;
white-space: nowrap;
text-overflow: ellipsis;
`;

View file

@ -0,0 +1,18 @@
import * as React from 'react';
import { Guild } from 'roleypoly/src/design-system/shared-types';
import { Avatar, utils } from 'roleypoly/src/design-system/atoms/avatar';
import { SlugContainer, SlugName } from './NavSlug.styled';
import { GoOrganization } from 'react-icons/go';
type Props = {
guild: Guild | null;
};
export const NavSlug = (props: Props) => (
<SlugContainer>
<Avatar src={props.guild?.icon} deliberatelyEmpty={!props.guild} size={35}>
{props.guild ? utils.initialsFromName(props.guild.name) : <GoOrganization />}
</Avatar>
<SlugName>{props.guild?.name || <>Your Guilds</>}</SlugName>
</SlugContainer>
);

View file

@ -0,0 +1 @@
export * from './NavSlug';

View file

@ -0,0 +1,17 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "picker-category",
deps = [
"react",
"react-tooltip",
"styled-components",
"//src/design-system/atoms/role",
"//src/design-system/atoms/typography",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,38 @@
import * as React from 'react';
import {
roleWikiData,
roleCategory,
mockCategory,
} from 'roleypoly/src/design-system/shared-types/storyData';
import { PickerCategory } from './PickerCategory';
export default {
title: 'Molecules/Picker Category',
component: PickerCategory,
args: {
title: 'Pronouns',
roles: roleCategory,
category: mockCategory,
selectedRoles: [],
},
};
export const Default = (args) => {
return <PickerCategory {...args} />;
};
export const Single = (args) => {
return <PickerCategory {...args} type="single" />;
};
Single.args = {
type: 'single',
};
export const Multi = (args) => {
return <PickerCategory {...args} type="single" />;
};
Multi.args = {
type: 'multi',
};
export const Wiki = (args) => {
return <PickerCategory {...args} wikiMode roleWikiData={roleWikiData} />;
};

View file

@ -0,0 +1,21 @@
import styled from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
export const Head = styled.div`
margin: 7px 5px;
line-height: 200%;
display: flex;
align-items: center;
justify-content: space-between;
`;
export const HeadTitle = styled.div`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
export const HeadSub = styled.div`
flex-shrink: 0;
margin-top: -4px;
`;

View file

@ -0,0 +1,67 @@
import * as React from 'react';
import ReactTooltip from 'react-tooltip';
import { Role } from 'roleypoly/src/design-system/atoms/role';
import { AmbientLarge, LargeText } from 'roleypoly/src/design-system/atoms/typography';
import {
Category as RPCCategory,
Role as RPCRole,
RoleSafety,
} from 'roleypoly/src/design-system/shared-types';
import styled from 'styled-components';
import { Head, HeadSub, HeadTitle } from './PickerCategory.styled';
export type CategoryProps = {
title: string;
roles: RPCRole[];
category: RPCCategory;
selectedRoles: string[];
onChange: (role: RPCRole) => (newState: boolean) => void;
type: 'single' | 'multi';
} & (
| {
wikiMode: true;
roleWikiData: { [roleId: string]: string };
}
| {
wikiMode: false;
}
);
const Category = styled.div`
display: flex;
flex-wrap: wrap;
`;
const Container = styled.div`
overflow: hidden;
padding: 5px;
`;
export const PickerCategory = (props: CategoryProps) => (
<div>
<Head>
<HeadTitle>
<LargeText>{props.title}</LargeText>
</HeadTitle>
{props.type === 'single' && (
<HeadSub>
<AmbientLarge>Pick one</AmbientLarge>
</HeadSub>
)}
</Head>
<Category>
{props.roles.map((role, idx) => (
<Container key={idx}>
<Role
role={role}
selected={props.selectedRoles.includes(role.id)}
onClick={props.onChange(role)}
disabled={role.safety !== RoleSafety.SAFE}
tooltipId={props.category.id}
/>
</Container>
))}
</Category>
<ReactTooltip id={props.category.id} />
</div>
);

View file

@ -0,0 +1 @@
export { PickerCategory } from './PickerCategory';

View file

@ -0,0 +1,17 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "preauth-greeting",
deps = [
"react",
"styled-components",
"//src/design-system/atoms/avatar",
"//src/design-system/atoms/space",
"//src/design-system/atoms/typography",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,13 @@
import * as React from 'react';
import { PreauthGreeting } from './PreauthGreeting';
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
export default {
title: 'Molecules/Preauth/Greeting',
component: PreauthGreeting,
args: {
guildSlug: guild,
},
};
export const Greeting = (args) => <PreauthGreeting {...args} />;

View file

@ -0,0 +1,32 @@
import * as React from 'react';
import { Avatar, utils as avatarUtils } from 'roleypoly/src/design-system/atoms/avatar';
import { Guild } from 'roleypoly/src/design-system/shared-types';
import { AccentTitle } from 'roleypoly/src/design-system/atoms/typography';
import { Space } from 'roleypoly/src/design-system/atoms/space';
import styled from 'styled-components';
type GreetingProps = {
guildSlug: Guild;
};
const Center = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
`;
export const PreauthGreeting = (props: GreetingProps) => (
<Center>
<Avatar size={64} src={props.guildSlug.icon}>
{avatarUtils.initialsFromName(props.guildSlug.name)}
</Avatar>
<AccentTitle>
Hi there. <b>{props.guildSlug.name}</b> uses Roleypoly to help assign you
roles.
</AccentTitle>
<Space />
<Space />
</Center>
);

View file

@ -0,0 +1 @@
export * from './PreauthGreeting';

View file

@ -0,0 +1,29 @@
load("//:hack/react.bzl", "react_library")
load("//:hack/jest.bzl", "jest_test")
package(default_visibility = ["//visibility:public"])
react_library(
name = "preauth-secret-code",
deps = [
"react",
"react-icons",
"styled-components",
"//src/design-system/atoms/button",
"//src/design-system/atoms/fader",
"//src/design-system/atoms/space",
"//src/design-system/atoms/text-input",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)
jest_test(
src = ":preauth-secret-code",
deps = [
"//src/design-system/atoms/button",
"//src/design-system/atoms/fader",
"//src/design-system/atoms/text-input",
],
)

View file

@ -0,0 +1,37 @@
jest.unmock('roleypoly/src/design-system/atoms/text-input');
jest.unmock('./PreauthSecretCode');
import { Button } from 'roleypoly/src/design-system/atoms/button';
import { TextInputWithIcon } from 'roleypoly/src/design-system/atoms/text-input';
import { shallow } from 'enzyme';
import * as React from 'react';
import { PreauthSecretCode } from './PreauthSecretCode';
import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader';
const value = 'unfathomable fishy sticks';
const onSubmit = jest.fn();
it('sends the secret code when submitted', () => {
const view = shallow(<PreauthSecretCode onSubmit={onSubmit} />);
view.find(TextInputWithIcon).simulate('change', { target: { value } });
view.find(Button).simulate('click');
expect(onSubmit).toBeCalledWith(value);
});
it('shows the submit button when secret code is not empty', () => {
const view = shallow(<PreauthSecretCode onSubmit={onSubmit} />);
view.find(TextInputWithIcon).simulate('change', { target: { value } });
expect(view.find(FaderOpacity).props().isVisible).toBe(true);
});
it('hides the submit button when secret code is empty', () => {
const view = shallow(<PreauthSecretCode onSubmit={onSubmit} />);
view.find(TextInputWithIcon).simulate('change', { target: { value: '' } });
expect(view.find(FaderOpacity).props().isVisible).toBe(false);
});

View file

@ -0,0 +1,9 @@
import * as React from 'react';
import { PreauthSecretCode } from './PreauthSecretCode';
export default {
title: 'Molecules/Preauth/Secret Code',
component: PreauthSecretCode,
};
export const SecretCode = (args) => <PreauthSecretCode {...args} />;

View file

@ -0,0 +1,46 @@
import * as React from 'react';
import { TextInputWithIcon } from 'roleypoly/src/design-system/atoms/text-input';
import { FiKey } from 'react-icons/fi';
import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader';
import { Button } from 'roleypoly/src/design-system/atoms/button';
import { Space } from 'roleypoly/src/design-system/atoms/space';
type PreauthProps = {
onSubmit: (code: string) => void;
};
export const PreauthSecretCode = (props: PreauthProps) => {
const [secretCode, setSecretCode] = React.useState('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSecretCode(event.target.value);
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
props.onSubmit(secretCode);
}
};
const handleSubmit = () => {
props.onSubmit(secretCode);
};
return (
<div>
<TextInputWithIcon
icon={<FiKey />}
value={secretCode}
placeholder="Super secret code..."
onChange={handleChange}
onKeyDown={handleKeyPress}
/>
<Space />
<FaderOpacity isVisible={secretCode.length > 0}>
<Button color="muted" onClick={handleSubmit}>
Submit Code
</Button>
</FaderOpacity>
</div>
);
};

View file

@ -0,0 +1 @@
export * from './PreauthSecretCode';

View file

@ -0,0 +1,24 @@
load("//:hack/react.bzl", "react_library")
load("//:hack/jest.bzl", "jest_test")
package(default_visibility = ["//visibility:public"])
react_library(
name = "reset-submit",
deps = [
"react",
"react-icons",
"styled-components",
"//src/design-system/atoms/breakpoints",
"//src/design-system/atoms/button",
"@types/react",
"@types/styled-components",
],
)
jest_test(
src = ":reset-submit",
deps = [
"//src/design-system/atoms/button",
],
)

View file

@ -0,0 +1,23 @@
import { Button } from 'roleypoly/src/design-system/atoms/button';
import { shallow } from 'enzyme';
import * as React from 'react';
import { ResetSubmit } from './ResetSubmit';
const onReset = jest.fn();
const onSubmit = jest.fn();
it('calls onReset when reset is clicked', () => {
const view = shallow(<ResetSubmit onSubmit={onSubmit} onReset={onReset} />);
view.find(Button).at(0).simulate('click');
expect(onReset).toBeCalled();
});
it('calls onSubmit when submit is clicked', () => {
const view = shallow(<ResetSubmit onSubmit={onSubmit} onReset={onReset} />);
view.find(Button).at(1).simulate('click');
expect(onSubmit).toBeCalled();
});

View file

@ -0,0 +1,9 @@
import * as React from 'react';
import { ResetSubmit } from './ResetSubmit';
export default {
title: 'Molecules',
component: ResetSubmit,
};
export const ResetAndSubmit = (args) => <ResetSubmit {...args} />;

View file

@ -0,0 +1,20 @@
import styled from 'styled-components';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
export const Buttons = styled.div`
display: flex;
flex-wrap: wrap;
`;
export const Left = styled.div`
flex: 0;
${onSmallScreen`
flex: 1 1 100%;
order: 2;
`}
`;
export const Right = styled.div`
flex: 1;
`;

View file

@ -0,0 +1,42 @@
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
import { Button } from 'roleypoly/src/design-system/atoms/button';
import * as React from 'react';
import { MdRestore } from 'react-icons/md';
import styled from 'styled-components';
type Props = {
onSubmit: () => void;
onReset: () => void;
};
const Buttons = styled.div`
display: flex;
flex-wrap: wrap;
`;
const Left = styled.div`
flex: 0;
${onSmallScreen`
flex: 1 1 100%;
order: 2;
`}
`;
const Right = styled.div`
flex: 1;
`;
export const ResetSubmit = (props: Props) => {
return (
<Buttons>
<Left>
<Button color="muted" icon={<MdRestore />} onClick={props.onReset}>
Reset
</Button>
</Left>
<Right>
<Button onClick={props.onSubmit}>Submit</Button>
</Right>
</Buttons>
);
};

View file

@ -0,0 +1 @@
export * from './ResetSubmit';

View file

@ -0,0 +1,28 @@
load("//:hack/react.bzl", "react_library")
load("//:hack/jest.bzl", "jest_test")
package(default_visibility = ["//visibility:public"])
react_library(
name = "server-masthead",
deps = [
"next",
"react",
"react-icons",
"styled-components",
"//src/design-system/atoms/avatar",
"//src/design-system/atoms/colors",
"//src/design-system/atoms/timings",
"//src/design-system/atoms/typography",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)
jest_test(
src = ":server-masthead",
deps = [
"//src/design-system/shared-types",
],
)

View file

@ -0,0 +1,19 @@
jest.unmock('./ServerMasthead');
import * as React from 'react';
import { shallow } from 'enzyme';
import { ServerMasthead } from './ServerMasthead';
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
import { Editable } from './ServerMasthead.styled';
it('shows Edit Server when editable is true', () => {
const view = shallow(<ServerMasthead editable={true} guild={guild} />);
expect(view.find(Editable).length).not.toBe(0);
});
it('hides Edit Server when editable is true', () => {
const view = shallow(<ServerMasthead editable={false} guild={guild} />);
expect(view.find(Editable).length).toBe(0);
});

View file

@ -0,0 +1,17 @@
import * as React from 'react';
import { ServerMasthead } from './ServerMasthead';
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
export default {
title: 'Molecules/Server Masthead',
args: {
editable: false,
guild,
},
};
export const Default = (args) => <ServerMasthead {...args} />;
export const Editable = (args) => <ServerMasthead {...args} />;
Editable.args = {
editable: true,
};

View file

@ -0,0 +1,37 @@
import styled from 'styled-components';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
export const Wrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
export const Name = styled.div`
margin: 0 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: flex-start;
`;
export const Icon = styled.div`
flex-shrink: 0;
`;
export const Editable = styled.div`
color: ${palette.taupe500};
display: flex;
align-items: center;
user-select: none;
transition: color ${transitions.actionable}s ease-in-out;
cursor: pointer;
&:hover {
color: ${palette.taupe600};
}
`;

View file

@ -0,0 +1,36 @@
import { Guild } from 'roleypoly/src/design-system/shared-types';
import { Avatar, utils } from 'roleypoly/src/design-system/atoms/avatar';
import { AccentTitle, AmbientLarge } from 'roleypoly/src/design-system/atoms/typography';
import Link from 'next/link';
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
import * as React from 'react';
import { GoPencil } from 'react-icons/go';
import { Editable, Icon, Name, Wrapper } from './ServerMasthead.styled';
export type ServerMastheadProps = {
guild: Guild;
editable: boolean;
};
export const ServerMasthead = (props: ServerMastheadProps) => {
return (
<Wrapper>
<Icon>
<Avatar size={props.editable ? 60 : 48} src={guild.icon}>
{utils.initialsFromName(props.guild.name)}
</Avatar>
</Icon>
<Name>
<AccentTitle>{props.guild.name}</AccentTitle>
{props.editable && (
<Link href="/s/[id]/edit" as={`/s/${props.guild.id}/edit`}>
<Editable role="button">
<GoPencil />
&nbsp; <AmbientLarge>Edit Server</AmbientLarge>
</Editable>
</Link>
)}
</Name>
</Wrapper>
);
};

View file

@ -0,0 +1 @@
export * from './ServerMasthead';

View file

@ -0,0 +1,17 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "user-avatar-group",
deps = [
"react",
"styled-components",
"//src/design-system/atoms/avatar",
"//src/design-system/atoms/breakpoints",
"//src/design-system/atoms/colors",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,19 @@
import * as React from 'react';
import { UserAvatarGroup } from './UserAvatarGroup';
import { user } from 'roleypoly/src/design-system/shared-types/storyData';
import { Hero } from 'roleypoly/src/design-system/atoms/hero';
export default {
title: 'Molecules/User Avatar Group',
component: UserAvatarGroup,
args: {
user,
preventCollapse: true,
},
};
export const Default = (args) => (
<Hero>
<UserAvatarGroup {...args} />
</Hero>
);

View file

@ -0,0 +1,30 @@
import styled, { css } from 'styled-components';
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
export const Collapse = styled.div<{ preventCollapse: boolean }>`
${(props) =>
!props.preventCollapse &&
onSmallScreen(css`
display: none;
`)}
`;
export const Group = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
white-space: nowrap;
`;
export const Discriminator = styled.span`
color: ${palette.taupe500};
font-size: 75%;
padding: 0 5px;
`;
export const GroupText = styled.span`
position: relative;
top: -2px;
`;

View file

@ -0,0 +1,24 @@
import * as React from 'react';
import { DiscordUser } from 'roleypoly/src/design-system/shared-types';
import { utils, Avatar } from 'roleypoly/src/design-system/atoms/avatar';
import { Group, Collapse, Discriminator, GroupText } from './UserAvatarGroup.styled';
type Props = {
user: DiscordUser;
preventCollapse?: boolean;
};
export const UserAvatarGroup = (props: Props) => (
<Group>
<Collapse preventCollapse={props.preventCollapse || false}>
<GroupText>
{props.user.username}
<Discriminator>#{props.user.discriminator}</Discriminator>
</GroupText>
&nbsp;
</Collapse>
<Avatar size={34} src={props.user.avatar}>
{utils.initialsFromName(props.user.username)}
</Avatar>
</Group>
);

View file

@ -0,0 +1 @@
export * from './UserAvatarGroup';

View file

@ -0,0 +1,20 @@
load("//:hack/react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "user-popover",
deps = [
"next",
"react",
"react-icons",
"styled-components",
"//src/design-system/atoms/breakpoints",
"//src/design-system/atoms/colors",
"//src/design-system/atoms/timings",
"//src/design-system/molecules/user-avatar-group",
"//src/design-system/shared-types",
"@types/react",
"@types/styled-components",
],
)

View file

@ -0,0 +1,18 @@
import { user } from 'roleypoly/src/design-system/shared-types/storyData';
import * as React from 'react';
import { UserPopover as UserPopoverComponent } from './UserPopover';
import { PopoverBase } from 'roleypoly/src/design-system/atoms/popover/Popover.styled';
export default {
title: 'Molecules/User Popover',
component: UserPopoverComponent,
args: {
user,
},
};
export const UserPopover = (args) => (
<PopoverBase active>
<UserPopoverComponent {...args} />
</PopoverBase>
);

View file

@ -0,0 +1,34 @@
import styled from 'styled-components';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
export const Base = styled.div`
text-align: right;
display: flex;
flex-direction: column;
user-select: none;
`;
export const NavAction = styled.div`
height: 2.25em;
display: flex;
align-items: center;
justify-content: flex-end;
transition: color ${transitions.actionable}s ease-in-out;
color: ${palette.taupe500};
box-sizing: border-box;
&:hover {
cursor: pointer;
color: ${palette.taupe600};
}
svg {
font-size: 120%;
box-sizing: content-box;
padding: 5px 8px;
position: relative;
top: 0.1em;
}
`;

View file

@ -0,0 +1,30 @@
import * as React from 'react';
import { DiscordUser } from 'roleypoly/src/design-system/shared-types';
import { UserAvatarGroup } from 'roleypoly/src/design-system/molecules/user-avatar-group';
import { Base, NavAction } from './UserPopover.styled';
import { GoGear, GoSignOut } from 'react-icons/go';
import Link from 'next/link';
type UserPopoverProps = {
user: DiscordUser;
};
export const UserPopover = (props: UserPopoverProps) => (
<Base>
<UserAvatarGroup user={props.user} preventCollapse={true} />
<NavAction>
<Link href="/user/settings">
<>
Settings <GoGear />
</>
</Link>
</NavAction>
<NavAction>
<Link href="/auth/machinery/logout">
<>
Log Out <GoSignOut />
</>
</Link>
</NavAction>
</Base>
);

View file

@ -0,0 +1 @@
export * from './UserPopover';

View file

@ -0,0 +1,18 @@
import * as React from 'react';
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
import { AppShell } from './AppShell';
import { rpUser, guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
const story = organismStories('App Shell', module);
story.add('Guest', () => (
<AppShell showFooter user={null}>
<h1>Hello World</h1>
</AppShell>
));
story.add('Logged In', () => (
<AppShell user={rpUser} guildEnumeration={guildEnum}>
<h1>Hello World</h1>
</AppShell>
));

View file

@ -0,0 +1,22 @@
import styled, { createGlobalStyle } from 'styled-components';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
export const Content = styled.div<{ small?: boolean }>`
margin: 0 auto;
margin-top: 50px;
width: ${(props) => (props.small ? '960px' : '1024px')};
max-width: 98vw;
max-height: calc(100vh - 50px);
`;
export const GlobalStyles = createGlobalStyle`
body {
background-color: ${palette.taupe200};
color: ${palette.grey600};
overflow-y: hidden;
scroll-behavior: smooth;
}
* {
box-sizing: border-box;
}
`;

View file

@ -0,0 +1,41 @@
import * as React from 'react';
import * as Masthead from 'roleypoly/src/design-system/organisms/masthead';
import { RoleypolyUser } from '@roleypoly/rpc/shared';
import { Footer } from 'roleypoly/src/design-system/molecules/footer';
import { Content, GlobalStyles } from './AppShell.styled';
import { GlobalStyleColors } from 'roleypoly/src/design-system/atoms/colors';
import { GuildEnumeration } from '@roleypoly/rpc/platform';
import { Scrollbars } from 'react-custom-scrollbars';
type AppShellProps = {
children: React.ReactNode;
user: RoleypolyUser.AsObject | null;
showFooter?: boolean;
small?: boolean;
activeGuildId?: string | null;
guildEnumeration?: GuildEnumeration.AsObject;
};
export const AppShell = (props: AppShellProps) => (
<>
<GlobalStyles />
<GlobalStyleColors />
{props.user !== null ? (
<Masthead.Authed
guildEnumeration={props.guildEnumeration || { guildsList: [] }}
activeGuildId={props.activeGuildId || null}
user={props.user}
/>
) : (
<Masthead.Guest />
)}
<Scrollbars
style={{ height: 'calc(100vh - 25px)', margin: 0, padding: 0 }}
autoHide
universal
>
<Content small={props.small}>{props.children}</Content>
{props.showFooter && <Footer />}
</Scrollbars>
</>
);

View file

@ -0,0 +1 @@
export * from './AppShell';

View file

@ -0,0 +1,16 @@
import * as React from 'react';
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
import { ErrorBanner } from './ErrorBanner';
import { text } from '@storybook/addon-knobs';
const story = organismStories('Error Banner', module);
story.add('Error Banner', () => (
<ErrorBanner
message={{
english: text('English', 'Primary Text'),
japanese: text('Japanese (Subtext)', 'Subtext'),
friendlyCode: text('Friendly Code', 'Oops!'),
}}
/>
));

View file

@ -0,0 +1,40 @@
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import { text300, text500, text700 } from 'roleypoly/src/design-system/atoms/typography';
import styled, { css } from 'styled-components';
export const ErrorWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
${onSmallScreen(css`
display: block;
text-align: center;
`)}
`;
export const ErrorDivider = styled.div`
width: 1px;
height: 3em;
background: ${palette.grey600};
margin: 0 1em;
${onSmallScreen(css`
display: none;
`)}
`;
export const ErrorSideCode = styled.div`
${text700}
${onSmallScreen(css`
margin-bottom: 0.4em;
`)}
`;
export const ErrorText = styled.div`
${text500}
`;
export const ErrorTextLower = styled.div`
${text300}
color: ${palette.taupe500};
`;

View file

@ -0,0 +1,24 @@
import * as React from 'react';
import {
ErrorWrapper,
ErrorDivider,
ErrorSideCode,
ErrorText,
ErrorTextLower,
} from './ErrorBanner.styled';
import { ErrorMessage } from 'templates/errors/errorStrings';
type ErrorBannerProps = {
message: Required<ErrorMessage>;
};
export const ErrorBanner = (props: ErrorBannerProps) => (
<ErrorWrapper>
<ErrorSideCode>{props.message.friendlyCode}</ErrorSideCode>
<ErrorDivider />
<div>
<ErrorText>{props.message.english}</ErrorText>
<ErrorTextLower>{props.message.japanese}</ErrorTextLower>
</div>
</ErrorWrapper>
);

View file

@ -0,0 +1 @@
export * from './ErrorBanner';

View file

@ -0,0 +1,19 @@
import * as React from 'react';
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
import { HelpPageBase } from './HelpPageBase';
import { Content } from 'roleypoly/src/design-system/organisms/app-shell/AppShell.styled';
const baseStory = organismStories('Help Pages', module);
export const HelpStoryWrapper = (props: { children: React.ReactNode }) => (
<Content>
<HelpPageBase>{props.children}</HelpPageBase>
</Content>
);
baseStory.add('Base', () => (
<HelpStoryWrapper>
<h1>What is the world but vibrations?</h1>
<p>Vibrations that synchronize and tie it together, running free forever.</p>
</HelpStoryWrapper>
));

View file

@ -0,0 +1,21 @@
import * as React from 'react';
import styled from 'styled-components';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
export type HelpPageProps = {
children: React.ReactNode;
};
const Container = styled.div`
background: ${palette.taupe300};
padding: 2em 3em;
width: 1024px;
max-width: 98vw;
margin: 0 auto;
margin-top: 75px;
box-sizing: border-box;
`;
export const HelpPageBase = (props: HelpPageProps) => (
<Container>{props.children}</Container>
);

View file

@ -0,0 +1 @@
export * from './HelpPageBase';

View file

@ -0,0 +1,10 @@
import { WhyNoRoles } from './WhyNoRoles';
import * as React from 'react';
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
import { HelpStoryWrapper } from 'roleypoly/src/design-system/organisms/help-page-base/HelpPageBase.story';
organismStories('Help Pages/Pages', module).add('Why No Roles', () => (
<HelpStoryWrapper>
<WhyNoRoles />
</HelpStoryWrapper>
));

View file

@ -0,0 +1,35 @@
import styled, { css } from 'styled-components';
import { palette, numberToChroma } from 'roleypoly/src/design-system/atoms/colors';
import { Role } from '@roleypoly/rpc/shared';
export const DiscordBase = styled.div`
background-color: ${palette.discord100};
border: solid 1px rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding: 10px;
user-select: none;
width: 175px;
`;
const hover = (roleColor: string) => css`
color: #efefef;
background-color: ${roleColor};
cursor: pointer;
`;
export const DiscordRole = styled.div<{
discordRole: Role.AsObject;
isRoleypoly: boolean;
}>`
padding: 6px 10px;
color: ${(props) => numberToChroma(props.discordRole.color).css()};
border-radius: 3px;
:hover {
${(props) => hover(numberToChroma(props.discordRole.color).alpha(0.5).css())}
}
${(props) =>
props.isRoleypoly &&
hover(numberToChroma(props.discordRole.color).alpha(0.5).css())}
`;

View file

@ -0,0 +1,86 @@
import * as React from 'react';
import {
HalfsiesContainer,
HalfsiesItem,
} from 'roleypoly/src/design-system/atoms/halfsies';
import { FaCheck, FaTimes } from 'react-icons/fa';
import { DiscordBase, DiscordRole } from './WhyNoRoles.styled';
import { demoData } from 'roleypoly/src/design-system/shared-types/demoData';
import { Role } from '@roleypoly/rpc/shared';
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import chroma from 'chroma-js';
import { SparkleOverlay } from 'roleypoly/src/design-system/atoms/sparkle';
const adminRoles: Role.AsObject[] = [
{
id: 'roley2',
name: 'Admin',
permissions: 0,
color: chroma('hotpink').num(),
position: -1,
managed: true,
safety: 0,
},
{
id: 'roley3',
name: 'Moderator',
permissions: 0,
color: chroma('lime').num(),
position: -1,
managed: true,
safety: 0,
},
];
const roleypolyRole: Role.AsObject = {
id: 'roley',
name: 'Roleypoly',
permissions: 0,
color: chroma(palette.taupe500).num(),
position: -1,
managed: true,
safety: 0,
};
const goodRoles = [...adminRoles, roleypolyRole, ...demoData];
const badRoles = [...adminRoles, ...demoData, roleypolyRole];
const MaybeWithOverlay = (props: { children: React.ReactNode; withOverlay: boolean }) => {
if (props.withOverlay) {
return (
<SparkleOverlay size={-5} repeatCount={10}>
{props.children}
</SparkleOverlay>
);
} else {
return <>{props.children}</>;
}
};
const Example = (props: { roles: Role.AsObject[]; isGood: boolean }) => (
<div>
<DiscordBase>
{props.roles.map((r) => (
<MaybeWithOverlay withOverlay={props.isGood && r.name === 'Roleypoly'}>
<DiscordRole discordRole={r} isRoleypoly={r.name === 'Roleypoly'}>
{r.name}
</DiscordRole>
</MaybeWithOverlay>
))}
</DiscordBase>
</div>
);
export const WhyNoRoles = () => (
<HalfsiesContainer>
<HalfsiesItem>
<FaCheck /> Good
<Example isGood roles={goodRoles} />
</HalfsiesItem>
<HalfsiesItem>
<FaTimes /> Baddd
<Example isGood={false} roles={badRoles} />
</HalfsiesItem>
</HalfsiesContainer>
);

View file

@ -0,0 +1 @@
export * from './WhyNoRoles';

View file

@ -0,0 +1,7 @@
import * as React from 'react';
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
import { Landing } from './Landing';
const story = organismStories('Landing', module);
story.add('Landing', () => <Landing />);

View file

@ -0,0 +1,31 @@
import { onTablet } from 'roleypoly/src/design-system/atoms/breakpoints';
import { text400 } from 'roleypoly/src/design-system/atoms/typography';
import styled, { css } from 'styled-components';
export const HeroText = styled.div`
${onTablet(css`
text-align: center;
`)}
`;
export const DemoSubtitle = styled.p`
${text400}
text-align: center;
margin-top: 0.4em;
`;
export const DemoAlignment = styled.div`
min-height: 125px;
${onTablet(css`
min-height: 95px;
`)}
`;
export const HeroCentering = styled.div`
display: flex;
flex-direction: column;
min-height: calc(100vh - 200px);
align-items: center;
justify-content: center;
margin-bottom: 2em;
`;

View file

@ -0,0 +1,41 @@
import { palette } from 'roleypoly/src/design-system/atoms/colors';
import { Space } from 'roleypoly/src/design-system/atoms/space';
import { LargeText, LargeTitle } from 'roleypoly/src/design-system/atoms/typography';
import { DemoDiscord } from 'roleypoly/src/design-system/molecules/demo-discord';
import { DemoPicker } from 'roleypoly/src/design-system/molecules/demo-picker';
import * as React from 'react';
import { DemoAlignment, DemoSubtitle, HeroCentering, HeroText } from './Landing.styled';
import {
HalfsiesContainer,
HalfsiesItem,
} from 'roleypoly/src/design-system/atoms/halfsies';
export const Landing = () => (
<HeroCentering>
<HeroText>
<div>
<LargeTitle>Discord roles for humans.</LargeTitle>
</div>
<div style={{ color: palette.taupe500 }}>
<LargeText>
Ditch the bot commands. It's&nbsp;{new Date().getFullYear()}.
</LargeText>
</div>
</HeroText>
<Space />
<HalfsiesContainer>
<HalfsiesItem style={{ marginTop: '2em' }}>
<DemoAlignment>
<DemoDiscord />
</DemoAlignment>
<DemoSubtitle>Why are you okay with antiques?</DemoSubtitle>
</HalfsiesItem>
<HalfsiesItem style={{ marginTop: '2em' }}>
<DemoAlignment>
<DemoPicker />
</DemoAlignment>
<DemoSubtitle>Just click or tap.</DemoSubtitle>
</HalfsiesItem>
</HalfsiesContainer>
</HeroCentering>
);

View file

@ -0,0 +1 @@
export * from './Landing';

View file

@ -0,0 +1,99 @@
import { GuildEnumeration } from '@roleypoly/rpc/platform';
import { RoleypolyUser } from '@roleypoly/rpc/shared';
import { Logomark } from 'roleypoly/src/design-system/atoms/branding';
import { Popover } from 'roleypoly/src/design-system/atoms/popover';
import { guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
import { GuildNav } from 'roleypoly/src/design-system/molecules/guild-nav';
import { NavSlug } from 'roleypoly/src/design-system/molecules/nav-slug';
import { UserAvatarGroup } from 'roleypoly/src/design-system/molecules/user-avatar-group';
import { UserPopover } from 'roleypoly/src/design-system/molecules/user-popover';
import Link from 'next/link';
import * as React from 'react';
import { GoOrganization } from 'react-icons/go';
import {
GuildPopoverHead,
InteractionBase,
MastheadA,
MastheadAlignment,
MastheadBase,
MastheadLeft,
MastheadRight,
} from './Masthead.styled';
type Props = {
user: RoleypolyUser.AsObject;
activeGuildId: string | null;
guildEnumeration: GuildEnumeration.AsObject;
};
export const Authed = (props: Props) => {
const [userPopoverState, setUserPopoverState] = React.useState(false);
const [serverPopoverState, setServerPopoverState] = React.useState(false);
return (
<MastheadBase>
<MastheadAlignment>
<MastheadLeft>
<Link href="/dashboard" passHref>
<MastheadA>
<Logomark height={40} />
</MastheadA>
</Link>
<InteractionBase
onClick={() => {
setServerPopoverState(true);
setUserPopoverState(false);
}}
hide={!serverPopoverState}
>
<NavSlug
guild={
guildEnum.guildsList.find(
(g) => g.id === props.activeGuildId
)?.guild || null
}
/>
</InteractionBase>
<Popover
headContent={
<GuildPopoverHead>
<GoOrganization />
My Guilds
</GuildPopoverHead>
}
canDefocus
position="bottom left"
active={serverPopoverState}
onExit={() => setServerPopoverState(false)}
>
<GuildNav guildEnumeration={props.guildEnumeration} />
</Popover>
</MastheadLeft>
<MastheadRight>
<InteractionBase
onClick={() => {
setUserPopoverState(true);
setServerPopoverState(false);
}}
hide={!userPopoverState}
>
{props.user.discorduser && (
<UserAvatarGroup user={props.user.discorduser} />
)}
</InteractionBase>
<Popover
headContent={<></>}
canDefocus
position="top right"
active={userPopoverState}
onExit={() => setUserPopoverState(false)}
>
{props.user.discorduser && (
<UserPopover user={props.user.discorduser} />
)}
</Popover>
</MastheadRight>
</MastheadAlignment>
</MastheadBase>
);
};

Some files were not shown because too many files have changed in this diff Show more