diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx
index 1fab5aa6334..c0cea1b9ff8 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx
@@ -12,7 +12,7 @@ import emptyIssueDark from "@/app/assets/empty-state/search/issues-dark.webp?url
import emptyIssueLight from "@/app/assets/empty-state/search/issues-light.webp?url";
// components
import { EmptyState } from "@/components/common/empty-state";
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
// hooks
import { useAppRouter } from "@/hooks/use-app-router";
// services
@@ -66,7 +66,7 @@ export default function IssueDetailsPage({ loaderData }: Route.ComponentProps) {
return (
-
+
);
}
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
index fc1f76aa5d4..f0caf31eed2 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
@@ -9,6 +9,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
// plane types
+import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button";
import type { TSearchEntityRequestPayload, TWebhookConnectionQueryParams } from "@plane/types";
import { EFileAssetType } from "@plane/types";
@@ -16,7 +17,7 @@ import { EFileAssetType } from "@plane/types";
// plane utils
import { cn } from "@plane/utils";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { PageHead } from "@/components/core/page-title";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
import type { TPageRootConfig, TPageRootHandlers } from "@/components/pages/editor/page-root";
@@ -43,6 +44,8 @@ function PageDetailsPage({ params }: Route.ComponentProps) {
// router
const router = useAppRouter();
const { workspaceSlug, projectId, pageId } = params;
+ // translation
+ const { t } = useTranslation();
// store hooks
const { createPage, fetchPageDetails } = usePageStore(storeType);
const page = usePage({
@@ -154,7 +157,7 @@ function PageDetailsPage({ params }: Route.ComponentProps) {
if ((!page || !id) && !pageDetailsError)
return (
-
+
);
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx
index 5cb9d3c17eb..1524aa4f546 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx
@@ -8,11 +8,12 @@ import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
+import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWebhook } from "@plane/types";
// ui
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
@@ -29,6 +30,8 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) {
const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
// router
const { workspaceSlug, webhookId } = params;
+ // translation
+ const { t } = useTranslation();
// mobx store
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace();
@@ -90,7 +93,7 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) {
if (!currentWebhook)
return (
-
+
);
diff --git a/apps/web/app/(all)/onboarding/page.tsx b/apps/web/app/(all)/onboarding/page.tsx
index 86ef1b1f724..30bec6eaee1 100644
--- a/apps/web/app/(all)/onboarding/page.tsx
+++ b/apps/web/app/(all)/onboarding/page.tsx
@@ -8,10 +8,11 @@ import { observer } from "mobx-react";
import useSWR from "swr";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { OnboardingRoot } from "@/components/onboarding";
// constants
import { USER_WORKSPACES_LIST } from "@plane/constants";
+import { useTranslation } from "@plane/i18n";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
@@ -28,6 +29,8 @@ function OnboardingPage() {
// store hooks
const { data: user } = useUser();
const { fetchWorkspaces } = useWorkspace();
+ // translation
+ const { t } = useTranslation();
// fetching workspaces list
useSWR(USER_WORKSPACES_LIST, () => {
@@ -53,7 +56,7 @@ function OnboardingPage() {
) : (
-
+
)}
diff --git a/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx b/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx
index d7df27ccbcd..2003dfb720d 100644
--- a/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx
+++ b/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx
@@ -10,7 +10,7 @@ import { PROFILE_SETTINGS_TABS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { TProfileSettingsTabs } from "@plane/types";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { PageHead } from "@/components/core/page-title";
import { ProfileSettingsContent } from "@/components/settings/profile/content";
import { ProfileSettingsSidebarRoot } from "@/components/settings/profile/sidebar";
@@ -34,7 +34,7 @@ function ProfileSettingsPage(props: Route.ComponentProps) {
if (!currentUser || !isAValidTab)
return (
-
+
);
diff --git a/apps/web/app/(all)/workspace-invitations/page.tsx b/apps/web/app/(all)/workspace-invitations/page.tsx
index d7e580e86fc..df540e6a081 100644
--- a/apps/web/app/(all)/workspace-invitations/page.tsx
+++ b/apps/web/app/(all)/workspace-invitations/page.tsx
@@ -16,10 +16,11 @@ import {
UserOutline,
} from "@makeplane/propel/icons";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space";
// constants
import { WORKSPACE_INVITATION } from "@plane/constants";
+import { useTranslation } from "@plane/i18n";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
@@ -41,6 +42,8 @@ function WorkspaceInvitationPage() {
const invitation_id = searchParams.get("invitation_id");
const slug = searchParams.get("slug");
const token = searchParams.get("token");
+ // translation
+ const { t } = useTranslation();
// store hooks
const { data: currentUser } = useUser();
@@ -127,7 +130,7 @@ function WorkspaceInvitationPage() {
)
) : (
-
+
)}
diff --git a/apps/web/app/assets/images/logo-spinner-dark.gif b/apps/web/app/assets/images/logo-spinner-dark.gif
deleted file mode 100644
index 8bd08325569..00000000000
Binary files a/apps/web/app/assets/images/logo-spinner-dark.gif and /dev/null differ
diff --git a/apps/web/app/assets/images/logo-spinner-light.gif b/apps/web/app/assets/images/logo-spinner-light.gif
deleted file mode 100644
index 8b571031424..00000000000
Binary files a/apps/web/app/assets/images/logo-spinner-light.gif and /dev/null differ
diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx
index 80346247684..2c6369694d7 100644
--- a/apps/web/app/root.tsx
+++ b/apps/web/app/root.tsx
@@ -7,7 +7,7 @@
import type { ReactNode } from "react";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
-import { ThemeProvider, useTheme } from "next-themes";
+import { ThemeProvider } from "next-themes";
// plane imports
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
// types
@@ -21,7 +21,7 @@ import ogImage from "@/app/assets/og-image.png?url";
import globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
// lib
import { isStaleAssetError, recoverFromStaleAsset } from "@/lib/stale-asset-error";
// local
@@ -116,14 +116,12 @@ export default function Root() {
}
export function HydrateFallback() {
- const { resolvedTheme } = useTheme();
-
- // if we are on the server or the theme is not resolved, return an empty div
- if (typeof window === "undefined" || resolvedTheme === undefined) return ;
-
+ // Propel's LogoSpinner paints both theme GIFs and CSS picks the active one via
+ // `data-theme`, so we do not wait on next-themes. Waiting left a blank canvas
+ // until `resolvedTheme` landed, which is the flash at the end of boot.
return (
-
+
);
}
diff --git a/apps/web/core/components/account/auth-forms/auth-header.tsx b/apps/web/core/components/account/auth-forms/auth-header.tsx
index 239d6582918..bdbddefc022 100644
--- a/apps/web/core/components/account/auth-forms/auth-header.tsx
+++ b/apps/web/core/components/account/auth-forms/auth-header.tsx
@@ -9,7 +9,7 @@ import useSWR from "swr";
import { useTranslation } from "@plane/i18n";
import type { IWorkspaceMemberInvitation } from "@plane/types";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { WorkspaceLogo } from "@/components/workspace/logo";
// helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
@@ -102,7 +102,7 @@ export const AuthHeader = observer(function AuthHeader(props: TAuthHeader) {
if (isLoading)
return (
-
+
);
diff --git a/apps/web/core/components/common/logo-spinner.tsx b/apps/web/core/components/common/logo-spinner.tsx
deleted file mode 100644
index 9dfad1d8634..00000000000
--- a/apps/web/core/components/common/logo-spinner.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2023-present Plane Software, Inc. and contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- * See the LICENSE file for details.
- */
-
-import { useTheme } from "next-themes";
-// assets
-import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
-import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
-
-export function LogoSpinner() {
- const { resolvedTheme } = useTheme();
-
- const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight;
-
- return (
-
-

-
- );
-}
diff --git a/apps/web/core/components/onboarding/create-or-join-workspaces.tsx b/apps/web/core/components/onboarding/create-or-join-workspaces.tsx
index 1f29b30faea..4e70d9e601f 100644
--- a/apps/web/core/components/onboarding/create-or-join-workspaces.tsx
+++ b/apps/web/core/components/onboarding/create-or-join-workspaces.tsx
@@ -8,9 +8,10 @@ import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { AlertOctagonOutline } from "@makeplane/propel/icons";
// plane imports
+import { useTranslation } from "@plane/i18n";
import type { IWorkspaceMemberInvitation, TOnboardingSteps } from "@plane/types";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
// hooks
import { useUser } from "@/hooks/store/user";
import { useInstance } from "@/hooks/store/use-instance";
@@ -35,6 +36,8 @@ export const CreateOrJoinWorkspaces = observer(function CreateOrJoinWorkspaces(p
const { invitations, stepChange, finishOnboarding } = props;
// states
const [currentView, setCurrentView] = useState(null);
+ // translation
+ const { t } = useTranslation();
// store hooks
const { data: user } = useUser();
const { config } = useInstance();
@@ -87,7 +90,7 @@ export const CreateOrJoinWorkspaces = observer(function CreateOrJoinWorkspaces(p
)
) : (
-
+
)}
diff --git a/apps/web/core/components/workspace-notifications/root.tsx b/apps/web/core/components/workspace-notifications/root.tsx
index 6a9012021c2..55da0d0a215 100644
--- a/apps/web/core/components/workspace-notifications/root.tsx
+++ b/apps/web/core/components/workspace-notifications/root.tsx
@@ -9,10 +9,11 @@ import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
+import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { cn } from "@plane/utils";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
// hooks
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
import { useWorkspace } from "@/hooks/store/use-workspace";
@@ -27,6 +28,8 @@ type NotificationsRootProps = {
};
export const NotificationsRoot = observer(function NotificationsRoot({ workspaceSlug }: NotificationsRootProps) {
+ // translation
+ const { t } = useTranslation();
// hooks
const { currentWorkspace } = useWorkspace();
const {
@@ -94,7 +97,7 @@ export const NotificationsRoot = observer(function NotificationsRoot({ workspace
<>
{projectMemberInfoLoader ? (
-
+
) : (
-
+
);
diff --git a/apps/web/core/lib/wrappers/authentication-wrapper.tsx b/apps/web/core/lib/wrappers/authentication-wrapper.tsx
index fc142c3002c..f436534658b 100644
--- a/apps/web/core/lib/wrappers/authentication-wrapper.tsx
+++ b/apps/web/core/lib/wrappers/authentication-wrapper.tsx
@@ -8,8 +8,10 @@ import type { ReactNode } from "react";
import { observer } from "mobx-react";
import { useSearchParams, usePathname } from "next/navigation";
import useSWR from "swr";
+// plane imports
+import { useTranslation } from "@plane/i18n";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
@@ -36,6 +38,8 @@ export const AuthenticationWrapper = observer(function AuthenticationWrapper(pro
const nextPath = searchParams.get("next_path");
// props
const { children, pageType = EPageTypes.AUTHENTICATED } = props;
+ // translation
+ const { t } = useTranslation();
// hooks
const { isLoading: isUserLoading, data: currentUser, fetchCurrentUser } = useUser();
const { data: currentUserProfile } = useUserProfile();
@@ -81,7 +85,7 @@ export const AuthenticationWrapper = observer(function AuthenticationWrapper(pro
if ((isUserSWRLoading || isUserLoading || workspacesLoader) && !currentUser?.id)
return (
-
+
);
diff --git a/apps/web/core/lib/wrappers/instance-wrapper.tsx b/apps/web/core/lib/wrappers/instance-wrapper.tsx
index c5beae4d0fd..372705090bc 100644
--- a/apps/web/core/lib/wrappers/instance-wrapper.tsx
+++ b/apps/web/core/lib/wrappers/instance-wrapper.tsx
@@ -7,8 +7,10 @@
import type { ReactNode } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
+// plane imports
+import { useTranslation } from "@plane/i18n";
// components
-import { LogoSpinner } from "@/components/common/logo-spinner";
+import { LogoSpinner } from "@makeplane/propel/components/logo-spinner";
import { InstanceNotReady, MaintenanceView } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
@@ -19,6 +21,8 @@ type TInstanceWrapper = {
const InstanceWrapper = observer(function InstanceWrapper(props: TInstanceWrapper) {
const { children } = props;
+ // translation
+ const { t } = useTranslation();
// store
const { isLoading, instance, error, fetchInstanceInfo } = useInstance();
@@ -32,7 +36,7 @@ const InstanceWrapper = observer(function InstanceWrapper(props: TInstanceWrappe
if ((isLoading || isInstanceSWRLoading) && !instance)
return (
-
+
);
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index f94ebce9d00..849a9dee681 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -31,6 +31,14 @@ export default defineConfig(() => ({
},
dedupe: ["react", "react-dom", "@headlessui/react"],
},
+ ssr: {
+ // propel's LogoSpinner statically imports its GIF pair, which only a bundler can turn into a
+ // URL. Externalized, the server build would hand that import to Node, which cannot load a
+ // `.gif` — the SPA prerender then 500s. Bundling propel means Vite resolves the asset in the
+ // server build too, so the prerendered shell carries the same hashed `/assets/…` URL as the
+ // client bundle.
+ noExternal: ["@makeplane/propel"],
+ },
server: {
host: "127.0.0.1",
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b7e8251a913..2497cf00c82 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -876,7 +876,7 @@ importers:
version: 2.2.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@makeplane/propel':
specifier: 'catalog:'
- version: 0.2.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(date-fns@4.1.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.17)
+ version: 0.3.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(date-fns@4.1.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.17)
'@plane/constants':
specifier: workspace:*
version: link:../../packages/constants
@@ -1595,9 +1595,6 @@ importers:
react:
specifier: 19.2.8
version: 19.2.8
- react-day-picker:
- specifier: 'catalog:'
- version: 9.5.0(react@19.2.8)
react-dom:
specifier: 19.2.8
version: 19.2.8(react@19.2.8)