diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8029f53..0585ce3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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/... diff --git a/.storybook/preview.js b/.storybook/preview.js index 6a4dabd..577de29 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -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, + }, }; diff --git a/.storybook/theme.js b/.storybook/theme.js index 09d1e77..54c6d43 100644 --- a/.storybook/theme.js +++ b/.storybook/theme.js @@ -9,7 +9,7 @@ export const roleypolyTheme = create({ // UI appBg: palette.taupe300, - appContentBg: palette.taupe300, + appContentBg: palette.taupe200, appBorderColor: palette.taupe100, appBorderRadius: 0, diff --git a/hack/fixtures/BUILD.bazel b/hack/fixtures/BUILD.bazel deleted file mode 100644 index e512301..0000000 --- a/hack/fixtures/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/hack/fixtures/storyData.ts b/hack/fixtures/storyData.ts deleted file mode 100644 index 0a029f1..0000000 --- a/hack/fixtures/storyData.ts +++ /dev/null @@ -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, - }, - ], -}; diff --git a/hack/utils.bzl b/hack/utils.bzl index fe91aea..7406b3a 100644 --- a/hack/utils.bzl +++ b/hack/utils.bzl @@ -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) diff --git a/package.json b/package.json index 657ebbe..2ffa56a 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/common/utils/BUILD.bazel b/src/common/utils/BUILD.bazel index b212b02..93ff41d 100644 --- a/src/common/utils/BUILD.bazel +++ b/src/common/utils/BUILD.bazel @@ -19,7 +19,7 @@ react_library( jest_test( src = ":utils", deps = [ - "//hack/fixtures", + "//src/design-system/shared-types", "//src/rpc/shared", ], ) diff --git a/src/common/utils/hasPermission.spec.ts b/src/common/utils/hasPermission.spec.ts index 9c952ed..4af7887 100644 --- a/src/common/utils/hasPermission.spec.ts +++ b/src/common/utils/hasPermission.spec.ts @@ -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[] = [ { diff --git a/src/common/utils/protoReflection.spec.ts b/src/common/utils/protoReflection.spec.ts index 9b7050e..caa1e2b 100644 --- a/src/common/utils/protoReflection.spec.ts +++ b/src/common/utils/protoReflection.spec.ts @@ -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', () => { diff --git a/src/common/utils/protoReflection.ts b/src/common/utils/protoReflection.ts index 2c23b21..02ff735 100644 --- a/src/common/utils/protoReflection.ts +++ b/src/common/utils/protoReflection.ts @@ -1,26 +1,43 @@ import * as pbjs from 'google-protobuf'; +// Protobuf Message itself type GenericObject = T; + +// Message's "setter" call type ProtoFunction> = ( 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 = ( protoClass: { new (): T }, input: ReturnType ): GenericObject => { + // 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)( input[inputKey] ); diff --git a/src/design-system/Intro.stories.mdx b/src/design-system/Intro.stories.mdx new file mode 100644 index 0000000..6c3c409 --- /dev/null +++ b/src/design-system/Intro.stories.mdx @@ -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'; + + + + + + +# 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/ diff --git a/src/design-system/README.md b/src/design-system/README.md index baf6f95..11498d0 100644 --- a/src/design-system/README.md +++ b/src/design-system/README.md @@ -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: diff --git a/src/design-system/atoms/tab-view/BUILD.bazel b/src/design-system/atoms/tab-view/BUILD.bazel index 401854a..f4977ef 100644 --- a/src/design-system/atoms/tab-view/BUILD.bazel +++ b/src/design-system/atoms/tab-view/BUILD.bazel @@ -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", +) diff --git a/src/design-system/atoms/typist/BUILD.bazel b/src/design-system/atoms/typist/BUILD.bazel index 9ca7765..2680a6b 100644 --- a/src/design-system/atoms/typist/BUILD.bazel +++ b/src/design-system/atoms/typist/BUILD.bazel @@ -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", ) diff --git a/src/design-system/atoms/typography/BUILD.bazel b/src/design-system/atoms/typography/BUILD.bazel index a85a703..c1c8044 100644 --- a/src/design-system/atoms/typography/BUILD.bazel +++ b/src/design-system/atoms/typography/BUILD.bazel @@ -6,6 +6,8 @@ react_library( name = "typography", deps = [ "styled-components", + "//src/design-system/atoms/colors", + "//src/design-system/atoms/timings", "@types/styled-components", ], ) diff --git a/src/design-system/atoms/typography/mdx.tsx b/src/design-system/atoms/typography/mdx.tsx new file mode 100644 index 0000000..0a837b6 --- /dev/null +++ b/src/design-system/atoms/typography/mdx.tsx @@ -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, +}; diff --git a/src/design-system/atoms/typography/typography.stories.tsx b/src/design-system/atoms/typography/typography.stories.tsx index 044d50d..a86527a 100644 --- a/src/design-system/atoms/typography/typography.stories.tsx +++ b/src/design-system/atoms/typography/typography.stories.tsx @@ -107,3 +107,12 @@ export const Spacing = () => ( })} ); + +export const Link = () => ( + + Here is a link <3 + +); diff --git a/src/design-system/atoms/typography/typography.tsx b/src/design-system/atoms/typography/typography.tsx index c660a3b..8c9641a 100644 --- a/src/design-system/atoms/typography/typography.tsx +++ b/src/design-system/atoms/typography/typography.tsx @@ -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}; + } +`; diff --git a/src/design-system/molecules/demo-discord/BUILD.bazel b/src/design-system/molecules/demo-discord/BUILD.bazel new file mode 100644 index 0000000..7f9a69d --- /dev/null +++ b/src/design-system/molecules/demo-discord/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/demo-discord/DemoDiscord.stories.tsx b/src/design-system/molecules/demo-discord/DemoDiscord.stories.tsx new file mode 100644 index 0000000..593c352 --- /dev/null +++ b/src/design-system/molecules/demo-discord/DemoDiscord.stories.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { DemoDiscord } from './DemoDiscord'; + +export default { + title: 'Molecules/Role Demos', +}; + +export const Discord = () => ; diff --git a/src/design-system/molecules/demo-discord/DemoDiscord.styled.ts b/src/design-system/molecules/demo-discord/DemoDiscord.styled.ts new file mode 100644 index 0000000..fb1e068 --- /dev/null +++ b/src/design-system/molecules/demo-discord/DemoDiscord.styled.ts @@ -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; +`; diff --git a/src/design-system/molecules/demo-discord/DemoDiscord.tsx b/src/design-system/molecules/demo-discord/DemoDiscord.tsx new file mode 100644 index 0000000..c3b7139 --- /dev/null +++ b/src/design-system/molecules/demo-discord/DemoDiscord.tsx @@ -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 ( + + + {time.getHours() % 12}:{timeString.slice(3, 5)}  + {time.getHours() <= 12 ? 'AM' : 'PM'} + + setEasterEggCount(easterEggCount + 1)}> + okano cat + + + {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!`} + + + +   + `.iam ${role.name}`)} + /> + + + + + ); +}; diff --git a/src/design-system/molecules/demo-discord/index.ts b/src/design-system/molecules/demo-discord/index.ts new file mode 100644 index 0000000..70e62cb --- /dev/null +++ b/src/design-system/molecules/demo-discord/index.ts @@ -0,0 +1 @@ +export * from './DemoDiscord'; diff --git a/src/design-system/molecules/demo-picker/BUILD.bazel b/src/design-system/molecules/demo-picker/BUILD.bazel new file mode 100644 index 0000000..4f825fb --- /dev/null +++ b/src/design-system/molecules/demo-picker/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/demo-picker/DemoPicker.stories.tsx b/src/design-system/molecules/demo-picker/DemoPicker.stories.tsx new file mode 100644 index 0000000..9d1260f --- /dev/null +++ b/src/design-system/molecules/demo-picker/DemoPicker.stories.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { DemoPicker } from './DemoPicker'; + +export default { + title: 'Molecules/Role Demos', +}; + +export const Picker = () => ; diff --git a/src/design-system/molecules/demo-picker/DemoPicker.tsx b/src/design-system/molecules/demo-picker/DemoPicker.tsx new file mode 100644 index 0000000..a4b502a --- /dev/null +++ b/src/design-system/molecules/demo-picker/DemoPicker.tsx @@ -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 ( + + {demoData.map((role) => ( + + { + setSelectedStates({ + ...selectedStates, + [role.id]: !selectedStates[role.id], + }); + }} + /> + + ))} + + ); +}; diff --git a/src/design-system/molecules/demo-picker/index.ts b/src/design-system/molecules/demo-picker/index.ts new file mode 100644 index 0000000..1b9c7d3 --- /dev/null +++ b/src/design-system/molecules/demo-picker/index.ts @@ -0,0 +1 @@ +export * from './DemoPicker'; diff --git a/src/design-system/molecules/footer/BUILD.bazel b/src/design-system/molecules/footer/BUILD.bazel new file mode 100644 index 0000000..fc0a77a --- /dev/null +++ b/src/design-system/molecules/footer/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/footer/Flags.tsx b/src/design-system/molecules/footer/Flags.tsx new file mode 100644 index 0000000..3acfee4 --- /dev/null +++ b/src/design-system/molecules/footer/Flags.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; + +type FlagsProps = { + width?: number | string; + height?: number | string; +}; + +export const Flags = (props: FlagsProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/src/design-system/molecules/footer/Footer.stories.tsx b/src/design-system/molecules/footer/Footer.stories.tsx new file mode 100644 index 0000000..a7904bb --- /dev/null +++ b/src/design-system/molecules/footer/Footer.stories.tsx @@ -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) => ; diff --git a/src/design-system/molecules/footer/Footer.styled.ts b/src/design-system/molecules/footer/Footer.styled.ts new file mode 100644 index 0000000..0bebd9d --- /dev/null +++ b/src/design-system/molecules/footer/Footer.styled.ts @@ -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; + } +`; diff --git a/src/design-system/molecules/footer/Footer.tsx b/src/design-system/molecules/footer/Footer.tsx new file mode 100644 index 0000000..14d520a --- /dev/null +++ b/src/design-system/molecules/footer/Footer.tsx @@ -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 = () => ( + + +
+ © {year} Roleypoly – Made with{' '} + +  in Raleigh, NC +
+
+ Discord/Support –  + Patreon –  + GitHub +
+ + + +
+
+); diff --git a/src/design-system/molecules/footer/index.ts b/src/design-system/molecules/footer/index.ts new file mode 100644 index 0000000..ddcc5a9 --- /dev/null +++ b/src/design-system/molecules/footer/index.ts @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/design-system/molecules/guild-nav/BUILD.bazel b/src/design-system/molecules/guild-nav/BUILD.bazel new file mode 100644 index 0000000..5b067dd --- /dev/null +++ b/src/design-system/molecules/guild-nav/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/guild-nav/GuildNav.stories.tsx b/src/design-system/molecules/guild-nav/GuildNav.stories.tsx new file mode 100644 index 0000000..67dfc67 --- /dev/null +++ b/src/design-system/molecules/guild-nav/GuildNav.stories.tsx @@ -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 = () => ( + + + +); + +export const NoGuilds = () => ( + + + +); diff --git a/src/design-system/molecules/guild-nav/GuildNav.styled.ts b/src/design-system/molecules/guild-nav/GuildNav.styled.ts new file mode 100644 index 0000000..2a56801 --- /dev/null +++ b/src/design-system/molecules/guild-nav/GuildNav.styled.ts @@ -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; + } +`; diff --git a/src/design-system/molecules/guild-nav/GuildNav.tsx b/src/design-system/molecules/guild-nav/GuildNav.tsx new file mode 100644 index 0000000..0c9619b --- /dev/null +++ b/src/design-system/molecules/guild-nav/GuildNav.tsx @@ -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 ; + } + + if (hasPermission(roles, permissions.MANAGE_ROLES)) { + return ; + } + + return null; + }, [props.guild]); +}; + +export const GuildNav = (props: Props) => ( +
+ {sortBy(props.guildEnumeration.guildsList, 'id').map((guild) => ( + + + + + + + ))} + +
+); diff --git a/src/design-system/molecules/guild-nav/index.ts b/src/design-system/molecules/guild-nav/index.ts new file mode 100644 index 0000000..9034be6 --- /dev/null +++ b/src/design-system/molecules/guild-nav/index.ts @@ -0,0 +1 @@ +export * from './GuildNav'; diff --git a/src/design-system/molecules/nav-slug/BUILD.bazel b/src/design-system/molecules/nav-slug/BUILD.bazel new file mode 100644 index 0000000..1b665b5 --- /dev/null +++ b/src/design-system/molecules/nav-slug/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/nav-slug/NavSlug.stories.tsx b/src/design-system/molecules/nav-slug/NavSlug.stories.tsx new file mode 100644 index 0000000..7474f82 --- /dev/null +++ b/src/design-system/molecules/nav-slug/NavSlug.stories.tsx @@ -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 = () => ; +export const Example = () => ; diff --git a/src/design-system/molecules/nav-slug/NavSlug.styled.ts b/src/design-system/molecules/nav-slug/NavSlug.styled.ts new file mode 100644 index 0000000..3d4139f --- /dev/null +++ b/src/design-system/molecules/nav-slug/NavSlug.styled.ts @@ -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; +`; diff --git a/src/design-system/molecules/nav-slug/NavSlug.tsx b/src/design-system/molecules/nav-slug/NavSlug.tsx new file mode 100644 index 0000000..e91782e --- /dev/null +++ b/src/design-system/molecules/nav-slug/NavSlug.tsx @@ -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) => ( + + + {props.guild ? utils.initialsFromName(props.guild.name) : } + + {props.guild?.name || <>Your Guilds} + +); diff --git a/src/design-system/molecules/nav-slug/index.ts b/src/design-system/molecules/nav-slug/index.ts new file mode 100644 index 0000000..e8db90f --- /dev/null +++ b/src/design-system/molecules/nav-slug/index.ts @@ -0,0 +1 @@ +export * from './NavSlug'; diff --git a/src/design-system/molecules/picker-category/BUILD.bazel b/src/design-system/molecules/picker-category/BUILD.bazel new file mode 100644 index 0000000..fa1b608 --- /dev/null +++ b/src/design-system/molecules/picker-category/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/picker-category/PickerCategory.stories.tsx b/src/design-system/molecules/picker-category/PickerCategory.stories.tsx new file mode 100644 index 0000000..db8cb3b --- /dev/null +++ b/src/design-system/molecules/picker-category/PickerCategory.stories.tsx @@ -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 ; +}; +export const Single = (args) => { + return ; +}; +Single.args = { + type: 'single', +}; +export const Multi = (args) => { + return ; +}; +Multi.args = { + type: 'multi', +}; + +export const Wiki = (args) => { + return ; +}; diff --git a/src/design-system/molecules/picker-category/PickerCategory.styled.tsx b/src/design-system/molecules/picker-category/PickerCategory.styled.tsx new file mode 100644 index 0000000..593cee9 --- /dev/null +++ b/src/design-system/molecules/picker-category/PickerCategory.styled.tsx @@ -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; +`; diff --git a/src/design-system/molecules/picker-category/PickerCategory.tsx b/src/design-system/molecules/picker-category/PickerCategory.tsx new file mode 100644 index 0000000..4100399 --- /dev/null +++ b/src/design-system/molecules/picker-category/PickerCategory.tsx @@ -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) => ( +
+ + + {props.title} + + {props.type === 'single' && ( + + Pick one + + )} + + + {props.roles.map((role, idx) => ( + + + + ))} + + +
+); diff --git a/src/design-system/molecules/picker-category/index.ts b/src/design-system/molecules/picker-category/index.ts new file mode 100644 index 0000000..8d74439 --- /dev/null +++ b/src/design-system/molecules/picker-category/index.ts @@ -0,0 +1 @@ +export { PickerCategory } from './PickerCategory'; diff --git a/src/design-system/molecules/preauth-greeting/BUILD.bazel b/src/design-system/molecules/preauth-greeting/BUILD.bazel new file mode 100644 index 0000000..457364a --- /dev/null +++ b/src/design-system/molecules/preauth-greeting/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/preauth-greeting/PreauthGreeting.stories.tsx b/src/design-system/molecules/preauth-greeting/PreauthGreeting.stories.tsx new file mode 100644 index 0000000..de44434 --- /dev/null +++ b/src/design-system/molecules/preauth-greeting/PreauthGreeting.stories.tsx @@ -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) => ; diff --git a/src/design-system/molecules/preauth-greeting/PreauthGreeting.tsx b/src/design-system/molecules/preauth-greeting/PreauthGreeting.tsx new file mode 100644 index 0000000..cd42273 --- /dev/null +++ b/src/design-system/molecules/preauth-greeting/PreauthGreeting.tsx @@ -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) => ( +
+ + {avatarUtils.initialsFromName(props.guildSlug.name)} + + + Hi there. {props.guildSlug.name} uses Roleypoly to help assign you + roles. + + + +
+); diff --git a/src/design-system/molecules/preauth-greeting/index.ts b/src/design-system/molecules/preauth-greeting/index.ts new file mode 100644 index 0000000..7f1a2a7 --- /dev/null +++ b/src/design-system/molecules/preauth-greeting/index.ts @@ -0,0 +1 @@ +export * from './PreauthGreeting'; diff --git a/src/design-system/molecules/preauth-secret-code/BUILD.bazel b/src/design-system/molecules/preauth-secret-code/BUILD.bazel new file mode 100644 index 0000000..57bcd26 --- /dev/null +++ b/src/design-system/molecules/preauth-secret-code/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.spec.tsx b/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.spec.tsx new file mode 100644 index 0000000..4ffab17 --- /dev/null +++ b/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.spec.tsx @@ -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(); + + 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(); + + 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(); + + view.find(TextInputWithIcon).simulate('change', { target: { value: '' } }); + + expect(view.find(FaderOpacity).props().isVisible).toBe(false); +}); diff --git a/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.stories.tsx b/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.stories.tsx new file mode 100644 index 0000000..1049586 --- /dev/null +++ b/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.stories.tsx @@ -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) => ; diff --git a/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.tsx b/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.tsx new file mode 100644 index 0000000..f55bec4 --- /dev/null +++ b/src/design-system/molecules/preauth-secret-code/PreauthSecretCode.tsx @@ -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) => { + setSecretCode(event.target.value); + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + props.onSubmit(secretCode); + } + }; + + const handleSubmit = () => { + props.onSubmit(secretCode); + }; + + return ( +
+ } + value={secretCode} + placeholder="Super secret code..." + onChange={handleChange} + onKeyDown={handleKeyPress} + /> + + 0}> + + +
+ ); +}; diff --git a/src/design-system/molecules/preauth-secret-code/index.ts b/src/design-system/molecules/preauth-secret-code/index.ts new file mode 100644 index 0000000..faee937 --- /dev/null +++ b/src/design-system/molecules/preauth-secret-code/index.ts @@ -0,0 +1 @@ +export * from './PreauthSecretCode'; diff --git a/src/design-system/molecules/reset-submit/BUILD.bazel b/src/design-system/molecules/reset-submit/BUILD.bazel new file mode 100644 index 0000000..b79c744 --- /dev/null +++ b/src/design-system/molecules/reset-submit/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/reset-submit/ResetSubmit.spec.tsx b/src/design-system/molecules/reset-submit/ResetSubmit.spec.tsx new file mode 100644 index 0000000..5129df2 --- /dev/null +++ b/src/design-system/molecules/reset-submit/ResetSubmit.spec.tsx @@ -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(); + + view.find(Button).at(0).simulate('click'); + + expect(onReset).toBeCalled(); +}); + +it('calls onSubmit when submit is clicked', () => { + const view = shallow(); + + view.find(Button).at(1).simulate('click'); + + expect(onSubmit).toBeCalled(); +}); diff --git a/src/design-system/molecules/reset-submit/ResetSubmit.stories.tsx b/src/design-system/molecules/reset-submit/ResetSubmit.stories.tsx new file mode 100644 index 0000000..85825f6 --- /dev/null +++ b/src/design-system/molecules/reset-submit/ResetSubmit.stories.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { ResetSubmit } from './ResetSubmit'; + +export default { + title: 'Molecules', + component: ResetSubmit, +}; + +export const ResetAndSubmit = (args) => ; diff --git a/src/design-system/molecules/reset-submit/ResetSubmit.styled.ts b/src/design-system/molecules/reset-submit/ResetSubmit.styled.ts new file mode 100644 index 0000000..e7c622a --- /dev/null +++ b/src/design-system/molecules/reset-submit/ResetSubmit.styled.ts @@ -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; +`; diff --git a/src/design-system/molecules/reset-submit/ResetSubmit.tsx b/src/design-system/molecules/reset-submit/ResetSubmit.tsx new file mode 100644 index 0000000..7d2499c --- /dev/null +++ b/src/design-system/molecules/reset-submit/ResetSubmit.tsx @@ -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 ( + + + + + + + + + ); +}; diff --git a/src/design-system/molecules/reset-submit/index.ts b/src/design-system/molecules/reset-submit/index.ts new file mode 100644 index 0000000..d175a14 --- /dev/null +++ b/src/design-system/molecules/reset-submit/index.ts @@ -0,0 +1 @@ +export * from './ResetSubmit'; diff --git a/src/design-system/molecules/server-masthead/BUILD.bazel b/src/design-system/molecules/server-masthead/BUILD.bazel new file mode 100644 index 0000000..2e2e5ca --- /dev/null +++ b/src/design-system/molecules/server-masthead/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/server-masthead/ServerMasthead.spec.tsx b/src/design-system/molecules/server-masthead/ServerMasthead.spec.tsx new file mode 100644 index 0000000..961c782 --- /dev/null +++ b/src/design-system/molecules/server-masthead/ServerMasthead.spec.tsx @@ -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(); + + expect(view.find(Editable).length).not.toBe(0); +}); + +it('hides Edit Server when editable is true', () => { + const view = shallow(); + + expect(view.find(Editable).length).toBe(0); +}); diff --git a/src/design-system/molecules/server-masthead/ServerMasthead.stories.tsx b/src/design-system/molecules/server-masthead/ServerMasthead.stories.tsx new file mode 100644 index 0000000..e0cce2a --- /dev/null +++ b/src/design-system/molecules/server-masthead/ServerMasthead.stories.tsx @@ -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) => ; +export const Editable = (args) => ; +Editable.args = { + editable: true, +}; diff --git a/src/design-system/molecules/server-masthead/ServerMasthead.styled.ts b/src/design-system/molecules/server-masthead/ServerMasthead.styled.ts new file mode 100644 index 0000000..43f8c47 --- /dev/null +++ b/src/design-system/molecules/server-masthead/ServerMasthead.styled.ts @@ -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}; + } +`; diff --git a/src/design-system/molecules/server-masthead/ServerMasthead.tsx b/src/design-system/molecules/server-masthead/ServerMasthead.tsx new file mode 100644 index 0000000..3afdb4d --- /dev/null +++ b/src/design-system/molecules/server-masthead/ServerMasthead.tsx @@ -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 ( + + + + {utils.initialsFromName(props.guild.name)} + + + + {props.guild.name} + {props.editable && ( + + + +   Edit Server + + + )} + + + ); +}; diff --git a/src/design-system/molecules/server-masthead/index.ts b/src/design-system/molecules/server-masthead/index.ts new file mode 100644 index 0000000..a206ba4 --- /dev/null +++ b/src/design-system/molecules/server-masthead/index.ts @@ -0,0 +1 @@ +export * from './ServerMasthead'; diff --git a/src/design-system/molecules/user-avatar-group/BUILD.bazel b/src/design-system/molecules/user-avatar-group/BUILD.bazel new file mode 100644 index 0000000..e13e4b6 --- /dev/null +++ b/src/design-system/molecules/user-avatar-group/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx b/src/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx new file mode 100644 index 0000000..995a974 --- /dev/null +++ b/src/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx @@ -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) => ( + + + +); diff --git a/src/design-system/molecules/user-avatar-group/UserAvatarGroup.styled.ts b/src/design-system/molecules/user-avatar-group/UserAvatarGroup.styled.ts new file mode 100644 index 0000000..1b4c29c --- /dev/null +++ b/src/design-system/molecules/user-avatar-group/UserAvatarGroup.styled.ts @@ -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; +`; diff --git a/src/design-system/molecules/user-avatar-group/UserAvatarGroup.tsx b/src/design-system/molecules/user-avatar-group/UserAvatarGroup.tsx new file mode 100644 index 0000000..686b1a8 --- /dev/null +++ b/src/design-system/molecules/user-avatar-group/UserAvatarGroup.tsx @@ -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) => ( + + + + {props.user.username} + #{props.user.discriminator} + +   + + + {utils.initialsFromName(props.user.username)} + + +); diff --git a/src/design-system/molecules/user-avatar-group/index.ts b/src/design-system/molecules/user-avatar-group/index.ts new file mode 100644 index 0000000..52cf06c --- /dev/null +++ b/src/design-system/molecules/user-avatar-group/index.ts @@ -0,0 +1 @@ +export * from './UserAvatarGroup'; diff --git a/src/design-system/molecules/user-popover/BUILD.bazel b/src/design-system/molecules/user-popover/BUILD.bazel new file mode 100644 index 0000000..05880f7 --- /dev/null +++ b/src/design-system/molecules/user-popover/BUILD.bazel @@ -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", + ], +) diff --git a/src/design-system/molecules/user-popover/UserPopover.stories.tsx b/src/design-system/molecules/user-popover/UserPopover.stories.tsx new file mode 100644 index 0000000..6358ac4 --- /dev/null +++ b/src/design-system/molecules/user-popover/UserPopover.stories.tsx @@ -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) => ( + + + +); diff --git a/src/design-system/molecules/user-popover/UserPopover.styled.ts b/src/design-system/molecules/user-popover/UserPopover.styled.ts new file mode 100644 index 0000000..d0b82cf --- /dev/null +++ b/src/design-system/molecules/user-popover/UserPopover.styled.ts @@ -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; + } +`; diff --git a/src/design-system/molecules/user-popover/UserPopover.tsx b/src/design-system/molecules/user-popover/UserPopover.tsx new file mode 100644 index 0000000..404da96 --- /dev/null +++ b/src/design-system/molecules/user-popover/UserPopover.tsx @@ -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) => ( + + + + + <> + Settings + + + + + + <> + Log Out + + + + +); diff --git a/src/design-system/molecules/user-popover/index.ts b/src/design-system/molecules/user-popover/index.ts new file mode 100644 index 0000000..2cf58d5 --- /dev/null +++ b/src/design-system/molecules/user-popover/index.ts @@ -0,0 +1 @@ +export * from './UserPopover'; diff --git a/src/design-system/organisms/app-shell/AppShell.story.tsx b/src/design-system/organisms/app-shell/AppShell.story.tsx new file mode 100644 index 0000000..7c21dd2 --- /dev/null +++ b/src/design-system/organisms/app-shell/AppShell.story.tsx @@ -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', () => ( + +

Hello World

+
+)); + +story.add('Logged In', () => ( + +

Hello World

+
+)); diff --git a/src/design-system/organisms/app-shell/AppShell.styled.tsx b/src/design-system/organisms/app-shell/AppShell.styled.tsx new file mode 100644 index 0000000..97f9785 --- /dev/null +++ b/src/design-system/organisms/app-shell/AppShell.styled.tsx @@ -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; + } +`; diff --git a/src/design-system/organisms/app-shell/AppShell.tsx b/src/design-system/organisms/app-shell/AppShell.tsx new file mode 100644 index 0000000..dadb8f5 --- /dev/null +++ b/src/design-system/organisms/app-shell/AppShell.tsx @@ -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) => ( + <> + + + {props.user !== null ? ( + + ) : ( + + )} + + {props.children} + {props.showFooter &&