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
 public/mockServiceWorker.js
 .history
 .history
 docs/
 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-dropzone": "^11.3.1",
     "react-hook-form": "^7.8.1",
     "react-hook-form": "^7.8.1",
     "react-intersection-observer": "^8.31.0",
     "react-intersection-observer": "^8.31.0",
+    "react-lottie-player": "^1.3.2",
     "react-number-format": "^4.4.4",
     "react-number-format": "^4.4.4",
     "react-router": "^6.0.0-beta.0",
     "react-router": "^6.0.0-beta.0",
     "react-router-dom": "^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']>
   first?: Maybe<Scalars['Int']>
   after?: Maybe<Scalars['String']>
   after?: Maybe<Scalars['String']>
   where?: Maybe<ChannelWhereInput>
   where?: Maybe<ChannelWhereInput>
-  orderBy?: Maybe<ChannelOrderByInput>
+  orderBy?: Maybe<Array<ChannelOrderByInput>>
 }
 }
 
 
 export type QueryMembershipByUniqueInputArgs = {
 export type QueryMembershipByUniqueInputArgs = {
@@ -348,14 +348,14 @@ export type QueryVideosArgs = {
   offset?: Maybe<Scalars['Int']>
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
   where?: Maybe<VideoWhereInput>
   where?: Maybe<VideoWhereInput>
-  orderBy?: Maybe<VideoOrderByInput>
+  orderBy?: Maybe<Array<VideoOrderByInput>>
 }
 }
 
 
 export type QueryVideosConnectionArgs = {
 export type QueryVideosConnectionArgs = {
   first?: Maybe<Scalars['Int']>
   first?: Maybe<Scalars['Int']>
   after?: Maybe<Scalars['String']>
   after?: Maybe<Scalars['String']>
   where?: Maybe<VideoWhereInput>
   where?: Maybe<VideoWhereInput>
-  orderBy?: Maybe<VideoOrderByInput>
+  orderBy?: Maybe<Array<VideoOrderByInput>>
 }
 }
 
 
 export type QueryWorkerByUniqueInputArgs = {
 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 type GetChannelsQueryResult = Apollo.QueryResult<GetChannelsQuery, GetChannelsQueryVariables>
 export const GetChannelsConnectionDocument = gql`
 export const GetChannelsConnectionDocument = gql`
   query GetChannelsConnection($first: Int, $after: String, $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 {
       edges {
         cursor
         cursor
         node {
         node {

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

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

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

@@ -52,7 +52,7 @@ query GetChannels($where: ChannelWhereInput) {
 }
 }
 
 
 query GetChannelsConnection($first: Int, $after: String, $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 {
     edges {
       cursor
       cursor
       node {
       node {

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

@@ -60,10 +60,10 @@ query GetVideo($where: VideoWhereUniqueInput!) {
 query GetVideosConnection(
 query GetVideosConnection(
   $first: Int
   $first: Int
   $after: String
   $after: String
-  $orderBy: VideoOrderByInput = createdAt_DESC
+  $orderBy: VideoOrderByInput! = createdAt_DESC
   $where: VideoWhereInput
   $where: VideoWhereInput
 ) {
 ) {
-  videosConnection(first: $first, after: $after, where: $where, orderBy: $orderBy) {
+  videosConnection(first: $first, after: $after, where: $where, orderBy: [$orderBy]) {
     edges {
     edges {
       cursor
       cursor
       node {
       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
     ...VideoFields
   }
   }
 }
 }

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

@@ -273,17 +273,17 @@ type Query {
     first: Int
     first: Int
     after: String
     after: String
     where: ChannelWhereInput
     where: ChannelWhereInput
-    orderBy: ChannelOrderByInput
+    orderBy: [ChannelOrderByInput!]
   ): ChannelConnection!
   ): ChannelConnection!
 
 
   # Lookup video by its ID
   # Lookup video by its ID
   videoByUniqueInput(where: VideoWhereUniqueInput!): Video
   videoByUniqueInput(where: VideoWhereUniqueInput!): Video
 
 
   # Lookup videos by where params
   # 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
   # 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
   # List all categories
   videoCategories: [VideoCategory!]!
   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 mockCategories } from './mockCategories'
 export { default as mockLicenses } from './mockLicenses'
 export { default as mockLicenses } from './mockLicenses'
 export { default as mockMemberships } from './mockMemberships'
 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,
   SearchQuery,
   SearchQueryVariables,
   SearchQueryVariables,
 } from '@/api/queries'
 } from '@/api/queries'
+import { GetWorkersDocument, GetWorkersQuery } from '@/api/queries/__generated__/workers.generated'
 import { ORION_GRAPHQL_URL, QUERY_NODE_GRAPHQL_URL } from '@/config/urls'
 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 {
 import {
   createBatchedVideoViewsAccessor,
   createBatchedVideoViewsAccessor,
@@ -135,6 +136,7 @@ const queryNodeHandlers = [
     SearchDocument,
     SearchDocument,
     createSearchAccessor({ videos: mockVideos, channels: mockChannels })
     createSearchAccessor({ videos: mockVideos, channels: mockChannels })
   ),
   ),
+  createQueryHandler<GetWorkersQuery>(queryNode, GetWorkersDocument, () => mockWorkers),
 ]
 ]
 
 
 const orionHandlers = [
 const orionHandlers = [

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

@@ -6,7 +6,7 @@ import { UseAssetDataArgs } from './types'
 
 
 export const useAsset = ({ entity, assetType }: UseAssetDataArgs) => {
 export const useAsset = ({ entity, assetType }: UseAssetDataArgs) => {
   const assetData = readAssetData(entity, assetType)
   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 asset = useAssetStore((state) => (contentId ? state.assets[contentId] : null))
   const pendingAsset = useAssetStore((state) => (contentId ? state.pendingAssets[contentId] : null))
   const pendingAsset = useAssetStore((state) => (contentId ? state.pendingAssets[contentId] : null))
   const addPendingAsset = useAssetStore((state) => state.actions.addPendingAsset)
   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 } },
     className: { table: { disable: true } },
     to: { table: { disable: true } },
     to: { table: { disable: true } },
     type: { 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
 } as Meta
 
 
@@ -41,7 +45,102 @@ export const Tertiary = Template.bind({})
 Tertiary.args = {
 Tertiary.args = {
   variant: 'tertiary',
   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({})
 export const WithIcon = Template.bind({})
 WithIcon.args = {
 WithIcon.args = {
   icon: <SvgGlyphAddVideo />,
   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 { SerializedStyles, css } from '@emotion/react'
 import styled from '@emotion/styled'
 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'
 import { Text } from '../Text'
 
 
+export type IconPlacement = 'left' | 'right'
+type ButtonIconWrapperProps = {
+  iconPlacement: IconPlacement
+  iconOnly?: boolean
+}
+
 type ButtonSizeProps = {
 type ButtonSizeProps = {
   size: ButtonSize
   size: ButtonSize
 }
 }
 
 
-export type TextColorVariant = 'default' | 'error'
 type TextProps = {
 type TextProps = {
-  textColorVariant?: TextColorVariant
+  textColorVariant?: ButtonVariant
+  textOnly?: boolean
+  iconOnly?: boolean
 } & ButtonSizeProps
 } & 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) {
   switch (size) {
     case 'large':
     case 'large':
       return css`
       return css`
@@ -58,13 +77,14 @@ export const StyledButtonBase = styled(ButtonBase)<ButtonSizeProps>`
   ${sizeOverwriteStyles};
   ${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>`
 export const StyledText = styled(Text)<TextProps>`
   /* compensate for line-height being 1 */
   /* compensate for line-height being 1 */
   ${textPaddingStyles};
   ${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 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 { ButtonBaseProps, ButtonSize } from '../ButtonBase'
 import { TextVariant } from '../Text'
 import { TextVariant } from '../Text'
 
 
 export type ButtonProps = {
 export type ButtonProps = {
   icon?: React.ReactNode
   icon?: React.ReactNode
-  textColorVariant?: TextColorVariant
+  iconPlacement?: IconPlacement
   children: string
   children: string
 } & Omit<ButtonBaseProps, 'children'>
 } & Omit<ButtonBaseProps, 'children'>
 
 
@@ -18,15 +18,37 @@ const BUTTON_SIZE_TO_TEXT_VARIANT: Record<ButtonSize, TextVariant> = {
 }
 }
 
 
 export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
 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 (
     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 && (
         {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}
             {children}
           </StyledText>
           </StyledText>
         )}
         )}
+        {icon && iconPlacement === 'right' && (
+          <ButtonIconWrapper iconOnly={iconOnly} iconPlacement={iconPlacement}>
+            {icon}
+          </ButtonIconWrapper>
+        )}
       </StyledButtonBase>
       </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'
 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 ButtonSize = 'large' | 'medium' | 'small'
 export type ButtonBaseStyleProps = {
 export type ButtonBaseStyleProps = {
   variant: ButtonVariant
   variant: ButtonVariant
   size: ButtonSize
   size: ButtonSize
   clickable?: boolean
   clickable?: boolean
+  textOnly: boolean
+  iconOnly: boolean
 }
 }
 
 
-const variantStyles = ({ variant }: ButtonBaseStyleProps): SerializedStyles => {
+const variantStyles = ({ variant, textOnly, iconOnly }: ButtonBaseStyleProps): SerializedStyles => {
   switch (variant) {
   switch (variant) {
     case 'primary':
     case 'primary':
       return css`
       return css`
         background-color: ${colors.blue[500]};
         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]};
           background-color: ${colors.blue[700]};
         }
         }
 
 
         &:active {
         &:active {
           background-color: ${colors.blue[900]};
           background-color: ${colors.blue[900]};
+          color: ${textOnly && colors.blue[400]};
+
+          path {
+            fill: ${textOnly && colors.blue[400]};
+          }
         }
         }
       `
       `
     case 'secondary':
     case 'secondary':
       return css`
       return css`
         /* 1px inset border */
         /* 1px inset border */
         box-shadow: inset 0 0 0 1px ${colors.gray[500]};
         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 {
         &: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':
     case 'tertiary':
       return css`
       return css`
-        &:hover {
+        color: ${colors.gray[50]};
+        ${iconOnly && `border-radius: 50%`};
+
+        &:hover,
+        &:focus {
           background-color: ${colors.transparentPrimary[12]};
           background-color: ${colors.transparentPrimary[12]};
         }
         }
 
 
@@ -50,6 +81,84 @@ const variantStyles = ({ variant }: ButtonBaseStyleProps): SerializedStyles => {
           background-color: ${colors.transparentPrimary[6]};
           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>`
 export const StyledButtonBase = styled('button', { shouldForwardProp: isPropValid })<ButtonBaseStyleProps>`
   display: inline-flex;
   display: inline-flex;
   align-items: center;
   align-items: center;
@@ -91,4 +240,9 @@ export const StyledButtonBase = styled('button', { shouldForwardProp: isPropVali
 
 
   ${variantStyles};
   ${variantStyles};
   ${sizeStyles};
   ${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 React from 'react'
 import { Link } from 'react-router-dom'
 import { Link } from 'react-router-dom'
 
 
-import { ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
+import { BorderWrapper, ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
 
 
 export type ButtonBaseProps = {
 export type ButtonBaseProps = {
   disabled?: boolean
   disabled?: boolean
   onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
   onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
   to?: To
   to?: To
   type?: 'button' | 'submit'
   type?: 'button' | 'submit'
+  textOnly?: boolean
+  iconOnly?: boolean
   children?: React.ReactNode
   children?: React.ReactNode
   className?: string
   className?: string
 } & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant'>>
 } & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant'>>
@@ -26,7 +28,21 @@ const getLinkPropsFromTo = (to?: To) => {
 }
 }
 
 
 export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
 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 clickable = !!onClick || !!to || type === 'submit'
 
 
     const linkProps = getLinkPropsFromTo(to)
     const linkProps = getLinkPropsFromTo(to)
@@ -42,9 +58,11 @@ export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
         {...linkProps}
         {...linkProps}
         size={size}
         size={size}
         variant={variant}
         variant={variant}
+        textOnly={textOnly}
+        iconOnly={iconOnly}
         {...styleProps}
         {...styleProps}
       >
       >
-        {children}
+        <BorderWrapper textOnly={textOnly}>{children}</BorderWrapper>
       </StyledButtonBase>
       </StyledButtonBase>
     )
     )
   }
   }

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

@@ -21,6 +21,15 @@ export default {
       defaultValue:
       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.',
         '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 />],
   decorators: [(Story) => <Story />],
 } as Meta
 } 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 { 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 = {
 export type DismissibleMessageProps = {
-  title?: string
-  description?: string
   id: 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 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) {
   if (isDismissedMessage) {
     return null
     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 = {
 Tertiary.args = {
   variant: 'tertiary',
   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'
 import { ButtonBaseProps } from '../ButtonBase'
 
 
-export type IconButtonProps = ButtonBaseProps & {
+export type IconButtonProps = Omit<ButtonBaseProps, 'textOnly'> & {
   children: ReactNode
   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 './TextField'
 export * from './Checkbox'
 export * from './Checkbox'
 export * from './TextArea'
 export * from './TextArea'
+export * from './Banner'
 export * from './DismissibleMessage'
 export * from './DismissibleMessage'
 export * from './ActionBar'
 export * from './ActionBar'
 export * from './MultiFileSelect'
 export * from './MultiFileSelect'
@@ -40,3 +41,5 @@ export * from './ExpandButton'
 export * from './Snackbar'
 export * from './Snackbar'
 export * from './HelperText'
 export * from './HelperText'
 export * from './LegalText'
 export * from './LegalText'
+export * from './Loader'
+export * from './AnimatedError'

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

@@ -16,6 +16,7 @@ export default {
   },
   },
   blue: {
   blue: {
     900: '#030041',
     900: '#030041',
+    800: '#221BCC',
     700: '#261EE4',
     700: '#261EE4',
     600: '#2F2FF4',
     600: '#2F2FF4',
     500: '#4038FF',
     500: '#4038FF',
@@ -24,11 +25,34 @@ export default {
     200: '#B4BBFF',
     200: '#B4BBFF',
     50: '#E0E1FF',
     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',
   success: '#1CCB00',
-  transparentSuccess: 'rgba(0, 219, 176, 0.08)',
   error: '#FF3861',
   error: '#FF3861',
-  transparentError: 'rgba(255, 56, 97, 0.08)',
   warning: '#EB4F1E',
   warning: '#EB4F1E',
+  transparentSuccess: 'rgba(26, 183, 0, 0.08)',
+  transparentError: 'rgba(176, 0, 32, 0.08)',
   transparentWhite: {
   transparentWhite: {
     4: 'rgba(255,255,255, 0.04)',
     4: 'rgba(255,255,255, 0.04)',
     6: 'rgba(255,255,255, 0.06)',
     6: 'rgba(255,255,255, 0.06)',
@@ -39,6 +63,7 @@ export default {
     24: 'rgba(0,0,0, 0.24)',
     24: 'rgba(0,0,0, 0.24)',
     54: 'rgba(0,0,0, 0.54)',
     54: 'rgba(0,0,0, 0.54)',
     66: 'rgba(0,0,0, 0.66)',
     66: 'rgba(0,0,0, 0.66)',
+    86: 'rgba(0,0,0, 0.86)',
   },
   },
   transparentPrimary: {
   transparentPrimary: {
     6: 'rgba(180, 187, 255, 0.06)',
     6: 'rgba(180, 187, 255, 0.06)',
@@ -47,4 +72,7 @@ export default {
     18: 'rgba(180, 187, 255, 0.18)',
     18: 'rgba(180, 187, 255, 0.18)',
     20: 'rgba(180, 187, 255, 0.20)',
     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 { colors } from '@/shared/theme'
 
 
 import {
 import {
+  Animations,
   AutomaticCrop,
   AutomaticCrop,
   Dialogs,
   Dialogs,
   FileHashing,
   FileHashing,
@@ -21,6 +22,7 @@ import {
 } from './Playgrounds'
 } from './Playgrounds'
 
 
 const playgroundRoutes = [
 const playgroundRoutes = [
+  { path: 'animations', element: <Animations />, name: 'Animations' },
   { path: 'validation-form', element: <PlaygroundValidationForm />, name: 'Validation Form' },
   { path: 'validation-form', element: <PlaygroundValidationForm />, name: 'Validation Form' },
   { path: 'drafts', element: <PlaygroundDrafts />, name: 'Drafts' },
   { path: 'drafts', element: <PlaygroundDrafts />, name: 'Drafts' },
   { path: 'video-metadata', element: <VideoMetaData />, name: 'Video Metadata' },
   { path: 'video-metadata', element: <VideoMetaData />, name: 'Video Metadata' },
@@ -78,6 +80,7 @@ const NavContainer = styled.div`
 `
 `
 
 
 const ContentContainer = styled.div`
 const ContentContainer = styled.div`
+  width: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   padding-left: 30px;
   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 './UploadFiles'
 export * from './VideoMetaData'
 export * from './VideoMetaData'
 export * from './PlaygroundCommonStore'
 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>
           </FormField>
           {isEdit && (
           {isEdit && (
             <DeleteVideoContainer>
             <DeleteVideoContainer>
-              <DeleteVideoButton size="large" variant="tertiary" textColorVariant="error" onClick={handleDeleteVideo}>
+              <DeleteVideoButton size="large" variant="destructive-secondary" onClick={handleDeleteVideo}>
                 Delete video
                 Delete video
               </DeleteVideoButton>
               </DeleteVideoButton>
             </DeleteVideoContainer>
             </DeleteVideoContainer>

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

@@ -270,9 +270,18 @@ export const MyVideosView = () => {
               <StyledDismissibleMessage
               <StyledDismissibleMessage
                 id="video-draft-saved-locally-warning"
                 id="video-draft-saved-locally-warning"
                 title="Video drafts are saved locally"
                 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."
                 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}>
             <Grid maxColumns={null} onResize={handleOnResizeGrid}>
               {gridContent}
               {gridContent}
             </Grid>
             </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:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
     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:
 lower-case@2.0.1, lower-case@^2.0.1:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7"
   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"
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
   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:
 react-number-format@^4.4.4:
   version "4.4.4"
   version "4.4.4"
   resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-4.4.4.tgz#2a7f50be404f990ec15855cc6babfeae1be16351"
   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