chore: Auto redirect to new domain
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const response = await resolve(event, {});
|
||||
return response;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { env } from '$/util/env';
|
||||
import { isOnMermaidAI } from '$/util/migration/domainMigration';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import ExternalLinkWrapper from './ExternalLinkWrapper.svelte';
|
||||
|
||||
@@ -12,13 +13,12 @@
|
||||
> = $props();
|
||||
|
||||
const mermaidChartDomain = 'mermaid.ai';
|
||||
const isMermaidAiDomain = env.domain === mermaidChartDomain;
|
||||
</script>
|
||||
|
||||
<ExternalLinkWrapper
|
||||
{...props}
|
||||
domain={mermaidChartDomain}
|
||||
isVisible={env.isEnabledMermaidChartLinks}
|
||||
showPopup={!isMermaidAiDomain}>
|
||||
showPopup={!isOnMermaidAI()}>
|
||||
{@render children()}
|
||||
</ExternalLinkWrapper>
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
interface Props {
|
||||
mobileToggle?: Snippet;
|
||||
children: Snippet;
|
||||
hidePromotion?: boolean;
|
||||
}
|
||||
|
||||
let { children, mobileToggle }: Props = $props();
|
||||
let { children, mobileToggle, hidePromotion = false }: Props = $props();
|
||||
|
||||
type Links = ComponentProps<typeof DropdownNavMenu>['links'];
|
||||
|
||||
@@ -39,7 +40,7 @@
|
||||
}
|
||||
];
|
||||
|
||||
let activePromotion = $state(getActivePromotion());
|
||||
let activePromotion = $state(hidePromotion ? undefined : getActivePromotion());
|
||||
|
||||
const trackBannerClick = () => {
|
||||
if (!plausible || !activePromotion) {
|
||||
@@ -54,7 +55,7 @@
|
||||
{#if activePromotion}
|
||||
<div class="top-bar z-10 flex h-fit w-full bg-primary">
|
||||
<div
|
||||
class="flex flex-grow"
|
||||
class="flex grow"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={trackBannerClick}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import {
|
||||
dismissWelcomeBanner,
|
||||
incrementWelcomeBannerVisitCount,
|
||||
redirectToMermaidLive
|
||||
} from '$/util/migration/domainMigration';
|
||||
import { logEvent } from '$/util/stats';
|
||||
import { onMount } from 'svelte';
|
||||
import CloseIcon from '~icons/material-symbols/close-rounded';
|
||||
import SwapIcon from '~icons/material-symbols/swap-horiz-rounded';
|
||||
import WhyWeMovedModal from './WhyWeMovedModal.svelte';
|
||||
|
||||
let visible = $state(true);
|
||||
let showExplainer = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
incrementWelcomeBannerVisitCount();
|
||||
});
|
||||
|
||||
const handleContinue = () => {
|
||||
logEvent('bannerDismissed');
|
||||
dismissWelcomeBanner();
|
||||
visible = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Just dismiss for this session, don't set permanent dismissal
|
||||
logEvent('bannerDismissed');
|
||||
visible = false;
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
redirectToMermaidLive();
|
||||
};
|
||||
|
||||
const handleWhyDidWeMove = () => {
|
||||
logEvent('explainerOpened');
|
||||
showExplainer = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div
|
||||
class="relative mb-2 flex w-full flex-col items-center gap-3 border-b bg-pink-100 px-4 py-3 sm:flex-row sm:gap-4"
|
||||
role="banner">
|
||||
<div class="flex flex-1 flex-col items-center gap-2 text-center sm:flex-row sm:text-left">
|
||||
<SwapIcon class="hidden size-5 shrink-0 text-accent sm:block" />
|
||||
<div>
|
||||
<span class="font-medium">Mermaid Live Editor has a new home</span>
|
||||
<span class="hidden text-sm text-muted-foreground sm:inline">
|
||||
Same open-source editor, your diagrams stay client-side.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 text-sm text-blue-500 underline hover:text-blue-400"
|
||||
onclick={handleWhyDidWeMove}>
|
||||
Why did we move?
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="accent" size="sm" onclick={handleContinue}>Continue here</Button>
|
||||
<span class="text-sm text-muted-foreground">or</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-blue-500 underline hover:text-blue-400"
|
||||
onclick={handleGoBack}>
|
||||
go back to mermaid.live
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground sm:static sm:ml-2"
|
||||
onclick={handleClose}
|
||||
title="Dismiss">
|
||||
<CloseIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<WhyWeMovedModal bind:open={showExplainer} onOpenChange={(v) => (showExplainer = v)} />
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import { redirectToMermaidLive } from '$/util/migration/domainMigration';
|
||||
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
|
||||
import ShieldIcon from '~icons/material-symbols/shield-lock-outline-rounded';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
let { open = $bindable(), onOpenChange }: Props = $props();
|
||||
|
||||
const handleUseMermaidLive = () => {
|
||||
redirectToMermaidLive();
|
||||
};
|
||||
|
||||
const handleGotIt = () => {
|
||||
onOpenChange(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {onOpenChange}>
|
||||
<Dialog.Content class="max-w-lg p-8">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-2xl font-semibold">Why the new address?</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<p class="text-muted-foreground">
|
||||
We're consolidating the Mermaid ecosystem under mermaid.ai — one home for the editor, docs,
|
||||
and the commercial platform.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4 border-t pt-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<ShieldIcon class="mt-0.5 size-5 shrink-0 text-green-600" />
|
||||
<div>
|
||||
<p class="font-medium">Same privacy</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Diagrams render client-side. No data sent to servers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<BookIcon class="mt-0.5 size-5 shrink-0 text-blue-600" />
|
||||
<div>
|
||||
<p class="font-medium">Same open-source code</p>
|
||||
<p class="text-sm text-muted-foreground">Identical codebase. MIT licensed.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<ExternalLinkIcon class="mt-0.5 size-5 shrink-0 text-purple-600" />
|
||||
<div>
|
||||
<p class="font-medium">Old links still work</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
All mermaid.live URLs with diagrams keep working.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="mt-6 flex gap-3">
|
||||
<Button variant="outline" onclick={handleUseMermaidLive}>Use mermaid.live</Button>
|
||||
<Button variant="accent" onclick={handleGotIt}>Got it</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,233 @@
|
||||
import { logEvent } from '$/util/stats';
|
||||
|
||||
// Constants for the domain migration
|
||||
const mermaidLiveDomain = 'mermaid.live';
|
||||
const mermaidAiDomain = 'mermaid.ai';
|
||||
const bypassCookieName = 'mermaid-stay';
|
||||
const welcomeBannerStorageKey = 'mermaid-welcome-banner-dismissed';
|
||||
const welcomeBannerVisitCountKey = 'mermaid-welcome-banner-visits';
|
||||
const redirectReferrerKey = 'mermaid-redirect-from';
|
||||
|
||||
/**
|
||||
* Check if the current URL has pako data (diagram content).
|
||||
* Pako data is in the hash fragment as #pako:... or as a legacy format.
|
||||
*/
|
||||
const hasPakoData = (): boolean => {
|
||||
const hash = window.location.hash;
|
||||
return (
|
||||
hash.includes('pako:') || hash.includes('base64:') || (hash.length > 10 && hash.startsWith('#'))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current domain
|
||||
*/
|
||||
const getCurrentDomain = (): string => {
|
||||
return window.location.hostname;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we're on mermaid.live
|
||||
*/
|
||||
const isOnMermaidLive = (): boolean => {
|
||||
return (
|
||||
getCurrentDomain() === mermaidLiveDomain || getCurrentDomain().endsWith(`.${mermaidLiveDomain}`)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we're on mermaid.ai
|
||||
*/
|
||||
export const isOnMermaidAI = (): boolean => {
|
||||
const domain = getCurrentDomain();
|
||||
return domain === mermaidAiDomain || domain.endsWith(`.${mermaidAiDomain}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a cookie value by name
|
||||
*/
|
||||
const getCookie = (name: string): string | null => {
|
||||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
return match ? match[2] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a cookie
|
||||
*/
|
||||
const setCookie = (name: string, value: string, days: number): void => {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the bypass cookie is set (user opted to stay on mermaid.live)
|
||||
*/
|
||||
const hasBypassCookie = (): boolean => {
|
||||
return getCookie(bypassCookieName) === '1';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the bypass cookie (user opted to stay on mermaid.live)
|
||||
* Cookie is permanent (100 years) so user doesn't have to opt out repeatedly
|
||||
*/
|
||||
const setBypassCookie = (): void => {
|
||||
setCookie(bypassCookieName, '1', 36500); // ~100 years
|
||||
};
|
||||
|
||||
/**
|
||||
* Redirect to mermaid.live with ?stay=1 to trigger bypass cookie on that domain.
|
||||
* Used by escape hatch buttons in WelcomeBanner and WhyWeMovedModal.
|
||||
*/
|
||||
export const redirectToMermaidLive = (): void => {
|
||||
logEvent('bannerEscapeClicked');
|
||||
window.location.href = `https://${mermaidLiveDomain}/edit?stay=1`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the URL has the ?stay=1 parameter (used for escape hatch)
|
||||
*/
|
||||
const hasStayParam = (): boolean => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.has('stay');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the ?stay=1 parameter: set bypass cookie and redirect to clean URL
|
||||
*/
|
||||
export const handleStayParam = (): boolean => {
|
||||
if (hasStayParam()) {
|
||||
logEvent('stayOnMermaidLive');
|
||||
setBypassCookie();
|
||||
// Remove the ?stay=1 param from URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('stay');
|
||||
window.history.replaceState({}, '', url.pathname + url.hash);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get welcome banner visit count from localStorage
|
||||
*/
|
||||
const getWelcomeBannerVisitCount = (): number => {
|
||||
const count = window.localStorage.getItem(welcomeBannerVisitCountKey);
|
||||
return count ? parseInt(count, 10) : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Increment welcome banner visit count
|
||||
*/
|
||||
export const incrementWelcomeBannerVisitCount = (): number => {
|
||||
const count = getWelcomeBannerVisitCount() + 1;
|
||||
window.localStorage.setItem(welcomeBannerVisitCountKey, count.toString());
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if welcome banner should be shown (shown for first 3 visits, then auto-hide)
|
||||
*/
|
||||
export const shouldShowWelcomeBanner = (): boolean => {
|
||||
// Only show on mermaid.ai
|
||||
if (!isOnMermaidAI()) return false;
|
||||
// Check if banner was manually dismissed permanently
|
||||
if (window.localStorage.getItem(welcomeBannerStorageKey) === 'true') return false;
|
||||
// Check if redirect referrer was from mermaid.live
|
||||
if (window.sessionStorage.getItem(redirectReferrerKey) !== 'mermaid.live') return false;
|
||||
// Show for first 3 visits
|
||||
return getWelcomeBannerVisitCount() < 3;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark redirect origin (call this when redirecting from mermaid.live)
|
||||
*/
|
||||
const setRedirectReferrer = (): void => {
|
||||
window.sessionStorage.setItem(redirectReferrerKey, 'mermaid.live');
|
||||
};
|
||||
|
||||
/**
|
||||
* Dismiss the welcome banner permanently
|
||||
*/
|
||||
export const dismissWelcomeBanner = (): void => {
|
||||
window.localStorage.setItem(welcomeBannerStorageKey, 'true');
|
||||
};
|
||||
|
||||
// localStorage keys used by the app for user data
|
||||
const userDataStorageKeys = [
|
||||
'codeStore', // Current diagram code/state
|
||||
'manualHistoryStore', // Manual history entries
|
||||
'autoHistoryStore' // Auto history entries
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if user has any stored data in localStorage.
|
||||
* This includes saved diagrams, history entries, etc.
|
||||
*/
|
||||
const hasStoredUserData = (): boolean => {
|
||||
for (const key of userDataStorageKeys) {
|
||||
const value = window.localStorage.getItem(key);
|
||||
if (value && value !== '[]' && value !== 'null' && value !== '{}') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check and perform domain migration redirect if needed.
|
||||
* This should be called early in the app lifecycle.
|
||||
*
|
||||
* Redirect logic:
|
||||
* - If on mermaid.live with no pako data in URL, no bypass cookie, and no stored data → redirect to mermaid.ai/live
|
||||
* - If URL has pako data → stay (user followed a shared link)
|
||||
* - If bypass cookie is set → stay (user opted out of redirect)
|
||||
* - If user has stored data in localStorage → stay (user has history/diagrams saved locally)
|
||||
*
|
||||
* @returns true if a redirect was triggered, false otherwise
|
||||
*/
|
||||
export const checkAndRedirectIfNeeded = (): boolean => {
|
||||
// Only redirect from mermaid.live
|
||||
if (!isOnMermaidLive()) return false;
|
||||
|
||||
// Don't redirect if user has bypass cookie
|
||||
if (hasBypassCookie()) return false;
|
||||
|
||||
// Don't redirect if URL has pako data (shared link)
|
||||
if (hasPakoData()) return false;
|
||||
|
||||
// Don't redirect if user has stored data in localStorage (history, saved diagrams, etc.)
|
||||
if (hasStoredUserData()) return false;
|
||||
|
||||
// Perform the redirect to mermaid.ai/live
|
||||
// Set a sessionStorage flag so we know this user came from mermaid.live
|
||||
// (sessionStorage doesn't transfer cross-domain, so we'll use a query param instead)
|
||||
const currentPath = window.location.pathname;
|
||||
const targetPath = currentPath.replace(/^\/?/, '/live/');
|
||||
const redirectUrl = `https://${mermaidAiDomain}${targetPath}?from=mermaid.live`;
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle incoming redirect from mermaid.live to mermaid.ai.
|
||||
* Checks for the ?from=mermaid.live param and sets session storage.
|
||||
*/
|
||||
export const handleIncomingRedirect = (): void => {
|
||||
// Only on mermaid.ai
|
||||
if (!isOnMermaidAI()) return;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('from') === 'mermaid.live') {
|
||||
// Mark that this session originated from a redirect
|
||||
setRedirectReferrer();
|
||||
|
||||
// Clean up the URL
|
||||
params.delete('from');
|
||||
const cleanSearch = params.toString();
|
||||
const newUrl =
|
||||
window.location.pathname + (cleanSearch ? `?${cleanSearch}` : '') + window.location.hash;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { env } from '$/util/env';
|
||||
import { isOnMermaidAI } from '$/util/migration/domainMigration';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
let { closeBanner }: Props = $props();
|
||||
|
||||
const utmSource = env.domain === 'mermaid.ai' ? 'mermaid_ai_live' : 'mermaid_live_editor';
|
||||
const utmSource = isOnMermaidAI() ? 'mermaid_ai_live' : 'mermaid_live_editor';
|
||||
|
||||
const url = `https://mermaid.ai/app/user/billing/checkout?${new URLSearchParams({
|
||||
coupon: 'dmIuNbqx',
|
||||
|
||||
@@ -80,18 +80,26 @@ const minutesToMilliSeconds = (minutes: number): number => {
|
||||
const defaultDelay = minutesToMilliSeconds(1);
|
||||
const delaysPerEvent = {
|
||||
bannerClick: defaultDelay,
|
||||
bannerDismissed: defaultDelay,
|
||||
bannerEscapeClicked: defaultDelay,
|
||||
copyClipboard: defaultDelay,
|
||||
copyMarkdown: defaultDelay,
|
||||
download: defaultDelay,
|
||||
explainerOpened: defaultDelay,
|
||||
history: defaultDelay,
|
||||
loadGist: defaultDelay,
|
||||
loadSampleDiagram: defaultDelay,
|
||||
migration: defaultDelay,
|
||||
mobileViewToggle: defaultDelay,
|
||||
nudgeClicked: defaultDelay,
|
||||
nudgeDismissed: defaultDelay,
|
||||
panZoom: minutesToMilliSeconds(10),
|
||||
pwaInstalled: defaultDelay,
|
||||
// Domain migration events (mermaid.live → mermaid.ai)
|
||||
redirectTriggered: defaultDelay,
|
||||
render: minutesToMilliSeconds(5),
|
||||
renderDiagram: defaultDelay,
|
||||
stayOnMermaidLive: defaultDelay,
|
||||
themeChange: defaultDelay,
|
||||
version: defaultDelay
|
||||
} as const;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { C } from '$/constants';
|
||||
import { env } from './env';
|
||||
import { loadDataFromUrl } from './fileLoaders/loader';
|
||||
import { initLoading } from './loading';
|
||||
import { isOnMermaidAI } from './migration/domainMigration';
|
||||
import { applyMigrations } from './migrations';
|
||||
import { initURLSubscription, loadState, updateCodeStore, verifyState } from './state';
|
||||
import { initAnalytics, plausible } from './stats';
|
||||
@@ -91,7 +92,7 @@ function fallbackCopyToClipboard(text: string) {
|
||||
}
|
||||
|
||||
export const getUTMSource = (): string => {
|
||||
if (typeof window !== 'undefined' && window.location.host.includes('mermaid.ai')) {
|
||||
if (typeof window !== 'undefined' && isOnMermaidAI()) {
|
||||
return C.aiLiveEditor;
|
||||
}
|
||||
return C.utmSource;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Toaster } from '$/components/ui/sonner/index.js';
|
||||
import { loadingStateStore } from '$/util/loading';
|
||||
import {
|
||||
checkAndRedirectIfNeeded,
|
||||
handleIncomingRedirect,
|
||||
handleStayParam
|
||||
} from '$/util/migration/domainMigration';
|
||||
import { toggleDarkTheme } from '$/util/state';
|
||||
import { initHandler } from '$/util/util';
|
||||
import { base } from '$app/paths';
|
||||
@@ -17,6 +22,20 @@
|
||||
// This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed.
|
||||
// Then move it into src and vite will bundle it automatically.
|
||||
onMount(() => {
|
||||
// Domain migration: Handle ?stay=1 escape hatch parameter first
|
||||
// This sets a bypass cookie and cleans the URL
|
||||
handleStayParam();
|
||||
|
||||
// Domain migration: Check for redirects early in the lifecycle
|
||||
// On mermaid.live: redirect to mermaid.ai if no pako data and no bypass cookie
|
||||
// On mermaid.ai: handle incoming redirects from mermaid.live
|
||||
const wasRedirected = checkAndRedirectIfNeeded();
|
||||
if (wasRedirected) {
|
||||
// If we're redirecting, don't continue with the rest of the setup
|
||||
return;
|
||||
}
|
||||
handleIncomingRedirect();
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
void initHandler();
|
||||
});
|
||||
@@ -41,7 +60,7 @@
|
||||
<ModeWatcher />
|
||||
<Toaster />
|
||||
|
||||
<main class="h-[100dvh]">
|
||||
<main class="h-dvh">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import History from '$/components/History/History.svelte';
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
|
||||
import WelcomeBanner from '$/components/migration/WelcomeBanner.svelte';
|
||||
import Navbar from '$/components/Navbar.svelte';
|
||||
import PanZoomToolbar from '$/components/PanZoomToolbar.svelte';
|
||||
import Preset from '$/components/Preset.svelte';
|
||||
@@ -18,6 +19,7 @@
|
||||
import VersionSecurityToolbar from '$/components/VersionSecurityToolbar.svelte';
|
||||
import View from '$/components/View.svelte';
|
||||
import type { EditorMode, Tab } from '$/types';
|
||||
import { isOnMermaidAI, shouldShowWelcomeBanner } from '$/util/migration/domainMigration';
|
||||
import { PanZoomState } from '$/util/panZoom';
|
||||
import { stateStore, updateCodeStore, urlsStore } from '$/util/state';
|
||||
import { logEvent } from '$/util/stats';
|
||||
@@ -50,12 +52,18 @@
|
||||
let width = $state(0);
|
||||
let isMobile = $derived(width < 640);
|
||||
let isViewMode = $state(true);
|
||||
let showWelcomeBanner = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
await initHandler();
|
||||
window.addEventListener('appinstalled', () => {
|
||||
logEvent('pwaInstalled', { isMobile });
|
||||
});
|
||||
|
||||
// Check if we should show welcome banner on mermaid.ai
|
||||
if (isOnMermaidAI() && shouldShowWelcomeBanner()) {
|
||||
showWelcomeBanner = true;
|
||||
}
|
||||
});
|
||||
|
||||
let isHistoryOpen = $state(false);
|
||||
@@ -81,7 +89,7 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Navbar mobileToggle={isMobile ? mobileToggle : undefined}>
|
||||
<Navbar mobileToggle={isMobile ? mobileToggle : undefined} hidePromotion={showWelcomeBanner}>
|
||||
<Toggle bind:pressed={isHistoryOpen} size="sm">
|
||||
<HistoryIcon />
|
||||
</Toggle>
|
||||
@@ -98,6 +106,11 @@
|
||||
</McWrapper>
|
||||
</Navbar>
|
||||
|
||||
<!-- Domain migration: Welcome banner for mermaid.ai visitors redirected from mermaid.live -->
|
||||
{#if showWelcomeBanner}
|
||||
<WelcomeBanner />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-1 flex-col overflow-hidden" bind:clientWidth={width}>
|
||||
<div
|
||||
class={[
|
||||
@@ -137,10 +150,7 @@
|
||||
</Resizable.Pane>
|
||||
{#if isHistoryOpen}
|
||||
<Resizable.Handle class="ml-1 hidden opacity-0 sm:block" />
|
||||
<Resizable.Pane
|
||||
minSize={15}
|
||||
defaultSize={30}
|
||||
class="hidden h-full flex-grow flex-col sm:flex">
|
||||
<Resizable.Pane minSize={15} defaultSize={30} class="hidden h-full grow flex-col sm:flex">
|
||||
<History />
|
||||
</Resizable.Pane>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user