diff --git a/.github/workflows/auto-deploy.yaml b/.github/workflows/auto-deploy.yaml index 9f67e4fd6..b7fc8518b 100644 --- a/.github/workflows/auto-deploy.yaml +++ b/.github/workflows/auto-deploy.yaml @@ -8,7 +8,7 @@ jobs: build-and-push: if: github.event.pull_request.merged == true name: Build and Push Docker Image - runs-on: ubuntu-latest + runs-on: self-hosted permissions: contents: write pull-requests: write @@ -80,6 +80,7 @@ jobs: build-args: | NUXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ secrets.NUXT_PUBLIC_RECAPTCHA_SITE_KEY }} NUXT_PUBLIC_DM_WS_URL=${{ steps.image.outputs.dm_ws_url }} + RAVEN_TENOR_KEY=${{ secrets.RAVEN_TENOR_KEY }} tags: | "${{ steps.image.outputs.registry }}:${{ steps.image.outputs.tag }}" "${{ steps.image.outputs.registry }}:${{ steps.image.outputs.latest_tag }}" diff --git a/.github/workflows/code-quality-workflow.yaml b/.github/workflows/code-quality-workflow.yaml index 72956e8b7..8f079e007 100644 --- a/.github/workflows/code-quality-workflow.yaml +++ b/.github/workflows/code-quality-workflow.yaml @@ -13,17 +13,17 @@ jobs: style-quality: if: github.event.pull_request.draft == false name: Code Style & Quality - runs-on: ubuntu-latest + runs-on: self-hosted steps: - uses: actions/checkout@v4 - - name: Set up Corepack - run: corepack enable - - name: Set up pnpm run: corepack prepare pnpm@latest --activate + - name: Add pnpm to PATH + run: echo "/home/btngana/.local/share/pnpm" >> $GITHUB_PATH + - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.github/workflows/e2e-workflow.yaml b/.github/workflows/e2e-workflow.yaml index 7568ab362..b68042970 100644 --- a/.github/workflows/e2e-workflow.yaml +++ b/.github/workflows/e2e-workflow.yaml @@ -7,7 +7,7 @@ on: jobs: e2e-tests: - runs-on: ubuntu-latest + runs-on: self-hosted environment: e2e env: APP_POSTGRES_USER: ${{ secrets.POSTGRES_USER }} @@ -46,6 +46,13 @@ jobs: APP_SEED_ENV: ${{ secrets.SEED_ENV }} APP_BACKEND_URL: ${{ secrets.BACKEND_URL }} APP_CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} + APP_CLASSIFICATION_INTERVAL_MINUTES: ${{ secrets.CLASSIFICATION_INTERVAL_MINUTES }} + APP_CLASSIFY_REQ_LIMIT: ${{ secrets.CLASSIFY_REQ_LIMIT }} + APP_CLASSIFICATION_API_URL: ${{ secrets.CLASSIFICATION_API_URL }} + APP_CLASSIFY_TWEETS: ${{ secrets.CLASSIFY_TWEETS }} + APP_SUMMARY_API_KEY: ${{ secrets.SUMMARY_API_KEY }} + APP_RAVEN_TENOR_KEY: ${{ secrets.RAVEN_TENOR_KEY }} + APP_NUXT_PUBLIC_DM_WS_URL: ${{ secrets.NUXT_PUBLIC_DM_WS_URL_DEV }} APP_NUXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ secrets.NUXT_PUBLIC_GOOGLE_CLIENT_ID }} APP_NUXT_PUBLIC_GITHUB_CLIENT_ID: ${{ secrets.NUXT_PUBLIC_GITHUB_CLIENT_ID }} APP_NUXT_PUBLIC_GITHUB_REDIRECT_URI: ${{ secrets.NUXT_PUBLIC_GITHUB_REDIRECT_URI }} @@ -62,8 +69,8 @@ jobs: run: | printenv | grep '^APP_' | sed 's/^APP_//' > .env - - name: Set up Corepack - run: corepack enable + # - name: Set up Corepack + # run: corepack enable - name: Set up pnpm run: corepack prepare pnpm@latest --activate diff --git a/.github/workflows/pr-validation-workflow.yaml b/.github/workflows/pr-validation-workflow.yaml index aecf8549d..1454167da 100644 --- a/.github/workflows/pr-validation-workflow.yaml +++ b/.github/workflows/pr-validation-workflow.yaml @@ -12,7 +12,7 @@ permissions: jobs: validate-merge-permissions: if: github.event.pull_request.base.ref == 'main' - runs-on: ubuntu-latest + runs-on: self-hosted name: Validate Merge Permissions steps: - name: Verify Authorized User @@ -31,7 +31,7 @@ jobs: branch-name-validation: if: github.event.pull_request.base.ref == 'dev' name: Validate branch naming convention - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Enforce kebab-case branch name @@ -49,7 +49,7 @@ jobs: validate-pr-title: name: Validate PR title format - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Check PR title format diff --git a/.github/workflows/testers-checklist.yaml b/.github/workflows/testers-checklist.yaml index 1ff3768f4..4b18665e6 100644 --- a/.github/workflows/testers-checklist.yaml +++ b/.github/workflows/testers-checklist.yaml @@ -7,7 +7,7 @@ on: jobs: checklist: if: github.event.pull_request.draft == false - runs-on: ubuntu-latest + runs-on: self-hosted permissions: pull-requests: write diff --git a/Dockerfile b/Dockerfile index 2afb2655c..a057b518b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,11 @@ ENV NODE_ENV=development ARG NUXT_PUBLIC_RECAPTCHA_SITE_KEY ARG NUXT_PUBLIC_DM_WS_URL +ARG RAVEN_TENOR_KEY ENV NUXT_PUBLIC_RECAPTCHA_SITE_KEY=$NUXT_PUBLIC_RECAPTCHA_SITE_KEY ENV NUXT_PUBLIC_DM_WS_URL=$NUXT_PUBLIC_DM_WS_URL +ENV RAVEN_TENOR_KEY=$RAVEN_TENOR_KEY # Install dependencies COPY package.json pnpm-lock.yaml ./ diff --git a/README.md b/README.md index 25b58212c..eed8fc20c 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,293 @@ -# Nuxt Minimal Starter +
+ Raven Logo -Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. +# Raven Web App -## Setup +**Caw Your Thoughts** -Make sure to install dependencies: +A modern, responsive web application for the Raven social media platform built with Nuxt 4 and Vue 3. -```bash -# npm -npm install +[![Nuxt](https://img.shields.io/badge/Nuxt-4.1-00DC82?logo=nuxtdotjs)](https://nuxt.com/) +[![Vue](https://img.shields.io/badge/Vue-3.5-4FC08D?logo=vuedotjs)](https://vuejs.org/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript)](https://www.typescriptlang.org/) +[![TailwindCSS](https://img.shields.io/badge/TailwindCSS-4.1-06B6D4?logo=tailwindcss)](https://tailwindcss.com/) -# pnpm -pnpm install +
-# yarn -yarn install +## Features -# bun -bun install -``` +### Authentication & Security -## Development Server +- **Email/Password Authentication** - Secure sign-up and sign-in with form validation +- **OAuth Integration** - Google and GitHub OAuth 2.0 authentication +- **Password Recovery** - Forgot password flow with email-based reset +- **Session Persistence** - Automatic token refresh and session management +- **reCAPTCHA Protection** - Bot prevention on authentication forms -Start the development server on `http://localhost:3000`: +### Timeline & Feed -```bash -# npm -npm run dev +- **Home Timeline** - Personalized feed with posts from followed users +- **Real-time Updates** - Live feed updates via Server-Sent Events (SSE) +- **Infinite Scrolling** - Smooth cursor-based pagination +- **Pull-to-Refresh** - Mobile-friendly feed refresh -# pnpm -pnpm dev +### Tweets/Posts -# yarn -yarn dev +- **Tweet Composer** - Rich text editor with: + - Hashtag highlighting and auto-linking + - User mention highlighting and auto-linking + - Native emoji picker integration + - GIF picker via Tenor API + - Media attachments (up to 4 images/videos per tweet) + - Character count with visual indicator +- **Quote Tweets** - Retweet with your own commentary +- **Reply Threads** - Nested conversation threads +- **Tweet Actions** - Like, retweet, quote, reply, and bookmark +- **Media Grid** - Responsive media display for images and videos +- **Video Playback** - Video.js player with full controls -# bun -bun run dev -``` +### Direct Messaging -## Production +- **Real-time Chat** - Instant messaging with Socket.IO +- **Message Reactions** - React to messages with emojis +- **Image Sharing** - Send and view images in conversations +- **Message Deletion** - Delete sent messages +- **Conversation List** - View all active conversations with unread indicators +- **Sound Notifications** - Audio alerts for new messages -Build the application for production: +### Explore & Discovery -```bash -# npm -npm run build +- **For You** - Algorithmic content discovery based on interests +- **Trending Topics** - Popular hashtags and keywords +- **Category Sections** - Browse by category (News, Sports, Entertainment) +- **User Discovery** - Discover new users to follow + +### Search + +- **Unified Search** - Search users, tweets, and hashtags +- **Tabbed Results** - Filtered view by content type +- **Recent Searches** - Quick access to search history +- **Real-time Suggestions** - Instant search suggestions + +### Profile Management + +- **Profile Customization** - Edit display name, bio, location, website +- **Profile Picture** - Upload and crop profile images +- **Header Image** - Custom profile banner +- **Following/Followers** - View and manage connections +- **Profile Tabs** - View tweets, replies, media, and likes +- **User Actions** - Follow, block, mute users + +### Bookmarks + +- **Save Tweets** - Bookmark tweets for later viewing +- **Bookmark Feed** - Dedicated page for saved tweets + +### Notifications + +- **Notification Feed** - Real-time notification updates +- **Notification Types** - Likes, retweets, mentions, follows, replies, quotes +- **Read/Unread Status** - Track notification status +- **Filter Options** - Filter by notification type + +### Settings & Privacy + +- **Appearance** - Theme customization (light/dark mode) +- **Account Settings** - Manage account information and email preferences +- **Privacy Controls**: + - Muted accounts management + - Blocked accounts management +- **Sessions** - View and manage active sessions +- **Interest Preferences** - Customize content recommendations + +### User Experience + +- **Dark Mode** - Manual theme switching with system preference detection +- **RTL Support** - Right-to-left language support for Arabic +- **Localization** - Multi-language support (English & Arabic) +- **Responsive Design** - Mobile-first responsive layouts +- **Toast Notifications** - Non-intrusive feedback messages +- **Skeleton Loading** - Smooth loading states +- **Virtual Scrolling** - Optimized performance for long lists + +## Tech Stack + +### Core + +- **Nuxt** 4.1 - Vue meta-framework with SSR support +- **Vue** 3.5 - Progressive JavaScript framework +- **TypeScript** 5.9 - Type-safe JavaScript + +### State Management + +- **Pinia** 3.0 - Intuitive Vue store +- **TanStack Query** (Vue Query) - Server state and caching + +### Styling + +- **TailwindCSS** 4.1 - Utility-first CSS framework +- **Tailwind Merge** - Merge Tailwind classes intelligently +- **Class Variance Authority** - Variant-based component styling +- **tw-animate-css** - CSS animation utilities + +### UI Components + +- **Reka UI** - Headless UI components +- **Lucide Icons** - Modern icon set +- **Vue Sonner** - Toast notifications +- **Vue Frimousse** - Emoji picker + +### Forms & Validation + +- **VeeValidate** - Form handling and validation +- **Yup / Zod** - Schema validation libraries + +### Real-time Communication + +- **Socket.IO Client** - WebSocket-based real-time messaging +- **Event Source Polyfill** - SSE support for older browsers + +### Media + +- **Nuxt Image** - Optimized image handling +- **Video.js** - Video player + +### Utilities + +- **VueUse** - Collection of Vue composition utilities +- **Lodash** - Utility functions +- **Embla Carousel** - Carousel component + +### Testing -# pnpm -pnpm build +- **Vitest** - Unit testing framework +- **Vue Test Utils** - Vue component testing +- **Testing Library** - DOM testing utilities +- **Cypress** - End-to-end testing +- **MSW** - API mocking for tests +- **Playwright** - Browser automation -# yarn -yarn build +## Getting Started -# bun -bun run build +### Prerequisites + +- Node.js 22+ +- pnpm + +### Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/AhmedAmrNabil/raven-frontend + cd raven-frontend + ``` + +2. Install dependencies: + + ```bash + pnpm install + ``` + +3. Set up environment variables: + + ```bash + cp .env.example .env + ``` + + Edit `.env` with your configuration values. + +4. Start the development server: + + ```bash + pnpm dev + ``` + + The application will be available at `http://localhost:3000`. + +## Scripts + +| Command | Description | +| -------------------- | ------------------------ | +| `pnpm dev` | Start development server | +| `pnpm build` | Build for production | +| `pnpm start` | Start production server | +| `pnpm generate` | Generate static site | +| `pnpm preview` | Preview production build | +| `pnpm test` | Run unit tests | +| `pnpm test:cov` | Run tests with coverage | +| `pnpm test:e2e` | Run Cypress E2E tests | +| `pnpm test:e2e:open` | Open Cypress test runner | +| `pnpm mock:gen` | Generate mock data | + +## Docker + +Build the Docker image: + +```bash +docker build -t raven-frontend . ``` -Locally preview production build: +Run with Docker Compose: ```bash -# npm -npm run preview +docker-compose up +``` -# pnpm -pnpm preview +## Configuration -# yarn -yarn preview +### Environment Variables -# bun -bun run preview -``` +Key environment variables (see `.env.example` for full list): + +| Variable | Description | +| --------------------------------- | ------------------------------ | +| `BACKEND_URL` | Backend API URL | +| `NUXT_PUBLIC_USE_MOCKS` | Enable mock data mode | +| `NUXT_PUBLIC_GOOGLE_CLIENT_ID` | Google OAuth client ID | +| `NUXT_PUBLIC_GITHUB_CLIENT_ID` | GitHub OAuth client ID | +| `NUXT_PUBLIC_GITHUB_REDIRECT_URI` | GitHub OAuth callback URL | +| `NUXT_PUBLIC_GOOGLE_REDIRECT_URI` | Google OAuth callback URL | +| `NUXT_PUBLIC_RECAPTCHA_SITE_KEY` | reCAPTCHA site key | +| `NUXT_PUBLIC_BASE_URL` | Frontend base URL | +| `NUXT_PUBLIC_DM_WS_URL` | Direct messaging WebSocket URL | +| `RAVEN_TENOR_KEY` | Tenor API key for GIF picker | + +## Licenses + +### Fonts + +- **Inter** - The app uses the [Inter font family](https://fonts.google.com/specimen/Inter) by Rasmus Andersson, licensed under the [SIL Open Font License 1.1](https://scripts.sil.org/OFL). Loaded via `@nuxt/fonts` from Google Fonts. + +### Icons + +- **Lucide Icons** - Licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). +- **Material Design Icons (ic)** - Licensed under the [Apache License 2.0](https://github.com/Templarian/MaterialDesign/blob/master/LICENSE). + +### Media + +- **Sound Effects** - Notification sound in `public/sounds/`. +- **App Images** - App logo and favicon are original assets created for this project. +- **User-uploaded Content** - Subject to the platform's terms of service. + +### Third-Party Libraries + +All third-party dependencies are listed in `package.json` with their respective licenses. Key libraries: + +- **Video.js** - Licensed under the [Apache License 2.0](https://github.com/videojs/video.js/blob/main/LICENSE). +- **Reka UI** - Licensed under the [MIT License](https://github.com/unovue/reka-ui/blob/main/LICENSE). +- **VueUse** - Licensed under the [MIT License](https://github.com/vueuse/vueuse/blob/main/LICENSE). + +## Code Quality + +The project enforces code quality with: + +- **ESLint** - Linting with Nuxt and TypeScript rules +- **Prettier** - Code formatting with Tailwind plugin +- **Husky** - Git hooks for pre-commit checks +- **lint-staged** - Run linters on staged files +- **TypeScript** - Strict type checking + +## License -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. +This project is licensed under the [MIT License](LICENSE). diff --git a/app/app.vue b/app/app.vue index 7b31816a4..64d7b7f3f 100644 --- a/app/app.vue +++ b/app/app.vue @@ -10,7 +10,6 @@ useHead(() => ({ htmlAttrs: { lang: locale.value, dir: localeProperties.value.dir, - // class: 'dark', }, })); @@ -38,9 +37,9 @@ provide('unseenNotificationsCount', unseenNotificationsCount); watch( () => userStore.user, (newUser, oldUser) => { - if (newUser.username && !oldUser?.username) { + if (newUser?.username && !oldUser?.username) { connectDmSse(); - } else if (!newUser.username && oldUser?.username) { + } else if (!newUser?.username && oldUser?.username) { disconnectDmSse(); } }, diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 029ec43db..f9ff8a603 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -23,14 +23,32 @@ --dialog-backdrop: rgba(91, 112, 131, 0.4); --input-autofill: #e8f0fe; --oauth: #ffffff; - --toaster-bg-success: #dcfce7; - --toaster-text-success: #166534; - --toaster-bg-error: #fee2e2; - --toaster-text-error: #991b1b; - --toaster-bg-warning: #fef9c3; - --toaster-text-warning: #92400e; - --toaster-bg-info: #dbeafe; + --toaster-bg: #ffffff; + --toaster-text-success: #1cb356; + --toaster-text-error: #c22828; + --toaster-text-warning: #caa016; --toaster-text-info: #1e3a8a; + --radius: 0.625rem; + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { @@ -53,14 +71,31 @@ --popover-foreground: #e7e9ea; --input-autofill: #202939; --oauth: #ffffff; - --toaster-bg-success: #064e3b; - --toaster-text-success: #86efac; - --toaster-bg-error: #7f1d1d; - --toaster-text-error: #fecaca; - --toaster-bg-warning: #78350f; + --toaster-bg: #111111; + --toaster-text-success: #18f16b; + --toaster-text-error: #ff5252; --toaster-text-warning: #fcd34d; - --toaster-bg-info: #1e3a8a; - --toaster-text-info: #bfdbfe; + --toaster-text-info: #4197ff; + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } @theme inline { @@ -95,6 +130,32 @@ --color-toaster-text-warning: var(--toaster-text-warning); --color-toaster-bg-info: var(--toaster-bg-info); --color-toaster-text-info: var(--toaster-text-info); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-accent-foreground: var(--accent-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --breakpoint-midd: 1080px; + --width-8xl: 82rem; } @layer base { @@ -134,4 +195,95 @@ outline: none !important; box-shadow: none !important; } + + [data-sonner-toast] { + position: relative; + display: flex; + align-items: center; + gap: 12px; + + padding: 14px 18px; + border-radius: 8px; + + background: var(--toaster-bg); + + color: var(--foreground); + font-size: 14px; + font-weight: 500; + + box-shadow: + 0 20px 25px -5px rgb(0 0 0 / 0.15), + 0 8px 10px -6px rgb(0 0 0 / 0.1); + + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + + animation: toast-slide-in 180ms cubic-bezier(0.4, 0, 0.2, 1); + + @apply border-border border-1 shadow-lg; + } + + /* Left accent bar */ + [data-sonner-toast]::before { + content: ''; + position: absolute; + left: 4px; + top: 6px; + bottom: 6px; + width: 4px; + border-radius: 999px; + background: currentColor; + opacity: 0.9; + } + /* Toast types */ + [data-sonner-toast][data-type='success'] { + color: var(--toaster-text-success); + box-shadow: + 0 2px 3px -2px var(--toaster-text-success), + 0 2px 3px -2px var(--toaster-text-success); + } + + [data-sonner-toast][data-type='error'] { + color: var(--toaster-text-error); + box-shadow: + 0 2px 3px -2px var(--toaster-text-error), + 0 2px 3px -2px var(--toaster-text-error); + } + + [data-sonner-toast][data-type='warning'] { + color: var(--toaster-text-warning); + box-shadow: + 0 2px 3px -2px var(--toaster-text-warning), + 0 2px 3px -1px var(--toaster-text-warning); + } + + [data-sonner-toast][data-type='info'] { + color: var(--toaster-text-info); + box-shadow: + 0 2px 3px -2px var(--toaster-text-info), + 0 2px 3px -2px var(--toaster-text-info); + } + + /* Close button */ + [data-sonner-close-button] { + opacity: 0.5; + transition: opacity 120ms ease; + } + + [data-sonner-close-button]:hover { + opacity: 1; + } + + /* Animation */ + @keyframes toast-slide-in { + from { + opacity: 0; + transform: translateY(6px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } } diff --git a/app/components/Logo/Raven.vue b/app/components/Logo/Raven.vue index 95207cd96..f33d2ac77 100644 --- a/app/components/Logo/Raven.vue +++ b/app/components/Logo/Raven.vue @@ -1,6 +1,6 @@