Browse Source

merge dev branch

merge dev branch
Klaudiusz Dembler 3 years ago
parent
commit
d5ed49974a
43 changed files with 687 additions and 121 deletions
  1. 3 0
      .prettierignore
  2. 1 0
      package.json
  3. 3 3
      src/api/queries/__generated__/baseTypes.generated.ts
  4. 1 1
      src/api/queries/__generated__/channels.generated.tsx
  5. 6 6
      src/api/queries/__generated__/videos.generated.tsx
  6. 1 1
      src/api/queries/channels.graphql
  7. 4 4
      src/api/queries/videos.graphql
  8. 3 3
      src/api/schemas/extendedQueryNode.graphql
  9. 1 0
      src/mocking/data/index.ts
  10. 13 0
      src/mocking/data/mockWorkers.ts
  11. 3 1
      src/mocking/handlers.ts
  12. 1 1
      src/providers/assets/useAsset.tsx
  13. 0 0
      src/shared/assets/animations/error.json
  14. 0 0
      src/shared/assets/animations/loader-L.json
  15. 0 0
      src/shared/assets/animations/loader-M.json
  16. 1 0
      src/shared/assets/animations/loader-S.json
  17. 1 0
      src/shared/assets/animations/loader-XS.json
  18. 0 0
      src/shared/assets/animations/loader-player.json
  19. 12 0
      src/shared/components/AnimatedError/AnimatedError.tsx
  20. 1 0
      src/shared/components/AnimatedError/index.ts
  21. 63 0
      src/shared/components/Banner/Banner.style.ts
  22. 67 0
      src/shared/components/Banner/Banner.tsx
  23. 1 0
      src/shared/components/Banner/index.ts
  24. 100 1
      src/shared/components/Button/Button.stories.tsx
  25. 28 8
      src/shared/components/Button/Button.style.ts
  26. 28 6
      src/shared/components/Button/Button.tsx
  27. 162 8
      src/shared/components/ButtonBase/ButtonBase.style.ts
  28. 21 3
      src/shared/components/ButtonBase/ButtonBase.tsx
  29. 9 0
      src/shared/components/DismissibleMessage/DismissibleMessage.stories.tsx
  30. 0 38
      src/shared/components/DismissibleMessage/DismissibleMessage.style.ts
  31. 8 33
      src/shared/components/DismissibleMessage/DismissibleMessage.tsx
  32. 16 0
      src/shared/components/IconButton/IconButton.stories.tsx
  33. 1 1
      src/shared/components/IconButton/IconButton.tsx
  34. 40 0
      src/shared/components/Loader/Loader.tsx
  35. 1 0
      src/shared/components/Loader/index.ts
  36. 3 0
      src/shared/components/index.ts
  37. 30 2
      src/shared/theme/colors.ts
  38. 3 0
      src/views/playground/PlaygroundLayout.tsx
  39. 26 0
      src/views/playground/Playgrounds/Animations.tsx
  40. 1 0
      src/views/playground/Playgrounds/index.ts
  41. 1 1
      src/views/studio/EditVideoSheet/EditVideoForm/EditVideoForm.tsx
  42. 9 0
      src/views/studio/MyVideosView/MyVideosView.tsx
  43. 14 0
      yarn.lock

+ 3 - 0
.prettierignore

@@ -7,3 +7,6 @@ tsconfig.json
 public/mockServiceWorker.js
 .history
 docs/
+
+# don't prettify minified animation JSONs
+src/shared/assets/animations

+ 1 - 0
package.json

@@ -81,6 +81,7 @@
     "react-dropzone": "^11.3.1",
     "react-hook-form": "^7.8.1",
     "react-intersection-observer": "^8.31.0",
+    "react-lottie-player": "^1.3.2",
     "react-number-format": "^4.4.4",
     "react-router": "^6.0.0-beta.0",
     "react-router-dom": "^6.0.0-beta.0",

+ 3 - 3
src/api/queries/__generated__/baseTypes.generated.ts

@@ -318,7 +318,7 @@ export type QueryChannelsConnectionArgs = {
   first?: Maybe<Scalars['Int']>
   after?: Maybe<Scalars['String']>
   where?: Maybe<ChannelWhereInput>
-  orderBy?: Maybe<ChannelOrderByInput>
+  orderBy?: Maybe<Array<ChannelOrderByInput>>
 }
 
 export type QueryMembershipByUniqueInputArgs = {
@@ -348,14 +348,14 @@ export type QueryVideosArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
   where?: Maybe<VideoWhereInput>
-  orderBy?: Maybe<VideoOrderByInput>
+  orderBy?: Maybe<Array<VideoOrderByInput>>
 }
 
 export type QueryVideosConnectionArgs = {
   first?: Maybe<Scalars['Int']>
   after?: Maybe<Scalars['String']>
   where?: Maybe<VideoWhereInput>
-  orderBy?: Maybe<VideoOrderByInput>
+  orderBy?: Maybe<Array<VideoOrderByInput>>
 }
 
 export type QueryWorkerByUniqueInputArgs = {

+ 1 - 1
src/api/queries/__generated__/channels.generated.tsx

@@ -293,7 +293,7 @@ export type GetChannelsLazyQueryHookResult = ReturnType<typeof useGetChannelsLaz
 export type GetChannelsQueryResult = Apollo.QueryResult<GetChannelsQuery, GetChannelsQueryVariables>
 export const GetChannelsConnectionDocument = gql`
   query GetChannelsConnection($first: Int, $after: String, $where: ChannelWhereInput) {
-    channelsConnection(first: $first, after: $after, where: $where, orderBy: createdAt_DESC) {
+    channelsConnection(first: $first, after: $after, where: $where, orderBy: [createdAt_DESC]) {
       edges {
         cursor
         node {

+ 6 - 6
src/api/queries/__generated__/videos.generated.tsx

@@ -61,7 +61,7 @@ export type GetVideoQuery = {
 export type GetVideosConnectionQueryVariables = Types.Exact<{
   first?: Types.Maybe<Types.Scalars['Int']>
   after?: Types.Maybe<Types.Scalars['String']>
-  orderBy?: Types.Maybe<Types.VideoOrderByInput>
+  orderBy?: Types.VideoOrderByInput
   where?: Types.Maybe<Types.VideoWhereInput>
 }>
 
@@ -79,7 +79,7 @@ export type GetVideosQueryVariables = Types.Exact<{
   offset?: Types.Maybe<Types.Scalars['Int']>
   limit?: Types.Maybe<Types.Scalars['Int']>
   where?: Types.Maybe<Types.VideoWhereInput>
-  orderBy?: Types.Maybe<Types.VideoOrderByInput>
+  orderBy?: Types.VideoOrderByInput
 }>
 
 export type GetVideosQuery = {
@@ -213,10 +213,10 @@ export const GetVideosConnectionDocument = gql`
   query GetVideosConnection(
     $first: Int
     $after: String
-    $orderBy: VideoOrderByInput = createdAt_DESC
+    $orderBy: VideoOrderByInput! = createdAt_DESC
     $where: VideoWhereInput
   ) {
-    videosConnection(first: $first, after: $after, where: $where, orderBy: $orderBy) {
+    videosConnection(first: $first, after: $after, where: $where, orderBy: [$orderBy]) {
       edges {
         cursor
         node {
@@ -275,8 +275,8 @@ export type GetVideosConnectionQueryResult = Apollo.QueryResult<
   GetVideosConnectionQueryVariables
 >
 export const GetVideosDocument = gql`
-  query GetVideos($offset: Int, $limit: Int, $where: VideoWhereInput, $orderBy: VideoOrderByInput = createdAt_DESC) {
-    videos(offset: $offset, limit: $limit, where: $where, orderBy: $orderBy) {
+  query GetVideos($offset: Int, $limit: Int, $where: VideoWhereInput, $orderBy: VideoOrderByInput! = createdAt_DESC) {
+    videos(offset: $offset, limit: $limit, where: $where, orderBy: [$orderBy]) {
       ...VideoFields
     }
   }

+ 1 - 1
src/api/queries/channels.graphql

@@ -52,7 +52,7 @@ query GetChannels($where: ChannelWhereInput) {
 }
 
 query GetChannelsConnection($first: Int, $after: String, $where: ChannelWhereInput) {
-  channelsConnection(first: $first, after: $after, where: $where, orderBy: createdAt_DESC) {
+  channelsConnection(first: $first, after: $after, where: $where, orderBy: [createdAt_DESC]) {
     edges {
       cursor
       node {

+ 4 - 4
src/api/queries/videos.graphql

@@ -60,10 +60,10 @@ query GetVideo($where: VideoWhereUniqueInput!) {
 query GetVideosConnection(
   $first: Int
   $after: String
-  $orderBy: VideoOrderByInput = createdAt_DESC
+  $orderBy: VideoOrderByInput! = createdAt_DESC
   $where: VideoWhereInput
 ) {
-  videosConnection(first: $first, after: $after, where: $where, orderBy: $orderBy) {
+  videosConnection(first: $first, after: $after, where: $where, orderBy: [$orderBy]) {
     edges {
       cursor
       node {
@@ -78,8 +78,8 @@ query GetVideosConnection(
   }
 }
 
-query GetVideos($offset: Int, $limit: Int, $where: VideoWhereInput, $orderBy: VideoOrderByInput = createdAt_DESC) {
-  videos(offset: $offset, limit: $limit, where: $where, orderBy: $orderBy) {
+query GetVideos($offset: Int, $limit: Int, $where: VideoWhereInput, $orderBy: VideoOrderByInput! = createdAt_DESC) {
+  videos(offset: $offset, limit: $limit, where: $where, orderBy: [$orderBy]) {
     ...VideoFields
   }
 }

+ 3 - 3
src/api/schemas/extendedQueryNode.graphql

@@ -273,17 +273,17 @@ type Query {
     first: Int
     after: String
     where: ChannelWhereInput
-    orderBy: ChannelOrderByInput
+    orderBy: [ChannelOrderByInput!]
   ): ChannelConnection!
 
   # Lookup video by its ID
   videoByUniqueInput(where: VideoWhereUniqueInput!): Video
 
   # Lookup videos by where params
-  videos(offset: Int, limit: Int, where: VideoWhereInput, orderBy: VideoOrderByInput): [Video!]
+  videos(offset: Int, limit: Int, where: VideoWhereInput, orderBy: [VideoOrderByInput!]): [Video!]
 
   # List all videos by given constraints
-  videosConnection(first: Int, after: String, where: VideoWhereInput, orderBy: VideoOrderByInput): VideoConnection!
+  videosConnection(first: Int, after: String, where: VideoWhereInput, orderBy: [VideoOrderByInput!]): VideoConnection!
 
   # List all categories
   videoCategories: [VideoCategory!]!

+ 1 - 0
src/mocking/data/index.ts

@@ -4,3 +4,4 @@ export { default as mockChannels } from './mockChannels'
 export { default as mockCategories } from './mockCategories'
 export { default as mockLicenses } from './mockLicenses'
 export { default as mockMemberships } from './mockMemberships'
+export * from './mockWorkers'

+ 13 - 0
src/mocking/data/mockWorkers.ts

@@ -0,0 +1,13 @@
+import { WorkerType } from '@/api/queries'
+import { BasicWorkerFieldsFragment } from '@/api/queries/__generated__/workers.generated'
+
+export const mockWorkers: BasicWorkerFieldsFragment[] = [
+  {
+    __typename: 'Worker',
+    id: 'mock_worker',
+    workerId: '123',
+    metadata: 'http://google.com/storage',
+    type: WorkerType.Storage,
+    isActive: true,
+  },
+]

+ 3 - 1
src/mocking/handlers.ts

@@ -45,8 +45,9 @@ import {
   SearchQuery,
   SearchQueryVariables,
 } from '@/api/queries'
+import { GetWorkersDocument, GetWorkersQuery } from '@/api/queries/__generated__/workers.generated'
 import { ORION_GRAPHQL_URL, QUERY_NODE_GRAPHQL_URL } from '@/config/urls'
-import { mockCategories, mockChannels, mockMemberships, mockVideos } from '@/mocking/data'
+import { mockCategories, mockChannels, mockMemberships, mockVideos, mockWorkers } from '@/mocking/data'
 
 import {
   createBatchedVideoViewsAccessor,
@@ -135,6 +136,7 @@ const queryNodeHandlers = [
     SearchDocument,
     createSearchAccessor({ videos: mockVideos, channels: mockChannels })
   ),
+  createQueryHandler<GetWorkersQuery>(queryNode, GetWorkersDocument, () => mockWorkers),
 ]
 
 const orionHandlers = [

+ 1 - 1
src/providers/assets/useAsset.tsx

@@ -6,7 +6,7 @@ import { UseAssetDataArgs } from './types'
 
 export const useAsset = ({ entity, assetType }: UseAssetDataArgs) => {
   const assetData = readAssetData(entity, assetType)
-  const contentId = assetData?.dataObject?.joystreamContentId ?? null
+  const contentId = assetData?.dataObject?.joystreamContentId ?? assetData?.urls?.[0] ?? null
   const asset = useAssetStore((state) => (contentId ? state.assets[contentId] : null))
   const pendingAsset = useAssetStore((state) => (contentId ? state.pendingAssets[contentId] : null))
   const addPendingAsset = useAssetStore((state) => state.actions.addPendingAsset)

File diff suppressed because it is too large
+ 0 - 0
src/shared/assets/animations/error.json


File diff suppressed because it is too large
+ 0 - 0
src/shared/assets/animations/loader-L.json


File diff suppressed because it is too large
+ 0 - 0
src/shared/assets/animations/loader-M.json


+ 1 - 0
src/shared/assets/animations/loader-S.json

@@ -0,0 +1 @@
+{"v":"5.6.7","fr":60,"ip":0,"op":60,"w":24,"h":24,"nm":"Loader S","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Loader","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":59,"s":[360]}]},"p":{"a":0,"k":[12,12,0]},"s":{"a":0,"k":[50,50,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[36,36]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980407,0.219607844949,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":65},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Blue","bm":0,"hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[36,36]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.709803921569,0.756862745098,0.788235294118,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tm","s":{"a":0,"k":65},"e":{"a":0,"k":100},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Gray","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}

+ 1 - 0
src/shared/assets/animations/loader-XS.json

@@ -0,0 +1 @@
+{"v":"5.6.7","fr":60,"ip":0,"op":60,"w":16,"h":16,"nm":"Loader XS","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Loader","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":59,"s":[360]}]},"p":{"a":0,"k":[8,8,0]},"s":{"a":0,"k":[50,50,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980407,0.219607844949,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":65},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Blue","bm":0,"hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.709803921569,0.756862745098,0.788235294118,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tm","s":{"a":0,"k":65},"e":{"a":0,"k":100},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Gray","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}

File diff suppressed because it is too large
+ 0 - 0
src/shared/assets/animations/loader-player.json


+ 12 - 0
src/shared/components/AnimatedError/AnimatedError.tsx

@@ -0,0 +1,12 @@
+import React from 'react'
+import Lottie from 'react-lottie-player'
+
+import errorAnimation from '../../assets/animations/error.json'
+
+type AnimatedErrorProps = {
+  className?: string
+}
+
+export const AnimatedError: React.FC<AnimatedErrorProps> = ({ className }) => {
+  return <Lottie play loop={false} animationData={errorAnimation} className={className} />
+}

+ 1 - 0
src/shared/components/AnimatedError/index.ts

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

+ 63 - 0
src/shared/components/Banner/Banner.style.ts

@@ -0,0 +1,63 @@
+import styled from '@emotion/styled'
+
+import { Button, Text } from '@/shared/components'
+import { colors, sizes } from '@/shared/theme'
+
+import { BannerVariant } from './Banner'
+
+type BannerProps = {
+  variant: BannerVariant
+}
+
+export const BannerHeader = styled.div`
+  width: 100%;
+  height: ${sizes(8)};
+  display: flex;
+  align-items: center;
+`
+
+export const BannerIconContainer = styled.div`
+  display: flex;
+  justify-content: center;
+  align-content: center;
+  width: ${sizes(6)};
+  height: ${sizes(6)};
+  margin-right: ${sizes(2)};
+`
+
+export const BannerTitle = styled(Text)`
+  display: flex;
+  align-items: center;
+  word-break: break-word;
+`
+
+export const BannerButtonsContainer = styled.div`
+  display: flex;
+  margin-left: auto;
+`
+
+export const BannerActionButton = styled(Button)`
+  display: flex;
+  align-items: center;
+  padding: ${sizes(2)};
+  min-width: auto;
+`
+
+export const BannerDescription = styled(Text)`
+  margin-top: ${sizes(2)};
+  line-height: ${sizes(5)};
+  color: ${colors.gray[300]};
+  word-break: break-word;
+`
+
+export const BannerWrapper = styled.div<BannerProps>`
+  position: relative;
+  padding: ${sizes(4)};
+  box-shadow: ${({ variant }) => variant === 'primary' && `inset 0 0 0 1px ${colors.gray[700]}`};
+  width: 100%;
+  background-color: ${({ variant }) =>
+    variant === 'tertiary' ? colors.gray[700] : variant === 'secondary' ? colors.blue[500] : colors.transparent};
+  ${BannerDescription} {
+    color: ${({ variant }) => variant === 'secondary' && colors.blue[200]};
+  }
+`

+ 67 - 0
src/shared/components/Banner/Banner.tsx

@@ -0,0 +1,67 @@
+import React, { ReactNode } from 'react'
+
+import { IconButton } from '@/shared/components'
+import { SvgAlertError, SvgAlertInfo, SvgAlertSuccess, SvgAlertWarning, SvgGlyphClose } from '@/shared/icons'
+
+import {
+  BannerActionButton,
+  BannerButtonsContainer,
+  BannerDescription,
+  BannerHeader,
+  BannerIconContainer,
+  BannerTitle,
+  BannerWrapper,
+} from './Banner.style'
+
+export type BannerVariant = 'primary' | 'secondary' | 'tertiary'
+
+type BannerIconType = 'success' | 'error' | 'info' | 'warning'
+
+const ICON_TYPE_TO_ICON: Record<BannerIconType, ReactNode> = {
+  info: <SvgAlertInfo />,
+  success: <SvgAlertSuccess />,
+  error: <SvgAlertError />,
+  warning: <SvgAlertWarning />,
+}
+
+export type BannerProps = {
+  title?: string
+  description?: string
+  className?: string
+  variant?: BannerVariant
+  icon?: BannerIconType
+  onExitClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
+  actionText?: string
+  onActionClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
+}
+
+export const Banner: React.FC<BannerProps> = ({
+  title,
+  description,
+  className,
+  variant = 'primary',
+  icon,
+  actionText,
+  onExitClick,
+  onActionClick,
+}) => {
+  return (
+    <BannerWrapper className={className} variant={variant}>
+      <BannerHeader>
+        {icon && <BannerIconContainer>{ICON_TYPE_TO_ICON[icon]}</BannerIconContainer>}
+        <BannerTitle variant="subtitle2">{title}</BannerTitle>
+        <BannerButtonsContainer>
+          {actionText && (
+            <BannerActionButton variant="tertiary" onClick={onActionClick}>
+              {actionText}
+            </BannerActionButton>
+          )}
+          <IconButton aria-label="close dialog" onClick={onExitClick} variant="tertiary" size="small">
+            <SvgGlyphClose />
+          </IconButton>
+        </BannerButtonsContainer>
+      </BannerHeader>
+      {description && <BannerDescription variant="body2">{description}</BannerDescription>}
+    </BannerWrapper>
+  )
+}

+ 1 - 0
src/shared/components/Banner/index.ts

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

+ 100 - 1
src/shared/components/Button/Button.stories.tsx

@@ -14,7 +14,11 @@ export default {
     className: { table: { disable: true } },
     to: { table: { disable: true } },
     type: { table: { disable: true } },
-    variant: { table: { disable: true } },
+    textOnly: { table: { disable: true } },
+    iconPlacement: {
+      control: { type: 'select', options: ['left', 'right'] },
+      defaultValue: 'left',
+    },
   },
 } as Meta
 
@@ -41,7 +45,102 @@ export const Tertiary = Template.bind({})
 Tertiary.args = {
   variant: 'tertiary',
 }
+export const Destructive = Template.bind({})
+Destructive.args = {
+  variant: 'destructive',
+}
+export const DestructiveSecondary = Template.bind({})
+DestructiveSecondary.args = {
+  variant: 'destructive-secondary',
+}
+export const Warning = Template.bind({})
+Warning.args = {
+  variant: 'warning',
+}
+export const WarningSecondary = Template.bind({})
+WarningSecondary.args = {
+  variant: 'warning-secondary',
+}
 export const WithIcon = Template.bind({})
 WithIcon.args = {
   icon: <SvgGlyphAddVideo />,
 }
+const TextOnlyTemplate: Story<ButtonProps> = (args) => (
+  <div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large">
+        Large
+      </Button>
+      <Button {...args} size="medium">
+        Medium
+      </Button>
+      <Button {...args} size="small">
+        Small
+      </Button>
+    </div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large" variant="secondary">
+        Large
+      </Button>
+      <Button {...args} size="medium" variant="secondary">
+        Medium
+      </Button>
+      <Button {...args} size="small" variant="secondary">
+        Small
+      </Button>
+    </div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large" variant="destructive-secondary">
+        Large
+      </Button>
+      <Button {...args} size="medium" variant="destructive-secondary">
+        Medium
+      </Button>
+      <Button {...args} size="small" variant="destructive-secondary">
+        Small
+      </Button>
+    </div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large" variant="warning-secondary">
+        Large
+      </Button>
+      <Button {...args} size="medium" variant="warning-secondary">
+        Medium
+      </Button>
+      <Button {...args} size="small" variant="warning-secondary">
+        Small
+      </Button>
+    </div>
+  </div>
+)
+
+const IconOnlyTemplate: Story<ButtonProps> = (args) => (
+  <>
+    <Button {...args} size="large"></Button>
+    <Button {...args} size="medium"></Button>
+    <Button {...args} size="small"></Button>
+    <Button {...args} size="large" variant="secondary"></Button>
+    <Button {...args} size="medium" variant="secondary"></Button>
+    <Button {...args} size="small" variant="secondary"></Button>
+    <Button {...args} size="large" variant="tertiary"></Button>
+    <Button {...args} size="medium" variant="tertiary"></Button>
+    <Button {...args} size="small" variant="tertiary"></Button>
+    <Button {...args} size="large" variant="destructive-secondary"></Button>
+    <Button {...args} size="medium" variant="destructive-secondary"></Button>
+    <Button {...args} size="small" variant="destructive-secondary"></Button>
+    <Button {...args} size="large" variant="warning-secondary"></Button>
+    <Button {...args} size="medium" variant="warning-secondary"></Button>
+    <Button {...args} size="small" variant="warning-secondary"></Button>
+  </>
+)
+
+export const TextOnly = TextOnlyTemplate.bind({})
+TextOnly.args = {
+  textOnly: true,
+  icon: <SvgGlyphAddVideo />,
+}
+
+export const IconOnly = IconOnlyTemplate.bind({})
+IconOnly.args = {
+  icon: <SvgGlyphAddVideo />,
+}

+ 28 - 8
src/shared/components/Button/Button.style.ts

@@ -1,21 +1,40 @@
 import { SerializedStyles, css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import { colors, sizes } from '@/shared/theme'
+import { sizes } from '@/shared/theme'
 
-import { ButtonBase, ButtonSize } from '../ButtonBase'
+import { ButtonBase, ButtonSize, ButtonVariant } from '../ButtonBase'
 import { Text } from '../Text'
 
+export type IconPlacement = 'left' | 'right'
+type ButtonIconWrapperProps = {
+  iconPlacement: IconPlacement
+  iconOnly?: boolean
+}
+
 type ButtonSizeProps = {
   size: ButtonSize
 }
 
-export type TextColorVariant = 'default' | 'error'
 type TextProps = {
-  textColorVariant?: TextColorVariant
+  textColorVariant?: ButtonVariant
+  textOnly?: boolean
+  iconOnly?: boolean
 } & ButtonSizeProps
 
-const sizeOverwriteStyles = ({ size }: ButtonSizeProps): SerializedStyles => {
+const sizeOverwriteStyles = ({
+  size,
+  textOnly,
+  iconOnly,
+}: Pick<TextProps, 'size' | 'textOnly' | 'iconOnly'>): SerializedStyles | null => {
+  if (textOnly)
+    return css`
+      padding-left: 0;
+      padding-right: 0;
+    `
+  if (iconOnly) {
+    return null
+  }
   switch (size) {
     case 'large':
       return css`
@@ -58,13 +77,14 @@ export const StyledButtonBase = styled(ButtonBase)<ButtonSizeProps>`
   ${sizeOverwriteStyles};
 `
 
-export const ButtonIconWrapper = styled.span`
-  margin-right: ${sizes(2)};
+export const ButtonIconWrapper = styled.span<ButtonIconWrapperProps>`
+  margin-right: ${({ iconPlacement, iconOnly }) => (iconPlacement === 'left' && !iconOnly ? sizes(2) : 0)};
+  margin-left: ${({ iconPlacement, iconOnly }) => (iconPlacement === 'right' && !iconOnly ? sizes(2) : 0)};
 `
 
 export const StyledText = styled(Text)<TextProps>`
   /* compensate for line-height being 1 */
   ${textPaddingStyles};
 
-  color: ${({ textColorVariant }) => textColorVariant === 'error' && colors.error};
+  color: inherit;
 `

+ 28 - 6
src/shared/components/Button/Button.tsx

@@ -1,13 +1,13 @@
 import React from 'react'
 
-import { ButtonIconWrapper, StyledButtonBase, StyledText, TextColorVariant } from './Button.style'
+import { ButtonIconWrapper, IconPlacement, StyledButtonBase, StyledText } from './Button.style'
 
 import { ButtonBaseProps, ButtonSize } from '../ButtonBase'
 import { TextVariant } from '../Text'
 
 export type ButtonProps = {
   icon?: React.ReactNode
-  textColorVariant?: TextColorVariant
+  iconPlacement?: IconPlacement
   children: string
 } & Omit<ButtonBaseProps, 'children'>
 
@@ -18,15 +18,37 @@ const BUTTON_SIZE_TO_TEXT_VARIANT: Record<ButtonSize, TextVariant> = {
 }
 
 export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ icon, children, size = 'medium', textColorVariant = 'default', ...baseButtonProps }, ref) => {
+  ({ icon, children, size = 'medium', iconPlacement = 'left', variant, textOnly, ...baseButtonProps }, ref) => {
+    const iconOnly = !children
     return (
-      <StyledButtonBase ref={ref} size={size} {...baseButtonProps}>
-        {icon && <ButtonIconWrapper>{icon}</ButtonIconWrapper>}
+      <StyledButtonBase
+        ref={ref}
+        size={size}
+        {...baseButtonProps}
+        textOnly={textOnly}
+        variant={variant}
+        iconOnly={iconOnly}
+      >
+        {icon && iconPlacement === 'left' && (
+          <ButtonIconWrapper iconOnly={iconOnly} iconPlacement={iconPlacement}>
+            {icon}
+          </ButtonIconWrapper>
+        )}
         {children && (
-          <StyledText variant={BUTTON_SIZE_TO_TEXT_VARIANT[size]} textColorVariant={textColorVariant} size={size}>
+          <StyledText
+            variant={BUTTON_SIZE_TO_TEXT_VARIANT[size]}
+            textColorVariant={variant || 'primary'}
+            textOnly={textOnly}
+            size={size}
+          >
             {children}
           </StyledText>
         )}
+        {icon && iconPlacement === 'right' && (
+          <ButtonIconWrapper iconOnly={iconOnly} iconPlacement={iconPlacement}>
+            {icon}
+          </ButtonIconWrapper>
+        )}
       </StyledButtonBase>
     )
   }

+ 162 - 8
src/shared/components/ButtonBase/ButtonBase.style.ts

@@ -4,45 +4,76 @@ import styled from '@emotion/styled'
 
 import { colors, sizes, transitions } from '../../theme'
 
-export type ButtonVariant = 'primary' | 'secondary' | 'tertiary'
+export type ButtonVariant =
+  | 'primary'
+  | 'secondary'
+  | 'tertiary'
+  | 'destructive'
+  | 'destructive-secondary'
+  | 'warning'
+  | 'warning-secondary'
 export type ButtonSize = 'large' | 'medium' | 'small'
 export type ButtonBaseStyleProps = {
   variant: ButtonVariant
   size: ButtonSize
   clickable?: boolean
+  textOnly: boolean
+  iconOnly: boolean
 }
 
-const variantStyles = ({ variant }: ButtonBaseStyleProps): SerializedStyles => {
+const variantStyles = ({ variant, textOnly, iconOnly }: ButtonBaseStyleProps): SerializedStyles => {
   switch (variant) {
     case 'primary':
       return css`
         background-color: ${colors.blue[500]};
+        color: ${textOnly ? colors.blue[300] : colors.gray[50]};
 
-        &:hover {
+        path {
+          fill: ${textOnly && colors.blue[300]};
+        }
+
+        &:hover,
+        &:focus {
           background-color: ${colors.blue[700]};
         }
 
         &:active {
           background-color: ${colors.blue[900]};
+          color: ${textOnly && colors.blue[400]};
+
+          path {
+            fill: ${textOnly && colors.blue[400]};
+          }
         }
       `
     case 'secondary':
       return css`
         /* 1px inset border */
         box-shadow: inset 0 0 0 1px ${colors.gray[500]};
+        color: ${colors.gray[50]};
 
-        &:hover {
-          box-shadow: inset 0 0 0 1px ${colors.gray[50]};
+        &:hover,
+        &:focus {
+          box-shadow: inset 0 0 0 2px ${colors.gray[300]};
+          border-color: ${colors.gray[300]};
         }
 
         &:active {
-          box-shadow: inset 0 0 0 1px ${colors.gray[50]};
-          background-color: ${colors.blue[500]};
+          box-shadow: inset 0 0 0 2px ${colors.gray[50]};
+          color: ${textOnly && colors.gray[300]};
+
+          path {
+            fill: ${textOnly && colors.gray[300]};
+          }
         }
       `
     case 'tertiary':
       return css`
-        &:hover {
+        color: ${colors.gray[50]};
+        ${iconOnly && `border-radius: 50%`};
+
+        &:hover,
+        &:focus {
           background-color: ${colors.transparentPrimary[12]};
         }
 
@@ -50,6 +81,84 @@ const variantStyles = ({ variant }: ButtonBaseStyleProps): SerializedStyles => {
           background-color: ${colors.transparentPrimary[6]};
         }
       `
+    case 'destructive':
+      return css`
+        background-color: ${colors.secondary.alert[100]};
+        color: ${colors.gray[50]};
+
+        &:hover,
+        &:focus {
+          background-color: ${colors.secondary.alert[200]};
+        }
+
+        &:active {
+          background-color: ${colors.secondary.alert[300]};
+        }
+      `
+    case 'destructive-secondary':
+      return css`
+        box-shadow: inset 0 0 0 1px ${colors.gray[400]};
+        color: ${colors.secondary.alert[50]};
+
+        path {
+          fill: ${colors.secondary.alert[50]};
+        }
+
+        &:hover,
+        &:focus {
+          box-shadow: inset 0 0 0 2px ${colors.gray[300]};
+        }
+
+        &:active {
+          box-shadow: inset 0 0 0 2px ${colors.gray[50]};
+          color: ${textOnly && colors.secondary.alert[100]};
+
+          path {
+            fill: ${textOnly && colors.secondary.alert[100]};
+          }
+        }
+      `
+    case 'warning':
+      return css`
+        background-color: ${colors.secondary.warning[100]};
+        color: ${colors.gray[900]};
+
+        path {
+          fill: ${colors.gray[900]};
+        }
+
+        &:hover,
+        &:focus {
+          background-color: ${colors.secondary.warning[200]};
+        }
+
+        &:active {
+          background-color: ${colors.secondary.warning[300]};
+        }
+      `
+    case 'warning-secondary':
+      return css`
+        color: ${colors.secondary.warning[100]};
+        box-shadow: inset 0 0 0 1px ${colors.gray[400]};
+
+        path {
+          fill: ${colors.secondary.warning[100]};
+        }
+
+        &:hover,
+        &:focus {
+          box-shadow: inset 0 0 0 2px ${colors.gray[300]};
+        }
+
+        &:active {
+          color: ${textOnly && colors.secondary.warning[300]};
+          box-shadow: inset 0 0 0 2px ${colors.gray[50]};
+
+          path {
+            fill: ${textOnly && colors.secondary.warning[300]};
+          }
+        }
+      `
   }
 }
 
@@ -70,6 +179,46 @@ const sizeStyles = ({ size }: ButtonBaseStyleProps): SerializedStyles => {
   }
 }
 
+const textOnlyStyles = ({ textOnly }: ButtonBaseStyleProps): SerializedStyles | null =>
+  textOnly
+    ? css`
+        background-color: ${colors.transparent};
+        box-shadow: none;
+        padding: 0;
+
+        &:hover,
+        &:focus {
+          background-color: ${colors.transparent};
+          box-shadow: none;
+        }
+
+        &:active {
+          background-color: ${colors.transparent};
+          box-shadow: none;
+        }
+      `
+    : null
+
+export const BorderWrapper = styled.div<Pick<ButtonBaseStyleProps, 'textOnly'>>`
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+  margin-top: -0.5px;
+  margin-bottom: -0.5px;
+  height: 100%;
+  visibility: hidden;
+  border-bottom-width: ${({ textOnly }) => textOnly && '1px'};
+  border-bottom-style: ${({ textOnly }) => textOnly && 'solid'};
+
+  &:hover {
+    visibility: visible;
+  }
+
+  * {
+    visibility: visible;
+  }
+`
+
 export const StyledButtonBase = styled('button', { shouldForwardProp: isPropValid })<ButtonBaseStyleProps>`
   display: inline-flex;
   align-items: center;
@@ -91,4 +240,9 @@ export const StyledButtonBase = styled('button', { shouldForwardProp: isPropVali
 
   ${variantStyles};
   ${sizeStyles};
+  ${textOnlyStyles};
+
+  :focus ${BorderWrapper} {
+    visibility: visible;
+  }
 `

+ 21 - 3
src/shared/components/ButtonBase/ButtonBase.tsx

@@ -2,13 +2,15 @@ import { To } from 'history'
 import React from 'react'
 import { Link } from 'react-router-dom'
 
-import { ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
+import { BorderWrapper, ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
 
 export type ButtonBaseProps = {
   disabled?: boolean
   onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
   to?: To
   type?: 'button' | 'submit'
+  textOnly?: boolean
+  iconOnly?: boolean
   children?: React.ReactNode
   className?: string
 } & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant'>>
@@ -26,7 +28,21 @@ const getLinkPropsFromTo = (to?: To) => {
 }
 
 export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
-  ({ onClick, to, type = 'button', children, size = 'medium', variant = 'primary', disabled, ...styleProps }, ref) => {
+  (
+    {
+      onClick,
+      to,
+      type = 'button',
+      children,
+      size = 'medium',
+      variant = 'primary',
+      textOnly = false,
+      iconOnly = false,
+      disabled,
+      ...styleProps
+    },
+    ref
+  ) => {
     const clickable = !!onClick || !!to || type === 'submit'
 
     const linkProps = getLinkPropsFromTo(to)
@@ -42,9 +58,11 @@ export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
         {...linkProps}
         size={size}
         variant={variant}
+        textOnly={textOnly}
+        iconOnly={iconOnly}
         {...styleProps}
       >
-        {children}
+        <BorderWrapper textOnly={textOnly}>{children}</BorderWrapper>
       </StyledButtonBase>
     )
   }

+ 9 - 0
src/shared/components/DismissibleMessage/DismissibleMessage.stories.tsx

@@ -21,6 +21,15 @@ export default {
       defaultValue:
         'This mean you can only access one on the device you used to create it. Clearing your browser history will delete all your drafts.',
     },
+    variant: {
+      control: { type: 'select', options: ['primary', 'secondary', 'tertiary'] },
+      defaultValue: 'primary',
+    },
+    actionText: { defaultValue: 'Action' },
+    icon: {
+      control: { type: 'select', options: [null, 'error', 'success', 'info', 'warning'] },
+      defaultValue: null,
+    },
   },
   decorators: [(Story) => <Story />],
 } as Meta

+ 0 - 38
src/shared/components/DismissibleMessage/DismissibleMessage.style.ts

@@ -1,38 +0,0 @@
-import styled from '@emotion/styled'
-
-import { SvgGlyphInfo } from '@/shared/icons'
-import { colors, sizes } from '@/shared/theme'
-
-import { IconButton } from '../IconButton'
-import { Text } from '../Text'
-
-export const MessageWrapper = styled.div`
-  position: relative;
-  padding: ${sizes(4)};
-  border: 1px solid ${colors.gray[700]};
-  width: 100%;
-  max-width: 450px;
-`
-
-export const MessageButton = styled(IconButton)`
-  position: absolute;
-  top: ${sizes(2)};
-  right: ${sizes(2)};
-`
-
-export const StyledSvgGlyphInfo = styled(SvgGlyphInfo)`
-  margin-right: ${sizes(2)};
-`
-
-export const MessageTitle = styled(Text)`
-  display: flex;
-  align-items: center;
-  word-break: break-word;
-`
-
-export const MessageDescription = styled(Text)`
-  margin-top: ${sizes(2)};
-  line-height: ${sizes(5)};
-  color: ${colors.gray[300]};
-  word-break: break-word;
-`

+ 8 - 33
src/shared/components/DismissibleMessage/DismissibleMessage.tsx

@@ -1,47 +1,22 @@
-import React, { useEffect, useState } from 'react'
+import React from 'react'
 
 import { usePersonalDataStore } from '@/providers'
-import { SvgGlyphClose } from '@/shared/icons'
 
-import {
-  MessageButton,
-  MessageDescription,
-  MessageTitle,
-  MessageWrapper,
-  StyledSvgGlyphInfo,
-} from './DismissibleMessage.style'
+import { Banner, BannerProps } from '../Banner'
 
 export type DismissibleMessageProps = {
-  title?: string
-  description?: string
   id: string
-  className?: string
-}
+} & Omit<BannerProps, 'onExitClick'>
 
-export const DismissibleMessage: React.FC<DismissibleMessageProps> = ({ title, description, id, className }) => {
-  const dismissedMessages = usePersonalDataStore((state) => state.dismissedMessages)
+export const DismissibleMessage: React.FC<DismissibleMessageProps> = ({ id, ...dismissedMessageProps }) => {
+  const isDismissedMessage = usePersonalDataStore((state) =>
+    state.dismissedMessages.some((message) => message.id === id)
+  )
   const updateDismissedMessages = usePersonalDataStore((state) => state.actions.updateDismissedMessages)
-  const [isDismissedMessage, setDismissedMessage] = useState<boolean>()
-
-  useEffect(() => {
-    const isDissmised = dismissedMessages.some((channel) => channel.id === id)
-    setDismissedMessage(isDissmised)
-  }, [dismissedMessages, id])
 
   if (isDismissedMessage) {
     return null
   }
 
-  return (
-    <MessageWrapper className={className}>
-      <MessageTitle variant="subtitle2">
-        <StyledSvgGlyphInfo />
-        {title}
-      </MessageTitle>
-      <MessageButton aria-label="close dialog" onClick={() => updateDismissedMessages(id)} variant="tertiary">
-        <SvgGlyphClose />
-      </MessageButton>
-      <MessageDescription variant="body2">{description}</MessageDescription>
-    </MessageWrapper>
-  )
+  return <Banner {...dismissedMessageProps} onExitClick={() => updateDismissedMessages(id)} />
 }

+ 16 - 0
src/shared/components/IconButton/IconButton.stories.tsx

@@ -46,3 +46,19 @@ export const Tertiary = Template.bind({})
 Tertiary.args = {
   variant: 'tertiary',
 }
+export const Destructive = Template.bind({})
+Destructive.args = {
+  variant: 'destructive',
+}
+export const DestructiveSecondary = Template.bind({})
+DestructiveSecondary.args = {
+  variant: 'destructive-secondary',
+}
+export const Warning = Template.bind({})
+Warning.args = {
+  variant: 'warning',
+}
+export const WarningSecondary = Template.bind({})
+WarningSecondary.args = {
+  variant: 'warning-secondary',
+}

+ 1 - 1
src/shared/components/IconButton/IconButton.tsx

@@ -4,7 +4,7 @@ import { StyledButtonBase } from './IconButton.style'
 
 import { ButtonBaseProps } from '../ButtonBase'
 
-export type IconButtonProps = ButtonBaseProps & {
+export type IconButtonProps = Omit<ButtonBaseProps, 'textOnly'> & {
   children: ReactNode
 }
 

+ 40 - 0
src/shared/components/Loader/Loader.tsx

@@ -0,0 +1,40 @@
+import styled from '@emotion/styled'
+import React from 'react'
+import Lottie from 'react-lottie-player'
+
+import loaderLargeAnimation from '../../assets/animations/loader-L.json'
+import loaderMediumAnimation from '../../assets/animations/loader-M.json'
+import loaderSmallAnimation from '../../assets/animations/loader-S.json'
+import LoaderXSmallAnimation from '../../assets/animations/loader-XS.json'
+import loaderPlayerAnimation from '../../assets/animations/loader-player.json'
+
+type LoaderVariant = 'xlarge' | 'large' | 'medium' | 'small' | 'xsmall' | 'player'
+type LoaderProps = {
+  variant?: LoaderVariant
+  className?: string
+}
+type LoaderConfig = {
+  data: object
+  size: number
+}
+
+const VARIANT_TO_CONFIG: Record<LoaderVariant, LoaderConfig> = {
+  xlarge: { data: loaderLargeAnimation, size: 216 },
+  large: { data: loaderLargeAnimation, size: 108 },
+  medium: { data: loaderMediumAnimation, size: 36 },
+  small: { data: loaderSmallAnimation, size: 24 },
+  xsmall: { data: LoaderXSmallAnimation, size: 16 },
+  player: { data: loaderPlayerAnimation, size: 72 },
+}
+
+export const Loader: React.FC<LoaderProps> = ({ variant = 'medium', className }) => {
+  const config = VARIANT_TO_CONFIG[variant]
+  return <StyledLottie play animationData={config.data} size={config.size} className={className} />
+}
+
+const StyledLottie = styled(Lottie, {
+  shouldForwardProp: (prop) => prop !== 'size',
+})<{ size: number }>`
+  width: ${({ size }) => `${size}px`};
+  height: ${({ size }) => `${size}px`};
+`

+ 1 - 0
src/shared/components/Loader/index.ts

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

+ 3 - 0
src/shared/components/index.ts

@@ -29,6 +29,7 @@ export * from './Select'
 export * from './TextField'
 export * from './Checkbox'
 export * from './TextArea'
+export * from './Banner'
 export * from './DismissibleMessage'
 export * from './ActionBar'
 export * from './MultiFileSelect'
@@ -40,3 +41,5 @@ export * from './ExpandButton'
 export * from './Snackbar'
 export * from './HelperText'
 export * from './LegalText'
+export * from './Loader'
+export * from './AnimatedError'

+ 30 - 2
src/shared/theme/colors.ts

@@ -16,6 +16,7 @@ export default {
   },
   blue: {
     900: '#030041',
+    800: '#221BCC',
     700: '#261EE4',
     600: '#2F2FF4',
     500: '#4038FF',
@@ -24,11 +25,34 @@ export default {
     200: '#B4BBFF',
     50: '#E0E1FF',
   },
+  secondary: {
+    alert: {
+      300: '#73130D',
+      200: '#A5261D',
+      100: '#CC392F',
+      50: '#FF6157',
+      5: '#F1C4C1',
+    },
+    warning: {
+      300: '#97800C',
+      200: '#D4B411',
+      100: '#EFCF34',
+      50: '#FBE36A',
+      5: '#FDF1B5',
+    },
+    success: {
+      300: '#0B8E41',
+      200: '#0EAF51',
+      100: '#10CC5E',
+      50: '#55F699',
+      5: '#B3FFD2',
+    },
+  },
   success: '#1CCB00',
-  transparentSuccess: 'rgba(0, 219, 176, 0.08)',
   error: '#FF3861',
-  transparentError: 'rgba(255, 56, 97, 0.08)',
   warning: '#EB4F1E',
+  transparentSuccess: 'rgba(26, 183, 0, 0.08)',
+  transparentError: 'rgba(176, 0, 32, 0.08)',
   transparentWhite: {
     4: 'rgba(255,255,255, 0.04)',
     6: 'rgba(255,255,255, 0.06)',
@@ -39,6 +63,7 @@ export default {
     24: 'rgba(0,0,0, 0.24)',
     54: 'rgba(0,0,0, 0.54)',
     66: 'rgba(0,0,0, 0.66)',
+    86: 'rgba(0,0,0, 0.86)',
   },
   transparentPrimary: {
     6: 'rgba(180, 187, 255, 0.06)',
@@ -47,4 +72,7 @@ export default {
     18: 'rgba(180, 187, 255, 0.18)',
     20: 'rgba(180, 187, 255, 0.20)',
   },
+  transparentNeutral: {
+    54: 'rgba(24, 28, 32, 0.54)',
+  },
 }

+ 3 - 0
src/views/playground/PlaygroundLayout.tsx

@@ -7,6 +7,7 @@ import { ActiveUserProvider, ConnectionStatusManager, DialogProvider } from '@/p
 import { colors } from '@/shared/theme'
 
 import {
+  Animations,
   AutomaticCrop,
   Dialogs,
   FileHashing,
@@ -21,6 +22,7 @@ import {
 } from './Playgrounds'
 
 const playgroundRoutes = [
+  { path: 'animations', element: <Animations />, name: 'Animations' },
   { path: 'validation-form', element: <PlaygroundValidationForm />, name: 'Validation Form' },
   { path: 'drafts', element: <PlaygroundDrafts />, name: 'Drafts' },
   { path: 'video-metadata', element: <VideoMetaData />, name: 'Video Metadata' },
@@ -78,6 +80,7 @@ const NavContainer = styled.div`
 `
 
 const ContentContainer = styled.div`
+  width: 100%;
   display: flex;
   flex-direction: column;
   padding-left: 30px;

+ 26 - 0
src/views/playground/Playgrounds/Animations.tsx

@@ -0,0 +1,26 @@
+import styled from '@emotion/styled'
+import React from 'react'
+
+import { AnimatedError, Loader } from '@/shared/components'
+
+export const Animations = () => {
+  return (
+    <Container>
+      <Loader variant="xlarge" />
+      <Loader variant="large" />
+      <Loader variant="medium" />
+      <Loader variant="small" />
+      <Loader variant="xsmall" />
+      <Loader variant="player" />
+      <AnimatedError />
+    </Container>
+  )
+}
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: start;
+  gap: 32px;
+  justify-content: center;
+`

+ 1 - 0
src/views/playground/Playgrounds/index.ts

@@ -9,3 +9,4 @@ export * from './PlaygroundValidationForm'
 export * from './UploadFiles'
 export * from './VideoMetaData'
 export * from './PlaygroundCommonStore'
+export * from './Animations'

+ 1 - 1
src/views/studio/EditVideoSheet/EditVideoForm/EditVideoForm.tsx

@@ -618,7 +618,7 @@ export const EditVideoForm: React.FC<EditVideoFormProps> = ({
           </FormField>
           {isEdit && (
             <DeleteVideoContainer>
-              <DeleteVideoButton size="large" variant="tertiary" textColorVariant="error" onClick={handleDeleteVideo}>
+              <DeleteVideoButton size="large" variant="destructive-secondary" onClick={handleDeleteVideo}>
                 Delete video
               </DeleteVideoButton>
             </DeleteVideoContainer>

+ 9 - 0
src/views/studio/MyVideosView/MyVideosView.tsx

@@ -270,9 +270,18 @@ export const MyVideosView = () => {
               <StyledDismissibleMessage
                 id="video-draft-saved-locally-warning"
                 title="Video drafts are saved locally"
+                icon="info"
                 description="You will only be able to access drafts on the device you used to create them. Clearing your browser history will delete all your drafts."
               />
             )}
+            {currentTabName === 'Unlisted' && (
+              <StyledDismissibleMessage
+                id="unlisted-video-link-info"
+                title="Unlisted videos can be seen only with direct link"
+                icon="info"
+                description="You can share a private video with others by sharing a direct link to it. Unlisted video is not going to be searchable on our platform."
+              />
+            )}
             <Grid maxColumns={null} onResize={handleOnResizeGrid}>
               {gridContent}
             </Grid>

+ 14 - 0
yarn.lock

@@ -14541,6 +14541,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
+lottie-web@^5.7.6:
+  version "5.7.11"
+  resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.7.11.tgz#4ba74e8a629f76d3c0a0062ddc37d2b96e13765c"
+  integrity sha512-Jvz3PQqwrDj1rXGqfeQtipH/WNtM/Y4l8t8NIQXe1xUI0nVooH2bTYJGef0UkdBcWUx1s3miKsRhyP196g9tvQ==
+
 lower-case@2.0.1, lower-case@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7"
@@ -17802,6 +17807,15 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
+react-lottie-player@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/react-lottie-player/-/react-lottie-player-1.3.2.tgz#1424aec0b3a14f343b8dbe054709e2c9d5a5c8fb"
+  integrity sha512-kREeIztLgrkDqpwnJpqJobxQ9nZmduVQvdBJRVj7O10sL1B8cmLMVUa1gj1CxDBjHx+XC2FxPD4w3qey8bPPNQ==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    lodash.clonedeep "^4.5.0"
+    lottie-web "^5.7.6"
+
 react-number-format@^4.4.4:
   version "4.4.4"
   resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-4.4.4.tgz#2a7f50be404f990ec15855cc6babfeae1be16351"

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