diff --git a/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx b/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx index 4047561..7f6618c 100644 --- a/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx +++ b/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx @@ -7,3 +7,4 @@ export default { export const Dark = () => ; export const Light = () => ; +export const Skeleton = () => ; diff --git a/packages/design-system/atoms/dot-overlay/DotOverlay.tsx b/packages/design-system/atoms/dot-overlay/DotOverlay.tsx index 1a84233..f503145 100644 --- a/packages/design-system/atoms/dot-overlay/DotOverlay.tsx +++ b/packages/design-system/atoms/dot-overlay/DotOverlay.tsx @@ -1,3 +1,4 @@ +import { animateOpacity } from '@roleypoly/design-system/atoms/placeholder'; import * as React from 'react'; import styled from 'styled-components'; @@ -33,6 +34,22 @@ const DotOverlayLight = styled(dotOverlayBase)` ); `; -export const DotOverlay = ({ light }: { light?: boolean }) => { - return light ? : ; +const DotOverlaySkeleton = styled(DotOverlayDark)` + ${animateOpacity} +`; + +export const DotOverlay = ({ + light, + skeleton, +}: { + light?: boolean; + skeleton?: boolean; +}) => { + return skeleton ? ( + + ) : light ? ( + + ) : ( + + ); }; diff --git a/packages/design-system/atoms/loading-text/Loading.stories.tsx b/packages/design-system/atoms/loading-text/Loading.stories.tsx new file mode 100644 index 0000000..8d776a3 --- /dev/null +++ b/packages/design-system/atoms/loading-text/Loading.stories.tsx @@ -0,0 +1,12 @@ +import { Hero } from '@roleypoly/design-system/atoms/hero'; +import { LoadingFill } from './Loading'; +export default { + title: 'Atoms/Loading Text', + component: LoadingFill, +}; + +export const loading = (args) => ( + + + +); diff --git a/packages/design-system/atoms/loading-text/Loading.tsx b/packages/design-system/atoms/loading-text/Loading.tsx new file mode 100644 index 0000000..ec93eb0 --- /dev/null +++ b/packages/design-system/atoms/loading-text/Loading.tsx @@ -0,0 +1,30 @@ +const loadingTexts = [ + 'Loading Roleypoly...', + 'Reticulating splines...', + 'Mining cryptocoins...', + 'Going to Mars...', + 'Building the Box Signature Edition...', + 'Hiring a new CEO...', + 'Firing the new CEO...', + 'Doing a calculation...', + 'Doin a fixy boi...', + 'Feeling like a plastic bag...', + 'Levelling up...', + 'Your Roleypoly is evolving!!!', + 'Adding subtitles...', + 'Rolling... Rolling...', +]; + +export const LoadingFill = (props: { forceIndex?: keyof typeof loadingTexts }) => { + const useEasterEgg = Math.floor(Math.random() * 10) === 0; + const index = + props.forceIndex !== undefined + ? props.forceIndex + : useEasterEgg + ? Math.floor(Math.random() * loadingTexts.length) + : 0; + + const text = loadingTexts[index]; + + return <>{text}; +}; diff --git a/packages/design-system/atoms/loading-text/index.ts b/packages/design-system/atoms/loading-text/index.ts new file mode 100644 index 0000000..bc5115b --- /dev/null +++ b/packages/design-system/atoms/loading-text/index.ts @@ -0,0 +1 @@ +export * from './Loading'; diff --git a/packages/design-system/atoms/placeholder/Placeholder.stories.tsx b/packages/design-system/atoms/placeholder/Placeholder.stories.tsx new file mode 100644 index 0000000..2fc770b --- /dev/null +++ b/packages/design-system/atoms/placeholder/Placeholder.stories.tsx @@ -0,0 +1,13 @@ +import { palette } from '../colors'; +import { PlaceholderBox } from './Placeholder'; + +export default { + title: 'Atoms/Placeholder', + component: PlaceholderBox, + args: { + firstColor: palette.taupe100, + secondColor: palette.taupe300, + }, +}; + +export const placeholderBox = (args) => ; diff --git a/packages/design-system/atoms/placeholder/Placeholder.tsx b/packages/design-system/atoms/placeholder/Placeholder.tsx new file mode 100644 index 0000000..3663132 --- /dev/null +++ b/packages/design-system/atoms/placeholder/Placeholder.tsx @@ -0,0 +1,55 @@ +import styled, { css, keyframes } from 'styled-components'; +import { palette } from '../colors'; + +export const fadeInOut = keyframes` + from { + background-color: var(--placeholder-first-color); + } + to { + background-color: var(--placeholder-second-color); + } +`; + +export const animateFade = (firstColor?: string, secondColor?: string) => css` + @media (prefers-reduced-motion: no-preference) { + animation: ${fadeInOut} 2s ease-in-out infinite alternate; + } + + --placeholder-first-color: ${firstColor}; + --placeholder-second-color: ${secondColor}; +`; + +type PlaceholderProps = { + forceReduceMotion?: boolean; + firstColor?: string; + secondColor?: string; +}; + +export const PlaceholderBox = styled.div` + width: 7em; + height: 1em; + background-color: ${(props) => props.firstColor || palette.taupe200}; + display: inline-block; + border-radius: 2px; + position: relative; + top: 0.2em; + ${(props) => + props.secondColor && + !props.forceReduceMotion && + animateFade(props.firstColor, props.secondColor)} +`; + +export const opacityInOut = keyframes` + from { + opacity: 0.6; + } + to { + opacity: 0.3; + } +`; + +export const animateOpacity = css` + @media (prefers-reduced-motion: no-preference) { + animation: ${opacityInOut} 5s ease-in-out infinite alternate; + } +`; diff --git a/packages/design-system/atoms/placeholder/index.ts b/packages/design-system/atoms/placeholder/index.ts new file mode 100644 index 0000000..5c37725 --- /dev/null +++ b/packages/design-system/atoms/placeholder/index.ts @@ -0,0 +1 @@ +export * from './Placeholder'; diff --git a/packages/design-system/atoms/spinner/Spinner.stories.tsx b/packages/design-system/atoms/spinner/Spinner.stories.tsx new file mode 100644 index 0000000..530bf52 --- /dev/null +++ b/packages/design-system/atoms/spinner/Spinner.stories.tsx @@ -0,0 +1,13 @@ +import { Hero } from '@roleypoly/design-system/atoms/hero'; +import { Spinner } from './Spinner'; + +export default { + title: 'Atoms/Spinner', + component: Spinner, +}; + +export const spinner = (args) => ( + + + +); diff --git a/packages/design-system/atoms/spinner/Spinner.tsx b/packages/design-system/atoms/spinner/Spinner.tsx new file mode 100644 index 0000000..3799c97 --- /dev/null +++ b/packages/design-system/atoms/spinner/Spinner.tsx @@ -0,0 +1,78 @@ +import styled, { css, keyframes } from 'styled-components'; +import { palette } from '../colors'; + +type SpinnerProps = { + size?: number; + reverse?: boolean; + color?: string; + speed?: number; +}; + +const spinnerKeyframes = keyframes` + 0%, 100% { + border-width: 0.5px; + border-right-width: 3px; + border-left-width: 0; + } + 25% { + border-width: 0.5px; + border-top-width: 3px; + border-bottom-width: 0; + } + 50% { + border-width: 0.5px; + border-left-width: 3px; + border-right-width: 0; + } + 75% { + border-width: 0.5px; + border-bottom-width: 3px; + border-top-width: 0; + } +`; + +const SpinnerStyled = styled.div>` + @media (prefers-reduced-motion: no-preference) { + animation: ${spinnerKeyframes} ${(props) => props.speed}s linear infinite + ${(props) => (props.reverse ? `reverse` : '')}; + transform: rotateZ(0); + } + + border: 0.5px solid ${(props) => props.color}; + box-sizing: border-box; + width: ${(props) => props.size}px; + height: ${(props) => props.size}px; + border-radius: ${(props) => props.size * 0.7}px; + display: flex; + align-items: center; + justify-content: center; + + ${(props) => + props.reverse + ? css` + border-right-width: 0; + border-left-width: 3px; + ` + : css` + border-right-width: 3px; + border-left-width: 0; + `} +`; + +export const Spinner = ( + props: SpinnerProps & { innerColor?: string; outerColor?: string } +) => ( + + + +); diff --git a/packages/design-system/atoms/spinner/index.ts b/packages/design-system/atoms/spinner/index.ts new file mode 100644 index 0000000..b259397 --- /dev/null +++ b/packages/design-system/atoms/spinner/index.ts @@ -0,0 +1 @@ +export * from './Spinner'; diff --git a/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx b/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx index bf60186..b81d941 100644 --- a/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx +++ b/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx @@ -2,6 +2,7 @@ import { Hero } from '@roleypoly/design-system/atoms/hero'; import * as React from 'react'; import { user } from '../../fixtures/storyData'; import { UserAvatarGroup } from './UserAvatarGroup'; +import { UserAvatarGroupSkeleton } from './UserAvatarGroupSkeleton'; export default { title: 'Molecules/User Avatar Group', @@ -17,3 +18,9 @@ export const Default = (args) => ( ); + +export const skeleton = () => ( + + + +); diff --git a/packages/design-system/molecules/user-avatar-group/UserAvatarGroupSkeleton.tsx b/packages/design-system/molecules/user-avatar-group/UserAvatarGroupSkeleton.tsx new file mode 100644 index 0000000..b6a21aa --- /dev/null +++ b/packages/design-system/molecules/user-avatar-group/UserAvatarGroupSkeleton.tsx @@ -0,0 +1,17 @@ +import { Avatar } from '@roleypoly/design-system/atoms/avatar'; +import { palette } from '@roleypoly/design-system/atoms/colors'; +import { PlaceholderBox } from '@roleypoly/design-system/atoms/placeholder'; +import * as React from 'react'; +import { Collapse, Group, GroupText } from './UserAvatarGroup.styled'; + +export const UserAvatarGroupSkeleton = () => ( + + + + + +   + + + +); diff --git a/packages/design-system/molecules/user-avatar-group/index.ts b/packages/design-system/molecules/user-avatar-group/index.ts index 52cf06c..1e1e2b2 100644 --- a/packages/design-system/molecules/user-avatar-group/index.ts +++ b/packages/design-system/molecules/user-avatar-group/index.ts @@ -1 +1,2 @@ export * from './UserAvatarGroup'; +export * from './UserAvatarGroupSkeleton'; diff --git a/packages/design-system/organisms/app-shell/AppShell.tsx b/packages/design-system/organisms/app-shell/AppShell.tsx index ab8b771..2e348cd 100644 --- a/packages/design-system/organisms/app-shell/AppShell.tsx +++ b/packages/design-system/organisms/app-shell/AppShell.tsx @@ -15,13 +15,35 @@ export type AppShellProps = { guilds?: GuildSlug[]; recentGuilds?: string[]; disableGuildPicker?: boolean; + skeleton?: boolean; +}; + +const OptionallyScroll = (props: { + shouldScroll: boolean; + children: React.ReactNode; +}) => { + if (props.shouldScroll) { + return ( + + {props.children} + + ); + } + + return <>{props.children}; }; export const AppShell = (props: AppShellProps) => ( <> - {props.user ? ( + {props.skeleton ? ( + + ) : props.user ? ( ( ) : ( )} - - {props.children} - {props.showFooter &&