diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index dc8fb47ca3..b91f1f1a07 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -34,8 +34,6 @@ jobs:
build:
needs: deploy-context
env:
- FR_BASE_URL: ${{ needs.deploy-context.outputs.fr_url }}
- EN_BASE_URL: ${{ needs.deploy-context.outputs.en_url }}
VITE_FR_BASE_URL: ${{ needs.deploy-context.outputs.fr_url }}
VITE_EN_BASE_URL: ${{ needs.deploy-context.outputs.en_url }}
runs-on: ubuntu-18.04
@@ -65,8 +63,8 @@ jobs:
yarn workspace site build:prerender
- name: Replace site placeholders in netlify.toml redirection file
run: |
- sed -i "s|:SITE_FR|$FR_BASE_URL|g" site/netlify.toml;
- sed -i "s|:SITE_EN|$EN_BASE_URL|g" site/netlify.toml;
+ sed -i "s|:SITE_FR|$VITE_FR_BASE_URL|g" site/netlify.toml;
+ sed -i "s|:SITE_EN|$VITE_EN_BASE_URL|g" site/netlify.toml;
- name: Update Algolia index
run: yarn workspace site algolia:update
env:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 22d7d483da..73acd9c8cb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,10 +23,10 @@ Nous utilisons :
- [React](https://reactjs.org) pour la gestion de l'interface utilisateur
- [Redux](https://redux.js.org) pour gérer le “state” de l'application côté client
- [Prettier](https://prettier.io/) pour formater le code source, l'idéal est de configurer votre éditeur de texte pour que les fichiers soit formatés automatiquement quand vous sauvegardez un fichier. Si vous utilisez [VS Code](https://code.visualstudio.com/) cette configuration est automatique.
-- [Webpack](https://webpack.js.org) pour le “bundling”
+- [ViteJS](https://vitejs.dev) pour le “bundling” et le serveur de développement
- [Eslint](http://eslint.org) qui permet par exemple d'éviter de garder des variables inutilisées
- [Ramda](https://ramdajs.com) comme libraire d'utilitaires pour manipuler les listes/objects/etc (c'est une alternative à lodash ou underscore)
-- [Mocha](https://mochajs.org), [Jest](https://jestjs.io) et [Cypress](https://www.cypress.io) pour les l'execution des tests. Plus d'informations dans la section consacrée aux tests.
+- [Vitest](https://vitest.dev) et [Cypress](https://www.cypress.io) pour les l'execution des tests. Plus d'informations dans la section consacrée aux tests.
### Démarrage
@@ -121,10 +121,10 @@ yarn test
#### Tests de non-regression (snapshots)
```sh
-yarn test:regressions
+yarn test regressions
```
-Si vous souhaitez mettre à jour les snapshots vous pouvez utiliser le paramètre `--updateSnapshot`, son raccourci `-u`, ou encore le [mode interactif](https://jestjs.io/docs/en/snapshot-testing#interactive-snapshot-mode).
+Si vous souhaitez mettre à jour les snapshots vous pouvez utiliser le paramètre `--update`, son raccourci `-u`.
#### Tests d'integrations
diff --git a/site/package.json b/site/package.json
index 53e9ca8ec3..cb4bebc0f3 100644
--- a/site/package.json
+++ b/site/package.json
@@ -61,7 +61,6 @@
"@react-stately/radio": "^3.3.2",
"@react-stately/searchfield": "^3.1.3",
"@react-stately/toggle": "^3.2.3",
- "@rehooks/local-storage": "2.4.0",
"@sentry/react": "^6.3.5",
"@sentry/tracing": "^6.3.5",
"algoliasearch": "^4.10.2",
@@ -93,7 +92,6 @@
"regenerator-runtime": "^0.13.3",
"reselect": "^4.0.0",
"styled-components": "^5.3.1",
- "swr": "^0.1.16",
"whatwg-fetch": "^3.0.0",
"yaml": "^1.9.2"
},
diff --git a/site/prerender.cjs b/site/prerender.cjs
index 2a5d0e6019..ad39be3615 100644
--- a/site/prerender.cjs
+++ b/site/prerender.cjs
@@ -45,6 +45,8 @@ const templates = Object.fromEntries(
)
})()
+const htmlBodyStart = ''
+const htmlBodyEnd = ''
const headTagsStart = ''
const headTagsEnd = ''
@@ -53,7 +55,7 @@ async function prerenderUrl(url, site) {
// TODO: Add CI test to enforce meta tags on SSR pages
const { html, styleTags, helmet } = await render(url, lang)
const page = templates[site]
- .replace('', html)
+ .replace(new RegExp(htmlBodyStart + '[\\s\\S]+' + htmlBodyEnd, 'm'), html)
.replace('', styleTags)
.replace(
new RegExp(headTagsStart + '[\\s\\S]+' + headTagsEnd, 'm'),
diff --git a/site/source/Provider.tsx b/site/source/Provider.tsx
index f83e393738..edccc21457 100644
--- a/site/source/Provider.tsx
+++ b/site/source/Provider.tsx
@@ -31,7 +31,7 @@ import {
// ATInternet Tracking
import { TrackingContext } from './ATInternetTracking'
import { createTracker } from './ATInternetTracking/Tracker'
-import safeLocalStorage from './storage/safeLocalStorage'
+import * as safeLocalStorage from './storage/safeLocalStorage'
import { inIframe } from './utils'
if (
diff --git a/site/source/components/Feedback/index.tsx b/site/source/components/Feedback/index.tsx
index 81d89c51af..d91d5d1295 100644
--- a/site/source/components/Feedback/index.tsx
+++ b/site/source/components/Feedback/index.tsx
@@ -10,7 +10,7 @@ import { Trans } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
import { TrackingContext } from '../../ATInternetTracking'
-import safeLocalStorage from '../../storage/safeLocalStorage'
+import * as safeLocalStorage from '../../storage/safeLocalStorage'
import { INSCRIPTION_LINK } from '../layout/Footer/InscriptionBetaTesteur'
import './Feedback.css'
import Form from './FeedbackForm'
diff --git a/site/source/components/layout/Footer/Privacy.tsx b/site/source/components/layout/Footer/Privacy.tsx
index 76f44318d8..85d4cbe53e 100644
--- a/site/source/components/layout/Footer/Privacy.tsx
+++ b/site/source/components/layout/Footer/Privacy.tsx
@@ -5,7 +5,7 @@ import { Body, SmallBody } from 'DesignSystem/typography/paragraphs'
import { useCallback, useContext, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { TrackingContext, TrackPage } from '../../../ATInternetTracking'
-import safeLocalStorage from '../../../storage/safeLocalStorage'
+import * as safeLocalStorage from '../../../storage/safeLocalStorage'
export default function Privacy({ label }: { label?: string }) {
const tracker = useContext(TrackingContext)
diff --git a/site/source/components/layout/Header.tsx b/site/source/components/layout/Header.tsx
index 2046f1725b..19c69e60d8 100644
--- a/site/source/components/layout/Header.tsx
+++ b/site/source/components/layout/Header.tsx
@@ -1,5 +1,6 @@
import { Logo } from 'Components/Logo'
import SearchButton from 'Components/SearchButton'
+import BrowserOnly from 'Components/utils/BrowserOnly'
import { SitePathsContext } from 'Components/utils/SitePathsContext'
import { Container } from 'DesignSystem/layout'
import { Link } from 'DesignSystem/typography/link'
@@ -28,8 +29,9 @@ export default function Header() {
/>
{language === 'fr' && }
-
- {!import.meta.env.SSR && }
+
+
+
)
}
diff --git a/site/source/components/layout/NewsBanner.tsx b/site/source/components/layout/NewsBanner.tsx
index 1662d77dcf..a9b5c544d9 100644
--- a/site/source/components/layout/NewsBanner.tsx
+++ b/site/source/components/layout/NewsBanner.tsx
@@ -1,56 +1,59 @@
-// import { useLocalStorage, writeStorage } from '@rehooks/local-storage'
import { Appear } from 'Components/ui/animate'
import Emoji from 'Components/utils/Emoji'
import { SitePathsContext } from 'Components/utils/SitePathsContext'
import lastRelease from 'Data/last-release.json'
import { Banner, HideButton, InnerBanner } from 'DesignSystem/banner'
import { Link } from 'DesignSystem/typography/link'
-import { useContext, useEffect } from 'react'
+import { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { getItem, setItem } from '../../storage/safeLocalStorage'
const localStorageKey = 'last-viewed-release'
export const hideNewsBanner = () => {
- // writeStorage(localStorageKey, lastRelease.name)
+ setItem(localStorageKey, lastRelease.name)
}
+
export const determinant = (word: string) =>
/^[aeiouy]/i.exec(word) ? 'd’' : 'de '
export default function NewsBanner() {
- // const [lastViewedRelease] = useLocalStorage(localStorageKey)
const sitePaths = useContext(SitePathsContext)
- const { i18n, t } = useTranslation()
+ const { t } = useTranslation()
+ const lastViewedRelease = getItem(localStorageKey)
+ const [showBanner, setShowBanner] = useState(
+ lastViewedRelease && lastViewedRelease !== lastRelease.name
+ )
// We only want to show the banner to returning visitors, so we initiate the
// local storage value with the last release.
useEffect(() => {
- // writeStorage(
- // localStorageKey,
- // lastViewedRelease === undefined ? lastRelease.name : lastViewedRelease
- // )
+ setItem(
+ localStorageKey,
+ lastViewedRelease == undefined ? lastRelease.name : lastViewedRelease
+ )
}, [])
- const showBanner = false
-
if (!showBanner) {
return null
}
return (
-
-
-
-
- Découvrez les nouveautés{' '}
- {determinant(lastRelease.name)}
-
- {lastRelease.name.toLowerCase()}
-
-
-
- ×
-
-
-
-
+ setShowBanner(false)}>
+
+
+ Découvrez les nouveautés{' '}
+ {determinant(lastRelease.name)}
+
+ {lastRelease.name.toLowerCase()}
+
+
+ setShowBanner(false)}
+ aria-label={t('Fermer')}
+ >
+ ×
+
+
+
)
}
diff --git a/site/source/components/utils/BrowserOnly.tsx b/site/source/components/utils/BrowserOnly.tsx
new file mode 100644
index 0000000000..19182b4119
--- /dev/null
+++ b/site/source/components/utils/BrowserOnly.tsx
@@ -0,0 +1,15 @@
+import { Appear } from 'Components/ui/animate'
+import React from 'react'
+
+// We add a animation for all coponents displayed on the client only but not on
+// the SSR to avoid augment the CLS (Cumulative Layout Shift).
+export default function BrowserOnly({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ if (import.meta.env.SSR) {
+ return null
+ }
+ return {children}
+}
diff --git a/site/source/components/utils/persistState.ts b/site/source/components/utils/persistState.ts
index d8e174aebb..37b07c0d7f 100644
--- a/site/source/components/utils/persistState.ts
+++ b/site/source/components/utils/persistState.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
-import safeLocalStorage from '../../storage/safeLocalStorage'
+import * as safeLocalStorage from '../../storage/safeLocalStorage'
export const getInitialState = (key: string) => {
const value = safeLocalStorage.getItem(key)
diff --git a/site/source/locales/ui-en.yaml b/site/source/locales/ui-en.yaml
index d4985f10d2..e940774a56 100644
--- a/site/source/locales/ui-en.yaml
+++ b/site/source/locales/ui-en.yaml
@@ -1675,17 +1675,10 @@ replier: show less
responsabilité:
bouton1: Limited liability company
bouton2: Sole proprietorship
- description: "
-
- \t\t\t\t\t\tLimited liability? Sole proprietorship? Each option has
-
- \t\t\t\t\t\tlegal implications and leads to a different status for the
-
- \t\t\t\t\t\tyour business in France. This guide helps you choose
-
- \t\t\t\t\t\tbetween the different forms of liability.
-
- \t\t\t\t\t"
+ description: Limited liability? Sole proprietorship? Each option has legal
+ implications and leads to a different status for the creation of your
+ company in France. This guide helps you choose between the different forms
+ of liability.
entreprise-individuelle: >
<0>Sole proprietorship:0>
diff --git a/site/source/locales/ui-fr.yaml b/site/source/locales/ui-fr.yaml
index b6409b8e54..ca574f9c59 100644
--- a/site/source/locales/ui-fr.yaml
+++ b/site/source/locales/ui-fr.yaml
@@ -1313,20 +1313,10 @@ recherche-globale:
responsabilité:
bouton1: Société
bouton2: Entreprise individuelle
- description: "
-
- \t\t\t\t\t\tResponsabilité limitée ? entreprise individuelle ? Chaque
- option a
-
- \t\t\t\t\t\tdes implications juridiques et conduit à un statut différent
- pour la
-
- \t\t\t\t\t\tcréation de votre entreprise en France. Ce guide vous aide à
- choisir
-
- \t\t\t\t\t\tentre les différentes forme de responsabilité.
-
- \t\t\t\t\t"
+ description: Responsabilité limitée ? entreprise individuelle ? Chaque option a
+ des implications juridiques et conduit à un statut différent pour la
+ création de votre entreprise en France. Ce guide vous aide à choisir entre
+ les différentes forme de responsabilité.
entreprise-individuelle: "<0>Entreprise individuelle : 0>Une activité
économique exercée par une seule personne physique, en son nom propre. Moins
de formalités, mais plus de risques en cas de faillite, car votre patrimoine
diff --git a/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx b/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx
index 5ee12618b6..0c6967c2c4 100644
--- a/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx
+++ b/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx
@@ -28,12 +28,7 @@ export default function SoleProprietorship() {
name="description"
content={t(
'responsabilité.description',
- `
- Responsabilité limitée ? entreprise individuelle ? Chaque option a
- des implications juridiques et conduit à un statut différent pour la
- création de votre entreprise en France. Ce guide vous aide à choisir
- entre les différentes forme de responsabilité.
- `
+ 'Responsabilité limitée ? entreprise individuelle ? Chaque option a des implications juridiques et conduit à un statut différent pour la création de votre entreprise en France. Ce guide vous aide à choisir entre les différentes forme de responsabilité.'
)}
/>
diff --git a/site/source/pages/Gerer/DemandeMobilite/index.tsx b/site/source/pages/Gerer/DemandeMobilite/index.tsx
index 00b20d00c1..1123770113 100644
--- a/site/source/pages/Gerer/DemandeMobilite/index.tsx
+++ b/site/source/pages/Gerer/DemandeMobilite/index.tsx
@@ -2,6 +2,7 @@ import { Grid } from '@mui/material'
import RuleInput from 'Components/conversation/RuleInput'
import { WhenApplicable, WhenNotApplicable } from 'Components/EngineValue'
import PageHeader from 'Components/PageHeader'
+import BrowserOnly from 'Components/utils/BrowserOnly'
import Emoji from 'Components/utils/Emoji'
import { EngineContext, EngineProvider } from 'Components/utils/EngineContext'
import { Markdown } from 'Components/utils/markdown'
@@ -175,11 +176,11 @@ function FormulairePublicodes() {
- {!import.meta.env.SSR && (
+
- )}
+
{!!Object.keys(situation).length && (
diff --git a/site/source/pages/Nouveautes/Nouveautes.tsx b/site/source/pages/Nouveautes/Nouveautes.tsx
index e7a04471d6..db2cee1aca 100644
--- a/site/source/pages/Nouveautes/Nouveautes.tsx
+++ b/site/source/pages/Nouveautes/Nouveautes.tsx
@@ -11,13 +11,11 @@ import { Container } from 'DesignSystem/layout'
import { H1 } from 'DesignSystem/typography/heading'
import { GenericButtonOrLinkProps, Link } from 'DesignSystem/typography/link'
import { Body } from 'DesignSystem/typography/paragraphs'
-import { useContext, useEffect, useMemo } from 'react'
+import { useContext, useEffect, useMemo, useState } from 'react'
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom'
import styled from 'styled-components'
-import useSWR from 'swr'
import { TrackPage } from '../../ATInternetTracking'
-const fetcher = (url: RequestInfo) => fetch(url).then((r) => r.json())
const slugify = (name: string) => name.toLowerCase().replace(' ', '-')
type ReleasesData = Array<{
@@ -27,9 +25,13 @@ type ReleasesData = Array<{
export default function Nouveautés() {
// The release.json file may be big, we don't want to include it in the main
- // bundle, that's why we only fetch it on this page. Alternatively we could
- // use import("data/release.json") and configure code splitting with Webpack.
- const { data } = useSWR('/data/releases.json', fetcher)
+ // bundle, that's why we only fetch it on this page.
+ const [data, setData] = useState([])
+ useEffect(() => {
+ import('Data/releases.json').then(({ default: data }) => {
+ setData(data)
+ })
+ }, [])
const history = useHistory()
const sitePaths = useContext(SitePathsContext)
const slug = useRouteMatch<{ slug: string }>(`${sitePaths.nouveautés}/:slug`)
@@ -41,7 +43,7 @@ export default function Nouveautés() {
[data]
)
- if (!data) {
+ if (data.length === 0) {
return null
}
@@ -55,7 +57,6 @@ export default function Nouveautés() {
}
const releaseName = data[selectedRelease].name.toLowerCase()
-
return (
<>
diff --git "a/site/source/pages/Simulateurs/Salari\303\251.tsx" "b/site/source/pages/Simulateurs/Salari\303\251.tsx"
index 4ae4d4a445..0c36172c35 100644
--- "a/site/source/pages/Simulateurs/Salari\303\251.tsx"
+++ "b/site/source/pages/Simulateurs/Salari\303\251.tsx"
@@ -20,6 +20,7 @@ import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { targetUnitSelector } from 'Selectors/simulationSelectors'
import styled from 'styled-components'
+import BrowserOnly from 'Components/utils/BrowserOnly'
const ButtonContainer = styled.div`
margin: 2rem 1rem;
@@ -47,19 +48,21 @@ export default function SalariéSimulation() {
}
>
- {/** L'équipe Code Du Travail Numérique ne souhaite pas référencer
- * le simulateur dirigeant de SASU sur son site. */}
- {!import.meta.env.SSR &&
- !document.referrer?.includes('code.travail.gouv.fr') && (
-
-
- Vous êtes dirigeant d'une SAS(U) ?{' '}
-
- Accéder au simulateur de revenu dédié
-
-
-
- )}
+
+ {/** L'équipe Code Du Travail Numérique ne souhaite pas référencer
+ * le simulateur dirigeant de SASU sur son site. */}
+ {!import.meta.env.SSR &&
+ !document.referrer?.includes('code.travail.gouv.fr') && (
+
+
+ Vous êtes dirigeant d'une SAS(U) ?{' '}
+
+ Accéder au simulateur de revenu dédié
+
+
+
+ )}
+
>
)
diff --git a/site/source/storage/persistInFranceApp.ts b/site/source/storage/persistInFranceApp.ts
index c10ad500b9..4fc97c4597 100644
--- a/site/source/storage/persistInFranceApp.ts
+++ b/site/source/storage/persistInFranceApp.ts
@@ -3,7 +3,7 @@ import { InFranceAppState } from 'Reducers/inFranceAppReducer'
import { RootState } from 'Reducers/rootReducer'
import { Store } from 'redux'
import { debounce } from '../utils'
-import safeLocalStorage from './safeLocalStorage'
+import * as safeLocalStorage from './safeLocalStorage'
const VERSION = 7
diff --git a/site/source/storage/persistSimulation.ts b/site/source/storage/persistSimulation.ts
index 5c2007ff6d..2c0b9bdd0a 100644
--- a/site/source/storage/persistSimulation.ts
+++ b/site/source/storage/persistSimulation.ts
@@ -3,7 +3,7 @@ import { RootState } from 'Reducers/rootReducer'
import { Store } from 'redux'
import { PreviousSimulation } from 'Selectors/previousSimulationSelectors'
import { debounce } from '../utils'
-import safeLocalStorage from './safeLocalStorage'
+import * as safeLocalStorage from './safeLocalStorage'
import { deserialize, serialize } from './serializeSimulation'
const VERSION = 5
diff --git a/site/source/storage/safeLocalStorage.ts b/site/source/storage/safeLocalStorage.ts
index 0c9e795c58..43bbcc57a7 100644
--- a/site/source/storage/safeLocalStorage.ts
+++ b/site/source/storage/safeLocalStorage.ts
@@ -1,41 +1,35 @@
-export default {
- removeItem: function (key: string) {
- try {
- return window.localStorage.removeItem(key)
- } catch (error) {
- if (error instanceof Error && error.name === 'SecurityError') {
- // eslint-disable-next-line no-console
- console.warn(
- '[localStorage] Unable to remove item due to security settings'
- )
- }
- return null
+export function removeItem(key: string) {
+ try {
+ return window.localStorage.removeItem(key)
+ } catch (error) {
+ if (error instanceof Error && error.name === 'SecurityError') {
+ // eslint-disable-next-line no-console
+ console.warn(
+ '[localStorage] Unable to remove item due to security settings'
+ )
}
- },
- getItem: function (key: string) {
- try {
- return window.localStorage.getItem(key)
- } catch (error) {
- if (error instanceof Error && error.name === 'SecurityError') {
- // eslint-disable-next-line no-console
- console.warn(
- '[localStorage] Unable to get item due to security settings'
- )
- }
- return null
+ return null
+ }
+}
+export function getItem(key: string) {
+ try {
+ return window.localStorage.getItem(key)
+ } catch (error) {
+ if (error instanceof Error && error.name === 'SecurityError') {
+ // eslint-disable-next-line no-console
+ console.warn('[localStorage] Unable to get item due to security settings')
}
- },
- setItem: function (key: string, value: string) {
- try {
- return window.localStorage.setItem(key, value)
- } catch (error) {
- if (error instanceof Error && error.name === 'SecurityError') {
- // eslint-disable-next-line no-console
- console.warn(
- '[localStorage] Unable to set item due to security settings'
- )
- }
- return null
+ return null
+ }
+}
+export function setItem(key: string, value: string) {
+ try {
+ return window.localStorage.setItem(key, value)
+ } catch (error) {
+ if (error instanceof Error && error.name === 'SecurityError') {
+ // eslint-disable-next-line no-console
+ console.warn('[localStorage] Unable to set item due to security settings')
}
- },
+ return null
+ }
}
diff --git a/site/source/template.html b/site/source/template.html
index dbcd70c477..495581e406 100644
--- a/site/source/template.html
+++ b/site/source/template.html
@@ -65,13 +65,128 @@
html[data-useragent*='Trident'] #js {
display: none !important;
}
+
+ /* Prevent FOUC effect */
+ #js {
+ opacity: 0;
+ }
+
+ /* CSS Loader */
+
+ #loading {
+ animation: appear 0.6s;
+ transform: translateY(35vh);
+ width: 100%;
+ }
+ #lds-ellipsis {
+ margin: auto;
+ position: relative;
+ width: 64px;
+ animation: appear 1.5s;
+ height: 64px;
+ }
+ #lds-ellipsis div {
+ position: absolute;
+ top: 27px;
+ width: 11px;
+ height: 11px;
+ border-radius: 50%;
+ background: rgb(41, 117, 209);
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
+ }
+
+ #lds-ellipsis div:nth-child(1) {
+ left: 6px;
+ animation: lds-ellipsis1 0.6s infinite;
+ }
+
+ #lds-ellipsis div:nth-child(2) {
+ left: 6px;
+ animation: lds-ellipsis2 0.6s infinite;
+ }
+
+ #lds-ellipsis div:nth-child(3) {
+ left: 26px;
+ animation: lds-ellipsis2 0.6s infinite;
+ }
+
+ #lds-ellipsis div:nth-child(4) {
+ left: 45px;
+ animation: lds-ellipsis3 0.6s infinite;
+ }
+
+ @keyframes appear {
+ from {
+ opacity: 0;
+ }
+ 80% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+ }
+
+ @keyframes lds-ellipsis1 {
+ 0% {
+ transform: scale(0);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+
+ @keyframes lds-ellipsis3 {
+ 0% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(0);
+ }
+ }
+
+ @keyframes lds-ellipsis2 {
+ 0% {
+ transform: translate(0, 0);
+ }
+ 100% {
+ transform: translate(19px, 0);
+ }
+ }
-
+
+
+
+
+
+
+
+
+
+
+