Merge pull request #1938 from mermaid-js/sidv/editorChooserExperiments
feat(MC-4486): Add variations to editor picker
This commit is contained in:
Vendored
+3
@@ -12,11 +12,14 @@
|
||||
"fsegurai",
|
||||
"gantt",
|
||||
"gitgraph",
|
||||
"hugeicons",
|
||||
"KROKI",
|
||||
"localstorage",
|
||||
"mermaidchart",
|
||||
"mindmap",
|
||||
"NEWYEAR",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"Pageview",
|
||||
"pako",
|
||||
"panmove",
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@iconify-json/hugeicons": "^1.2.26",
|
||||
"@iconify-json/logos": "^1.2.11",
|
||||
"@iconify-json/material-symbols": "^1.2.20",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@playwright/test": "^1.52.0",
|
||||
|
||||
Generated
+20
@@ -102,6 +102,12 @@ importers:
|
||||
'@fortawesome/fontawesome-free':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
'@iconify-json/hugeicons':
|
||||
specifier: ^1.2.26
|
||||
version: 1.2.26
|
||||
'@iconify-json/logos':
|
||||
specifier: ^1.2.11
|
||||
version: 1.2.11
|
||||
'@iconify-json/material-symbols':
|
||||
specifier: ^1.2.20
|
||||
version: 1.2.20
|
||||
@@ -702,6 +708,12 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@iconify-json/hugeicons@1.2.26':
|
||||
resolution: {integrity: sha512-ZJB0EjFrYJy+EBUnx2/mGD4MCGScQr4dauV455lC9CuCt50guk6TosnOi9t2r4EIyhHmytO5IOeVFG4C8V9/hw==}
|
||||
|
||||
'@iconify-json/logos@1.2.11':
|
||||
resolution: {integrity: sha512-fOo4pGEatuyuCFNL+cwquYMa2Im0oJHRHV7lt/Qqs5Ode/lPImHCQcfTtPzZj7qYMPb/h8YHN3TG54uEowrjNQ==}
|
||||
|
||||
'@iconify-json/material-symbols@1.2.20':
|
||||
resolution: {integrity: sha512-+KqOT+3fD+LC2FbWiV8gd4+JLMiVUtmqrjzpKN1ji7rfMQTwvYJ94RT0WQlmL+vfDNJ5MTRe3rBzzJyvIH/aSg==}
|
||||
|
||||
@@ -4549,6 +4561,14 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@iconify-json/hugeicons@1.2.26':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/logos@1.2.11':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/material-symbols@1.2.20':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
@@ -224,7 +224,11 @@
|
||||
onClose={closePopup}
|
||||
onTryFree={() => {
|
||||
logMermaidChartClick('vibeDiagramming');
|
||||
window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank');
|
||||
window.open(
|
||||
$urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save,
|
||||
'_blank',
|
||||
'noopener'
|
||||
);
|
||||
closePopup();
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import PrivacyPolicyLink from '$/components/migration/PrivacyPolicyLink.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import CodeIcon from '~icons/custom/code';
|
||||
import ArrowIcon from '~icons/material-symbols/arrow-forward-rounded';
|
||||
import OpenSourceIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
import ChatIcon from '~icons/material-symbols/chat-outline-rounded';
|
||||
import EditIcon from '~icons/material-symbols/edit-outline-rounded';
|
||||
import HistoryIcon from '~icons/material-symbols/history';
|
||||
import HomeIcon from '~icons/material-symbols/home-storage-outline-rounded';
|
||||
import SparklesIcon from '~icons/material-symbols/kid-star-outline';
|
||||
import LanguageIcon from '~icons/material-symbols/language';
|
||||
import WidthIcon from '~icons/material-symbols/width-rounded';
|
||||
import type { EditorChooserVariantProps } from './editorChooserActions';
|
||||
|
||||
let { actions }: EditorChooserVariantProps = $props();
|
||||
|
||||
const plusFeatures = [
|
||||
{ icon: EditIcon, label: 'Visual editor' },
|
||||
{ icon: SparklesIcon, label: '300 AI credits' },
|
||||
{ icon: HomeIcon, label: 'Unlimited diagram storage' },
|
||||
{ icon: WidthIcon, label: 'Limitless diagram size' },
|
||||
{ icon: ChatIcon, label: 'View & comment collaboration' }
|
||||
];
|
||||
const openSourceFeatures = [
|
||||
{ icon: LanguageIcon, label: 'Diagram stored in URL' },
|
||||
{ icon: CodeIcon, label: 'Code editor' },
|
||||
{ icon: OpenSourceIcon, label: 'Open source' },
|
||||
{ icon: HistoryIcon, label: 'Version history' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Dialog.Content class="flex max-w-2xl flex-col gap-6 bg-pink-50 p-6 dark:bg-background">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-center text-2xl font-semibold">Choose your editor</Dialog.Title>
|
||||
<Dialog.Description class="text-center text-sm font-light">
|
||||
You'll never see this again
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<!-- Mermaid Plus Card -->
|
||||
<div
|
||||
class="relative flex flex-col overflow-hidden rounded-xl border-2 border-accent bg-white shadow dark:bg-card">
|
||||
<div class="bg-accent px-6 py-2 text-center text-sm font-semibold text-accent-foreground">
|
||||
Recommended
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Mermaid Plus</h3>
|
||||
<p class="text-sm text-muted-foreground">Unlock AI, storage and collaboration</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-center">
|
||||
<span
|
||||
class="rounded-full bg-pink-100 px-3 py-0.5 text-xs font-semibold text-pink-700 dark:bg-pink-950 dark:text-pink-300">
|
||||
10% off with code JS26
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<McWrapper labelPrefix="Opens ">
|
||||
<Button variant="accent" class="w-full" onclick={() => actions.startTrial()}>
|
||||
Start free trial
|
||||
</Button>
|
||||
</McWrapper>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 text-sm">
|
||||
{#each plusFeatures as feature (feature.label)}
|
||||
<li class="flex items-center gap-2">
|
||||
<feature.icon class="size-4 shrink-0 text-muted-foreground" />
|
||||
{feature.label}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Source Card -->
|
||||
<div class="flex flex-col gap-4 rounded-xl border bg-white p-6 shadow dark:bg-card">
|
||||
<div class="flex flex-col justify-end pt-10">
|
||||
<h3 class="text-xl font-bold">Open Source</h3>
|
||||
<p class="text-sm text-muted-foreground">Code only, no login, always free</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="mt-2 text-sm text-muted-foreground">Mermaid has a new home</p>
|
||||
<McWrapper labelPrefix="Opens ">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="w-full border-accent"
|
||||
onclick={() => actions.openMermaidAiLive('continueToNewHome')}>
|
||||
Continue to mermaid.ai/live
|
||||
<ArrowIcon class="ml-1 size-4" />
|
||||
</Button>
|
||||
</McWrapper>
|
||||
<Button variant="outline" class="w-full" onclick={() => actions.dismiss('stayOnLive')}>
|
||||
Stay on mermaid.live
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 text-sm">
|
||||
{#each openSourceFeatures as feature (feature.label)}
|
||||
<li class="flex items-center gap-2">
|
||||
<feature.icon class="size-4 shrink-0 text-muted-foreground" />
|
||||
{feature.label}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrivacyPolicyLink />
|
||||
</Dialog.Content>
|
||||
@@ -1,164 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import { dismissEditorChooser } from '$/util/migration/domainMigration';
|
||||
import { logEvent, logMermaidChartClick } from '$/util/stats';
|
||||
import { getCheckoutUrl } from '$/util/util';
|
||||
import CodeIcon from '~icons/custom/code';
|
||||
import ArrowIcon from '~icons/material-symbols/arrow-forward-rounded';
|
||||
import OpenSourceIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
import ChatIcon from '~icons/material-symbols/chat-outline-rounded';
|
||||
import EditIcon from '~icons/material-symbols/edit-outline-rounded';
|
||||
import HistoryIcon from '~icons/material-symbols/history';
|
||||
import HomeIcon from '~icons/material-symbols/home-storage-outline-rounded';
|
||||
import SparklesIcon from '~icons/material-symbols/kid-star-outline';
|
||||
import LanguageIcon from '~icons/material-symbols/language';
|
||||
import WidthIcon from '~icons/material-symbols/width-rounded';
|
||||
import { dismissEditorChooser, getEditorChooserVariant } from '$/util/migration/domainMigration';
|
||||
import EditorChooserControl from './EditorChooserControl.svelte';
|
||||
import EditorChooserTestA from './EditorChooserTestA.svelte';
|
||||
import EditorChooserTestB from './EditorChooserTestB.svelte';
|
||||
import EditorChooserTestC from './EditorChooserTestC.svelte';
|
||||
import { createEditorChooserActions } from './editorChooserActions';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
let { open = $bindable() }: Props = $props();
|
||||
|
||||
const close = () => {
|
||||
dismissEditorChooser();
|
||||
const variant = getEditorChooserVariant();
|
||||
// Tracks whether the current close was triggered by an explicit action
|
||||
// (button click logs its own event) vs. implicit dismissal (ESC/click-outside).
|
||||
let handled = false;
|
||||
const actions = createEditorChooserActions(variant, () => {
|
||||
handled = true;
|
||||
open = false;
|
||||
};
|
||||
|
||||
const handleStartTrial = () => {
|
||||
logEvent('chooseEditor', { choice: 'plus' });
|
||||
logMermaidChartClick('editorPicker');
|
||||
close();
|
||||
window.open(
|
||||
getCheckoutUrl({ utmCampaign: 'start_plus', utmMedium: '2_editor_selection' }),
|
||||
'_blank'
|
||||
);
|
||||
};
|
||||
|
||||
const handleStartFree = () => {
|
||||
logEvent('chooseEditor', { choice: 'openSource' });
|
||||
close();
|
||||
};
|
||||
|
||||
const handleContinueToNewHome = () => {
|
||||
logEvent('chooseEditor', { choice: 'newHome' });
|
||||
close();
|
||||
window.open('https://mermaid.ai/live', '_blank');
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root
|
||||
bind:open
|
||||
onOpenChange={(v) => {
|
||||
if (!v) handleStartFree();
|
||||
if (v) return;
|
||||
if (!handled) actions.log('dismissed');
|
||||
handled = false;
|
||||
dismissEditorChooser();
|
||||
}}>
|
||||
<Dialog.Content class="flex max-w-2xl flex-col gap-6 bg-pink-50 p-6 dark:bg-background">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-center text-2xl font-semibold">Choose your editor</Dialog.Title>
|
||||
<Dialog.Description class="text-center text-sm font-light">
|
||||
You'll never see this again
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<!-- Mermaid Plus Card -->
|
||||
<div
|
||||
class="relative flex flex-col overflow-hidden rounded-xl border-2 border-accent bg-white shadow dark:bg-card">
|
||||
<div class="bg-accent px-6 py-2 text-center text-sm font-semibold text-accent-foreground">
|
||||
Recommended
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Mermaid Plus</h3>
|
||||
<p class="text-sm text-muted-foreground">Unlock AI, storage and collaboration</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-center">
|
||||
<span
|
||||
class="rounded-full bg-pink-100 px-3 py-0.5 text-xs font-semibold text-pink-700 dark:bg-pink-950 dark:text-pink-300">
|
||||
10% off with code JS26
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button variant="accent" class="w-full" onclick={handleStartTrial}>
|
||||
Start free trial
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 text-sm">
|
||||
<li class="flex items-center gap-2">
|
||||
<EditIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Visual editor
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<SparklesIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
300 AI credits
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<HomeIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Unlimited diagram storage
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<WidthIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Limitless diagram size
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<ChatIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
View & comment collaboration
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Source Card -->
|
||||
<div class="flex flex-col gap-4 rounded-xl border bg-white p-6 shadow dark:bg-card">
|
||||
<div class="flex flex-col justify-end pt-10">
|
||||
<h3 class="text-xl font-bold">Open Source</h3>
|
||||
<p class="text-sm text-muted-foreground">Code only, no login, always free</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="mt-2 text-sm text-muted-foreground">Mermaid has a new home</p>
|
||||
<Button variant="outline" class="w-full border-accent" onclick={handleContinueToNewHome}>
|
||||
Continue to mermaid.ai/live
|
||||
<ArrowIcon class="ml-1 size-4" />
|
||||
</Button>
|
||||
<Button variant="outline" class="w-full" onclick={handleStartFree}>
|
||||
Stay on mermaid.live
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 text-sm">
|
||||
<li class="flex items-center gap-2">
|
||||
<LanguageIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Diagram stored in URL
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<CodeIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Code editor
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<OpenSourceIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Open source
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<HistoryIcon class="size-4 shrink-0 text-muted-foreground" />
|
||||
Version history
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="https://mermaid.ai/privacy-policy"
|
||||
target="_blank"
|
||||
class="text-sm text-foreground underline hover:text-accent">
|
||||
mermaid.ai Privacy Policy
|
||||
</a>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
{#if variant === 'testA'}
|
||||
<EditorChooserTestA {actions} />
|
||||
{:else if variant === 'testB'}
|
||||
<EditorChooserTestB {actions} />
|
||||
{:else if variant === 'testC'}
|
||||
<EditorChooserTestC {actions} />
|
||||
{:else}
|
||||
<EditorChooserControl {actions} />
|
||||
{/if}
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import PrivacyPolicyLink from '$/components/migration/PrivacyPolicyLink.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import { asset } from '$app/paths';
|
||||
import type { EditorChooserVariantProps } from './editorChooserActions';
|
||||
|
||||
let { actions }: EditorChooserVariantProps = $props();
|
||||
|
||||
const openSourceFeatures = [
|
||||
'Diagram stored in URL',
|
||||
'Code editor',
|
||||
'Open source',
|
||||
'Version history'
|
||||
];
|
||||
const plusFeatures = [
|
||||
'AI diagramming',
|
||||
'Visual editor',
|
||||
'Encrypted diagram storage',
|
||||
'Premium plugins',
|
||||
'View & comment collaboration'
|
||||
];
|
||||
</script>
|
||||
|
||||
<Dialog.Content class="flex max-w-3xl flex-col gap-6 bg-background p-8">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-center text-2xl font-bold">Choose your editor</Dialog.Title>
|
||||
<Dialog.Description class="sr-only">
|
||||
Choose between the open source editor or Mermaid Plus
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<!-- Open Source Card -->
|
||||
<div class="flex flex-col gap-4 rounded-xl border bg-card p-6">
|
||||
<div class="flex items-center">
|
||||
<img class="size-10" src={asset('/favicon.svg')} alt="Mermaid Live Editor" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Open source</h3>
|
||||
<p class="text-sm text-muted-foreground">Code only, no login, always free</p>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-col gap-2 text-sm">
|
||||
{#each openSourceFeatures as feature (feature)}
|
||||
<li class="flex items-center gap-3 rounded-lg border border-border p-3">
|
||||
<span class="size-2 shrink-0 rounded-full bg-blue-500"></span>
|
||||
{feature}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
class="mt-auto self-start"
|
||||
onclick={() => actions.dismiss('continueToLive')}>
|
||||
Continue to mermaid.live
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Mermaid Plus Card -->
|
||||
<div class="flex flex-col gap-4 rounded-xl border bg-card p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<img class="size-10" src={asset('/mermaidchart-logo.svg')} alt="Mermaid Chart" />
|
||||
<span
|
||||
class="rounded-md bg-secondary px-2 py-0.5 text-xs font-semibold text-secondary-foreground">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Mermaid Plus</h3>
|
||||
<p class="text-sm text-muted-foreground">Unlock AI, storage and collaboration</p>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-col gap-2 text-sm">
|
||||
{#each plusFeatures as feature (feature)}
|
||||
<li class="flex items-center gap-3 rounded-lg border border-border p-3">
|
||||
<span class="size-2 shrink-0 rounded-full bg-blue-500"></span>
|
||||
{feature}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<McWrapper labelPrefix="Opens ">
|
||||
<Button variant="accent" class="mt-auto self-start" onclick={() => actions.startTrial()}>
|
||||
Start free trial
|
||||
</Button>
|
||||
</McWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrivacyPolicyLink />
|
||||
</Dialog.Content>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
|
||||
import PrivacyPolicyLink from '$/components/migration/PrivacyPolicyLink.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import type { Component } from 'svelte';
|
||||
import AtlassianIcon from '~icons/logos/atlassian';
|
||||
import AmazonIcon from '~icons/logos/aws';
|
||||
import GoogleIcon from '~icons/logos/google';
|
||||
import MicrosoftIcon from '~icons/logos/microsoft';
|
||||
import type { EditorChooserVariantProps } from './editorChooserActions';
|
||||
|
||||
let { actions }: EditorChooserVariantProps = $props();
|
||||
|
||||
const features = [
|
||||
{ title: 'AI diagram generation', description: 'Describe what you need, AI builds it' },
|
||||
{
|
||||
title: 'Visual drag-and-drop editor',
|
||||
description: 'Edit diagrams without writing code'
|
||||
},
|
||||
{
|
||||
title: 'Unlimited diagram storage',
|
||||
description: 'Save and organize all your diagrams'
|
||||
},
|
||||
{
|
||||
title: 'Team collaboration',
|
||||
description: 'Share, comment, and edit together in real-time'
|
||||
}
|
||||
];
|
||||
|
||||
const trustedLogos: { name: string; icon: Component }[] = [
|
||||
{ name: 'Google', icon: GoogleIcon },
|
||||
{ name: 'Microsoft', icon: MicrosoftIcon },
|
||||
{ name: 'Atlassian', icon: AtlassianIcon },
|
||||
{ name: 'Amazon', icon: AmazonIcon }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Dialog.Content class="flex max-w-lg flex-col gap-3 bg-background p-8">
|
||||
<Dialog.Header class="flex-col items-start gap-2 space-y-0 text-left sm:text-left">
|
||||
<MermaidChartIcon class="size-10" />
|
||||
<Dialog.Title class="pt-2 text-2xl font-bold">Try the full Mermaid experience</Dialog.Title>
|
||||
<Dialog.Description class="text-xs font-light text-muted-foreground">
|
||||
Free forever, with Plus features free for 7 days.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<ul class="mt-2 flex flex-col gap-3">
|
||||
{#each features as feature (feature.title)}
|
||||
<li class="flex items-center gap-3 rounded-lg border border-border p-3">
|
||||
<span class="size-2 shrink-0 rounded-full bg-accent"></span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-xs">{feature.title}</p>
|
||||
<p class="text-xs font-light text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="mt-2 flex items-center gap-3">
|
||||
<McWrapper labelPrefix="Opens ">
|
||||
<Button variant="accent" onclick={() => actions.startTrial()}>Start free trial</Button>
|
||||
</McWrapper>
|
||||
<Button variant="outline" onclick={() => actions.dismiss('stayOnLive')}>
|
||||
Stay on mermaid.live
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-col items-start gap-4">
|
||||
<p class="text-xs">Trusted by 5M people and over 200k companies</p>
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
{#each trustedLogos as logo (logo.name)}
|
||||
<logo.icon class="h-6 w-auto grayscale" aria-label={logo.name} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrivacyPolicyLink />
|
||||
</Dialog.Content>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import PrivacyPolicyLink from '$/components/migration/PrivacyPolicyLink.svelte';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import { asset } from '$app/paths';
|
||||
import type { Component } from 'svelte';
|
||||
import SparklesIcon from '~icons/hugeicons/sparkles';
|
||||
import ChevronIcon from '~icons/material-symbols/chevron-right-rounded';
|
||||
import CodeIcon from '~icons/material-symbols/code-rounded';
|
||||
import EditIcon from '~icons/material-symbols/rebase-edit-outline-rounded';
|
||||
import type { EditorChooserVariantProps } from './editorChooserActions';
|
||||
|
||||
let { actions }: EditorChooserVariantProps = $props();
|
||||
|
||||
interface Option {
|
||||
buttonClick: string;
|
||||
badge: string;
|
||||
description: string;
|
||||
icon: Component;
|
||||
onclick: () => void;
|
||||
title: string;
|
||||
external: boolean;
|
||||
}
|
||||
|
||||
const options: Option[] = [
|
||||
{
|
||||
badge: 'Start free trial',
|
||||
buttonClick: 'createWithAi',
|
||||
description: 'Describe your diagram in plain text and AI generates it for you',
|
||||
external: true,
|
||||
icon: SparklesIcon,
|
||||
onclick: () => actions.startTrial('createWithAi'),
|
||||
title: 'Create with AI'
|
||||
},
|
||||
{
|
||||
badge: 'Start free trial',
|
||||
buttonClick: 'useVisualEditor',
|
||||
description: 'Drag, drop, and connect shapes on an interactive canvas',
|
||||
external: true,
|
||||
icon: EditIcon,
|
||||
onclick: () => actions.startTrial('useVisualEditor'),
|
||||
title: 'Use visual editor'
|
||||
},
|
||||
{
|
||||
badge: 'Mermaid live',
|
||||
buttonClick: 'writeCode',
|
||||
description: 'Use Mermaid syntax directly in the code editor',
|
||||
external: false,
|
||||
icon: CodeIcon,
|
||||
onclick: () => actions.dismiss('writeCode'),
|
||||
title: 'Write code'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Dialog.Content class="flex max-w-xl flex-col gap-3 bg-background p-8">
|
||||
<Dialog.Header class="items-center gap-2 space-y-0">
|
||||
<img class="size-10" src={asset('/favicon.svg')} alt="Mermaid" />
|
||||
<Dialog.Title class="pt-2 text-2xl font-bold">How do you want to create?</Dialog.Title>
|
||||
<Dialog.Description class="text-xs text-muted-foreground">
|
||||
Choose how to start your diagram
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="mt-3 flex flex-col gap-3">
|
||||
{#snippet optionButton(option: Option)}
|
||||
<button
|
||||
type="button"
|
||||
class="group flex w-full items-center gap-3 rounded-lg border border-border px-3 py-3.5 text-left transition-colors hover:border-accent"
|
||||
onclick={option.onclick}>
|
||||
<option.icon class="size-5 shrink-0 text-muted-foreground group-hover:text-accent" />
|
||||
<div class="flex flex-1 flex-col gap-0.5">
|
||||
<span class="text-sm font-medium">{option.title}</span>
|
||||
<span class="text-xs font-light text-muted-foreground">{option.description}</span>
|
||||
</div>
|
||||
<span
|
||||
class="shrink-0 rounded-full bg-secondary px-3 py-1 text-xs font-normal text-secondary-foreground">
|
||||
{option.badge}
|
||||
</span>
|
||||
<ChevronIcon class="size-5 shrink-0 text-muted-foreground group-hover:text-accent" />
|
||||
</button>
|
||||
{/snippet}
|
||||
{#each options as option (option.buttonClick)}
|
||||
{#if option.external}
|
||||
<McWrapper labelPrefix="Opens ">
|
||||
{@render optionButton(option)}
|
||||
</McWrapper>
|
||||
{:else}
|
||||
{@render optionButton(option)}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-center text-xs text-muted-foreground">
|
||||
AI and visual editor are part of mermaid.ai. Start free today.
|
||||
</p>
|
||||
|
||||
<PrivacyPolicyLink />
|
||||
</Dialog.Content>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="https://mermaid.ai/privacy-policy"
|
||||
target="_blank"
|
||||
class="text-sm text-foreground underline hover:text-accent">
|
||||
mermaid.ai Privacy Policy
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { EditorChooserVariant } from '$/util/experiments';
|
||||
import { logEvent, logMermaidChartClick } from '$/util/stats';
|
||||
import { getCheckoutUrl, getMermaidAiLiveUrl } from '$/util/util';
|
||||
|
||||
const utmMedium = 'editorSelection';
|
||||
|
||||
// See MC-4486: UTM campaign per variant for CTAs routing to mermaid.ai.
|
||||
// The campaign defaults to the variant name; TestC's two trial CTAs are disambiguated
|
||||
// by buttonClick so we can tell AI vs visual-editor clicks apart in analytics.
|
||||
const testCUtmCampaignByButtonClick: Record<string, string> = {
|
||||
createWithAi: 'testCAi',
|
||||
useVisualEditor: 'testCVisual'
|
||||
};
|
||||
|
||||
const resolveUtmCampaign = (variant: EditorChooserVariant, buttonClick: string): string => {
|
||||
if (variant === 'testC') {
|
||||
return testCUtmCampaignByButtonClick[buttonClick] ?? variant;
|
||||
}
|
||||
return variant;
|
||||
};
|
||||
|
||||
export interface EditorChooserActions {
|
||||
log: (buttonClick: string) => void;
|
||||
startTrial: (buttonClick?: string) => void;
|
||||
dismiss: (buttonClick: string) => void;
|
||||
openMermaidAiLive: (buttonClick: string) => void;
|
||||
}
|
||||
|
||||
export interface EditorChooserVariantProps {
|
||||
actions: EditorChooserActions;
|
||||
}
|
||||
|
||||
export const createEditorChooserActions = (
|
||||
variant: EditorChooserVariant,
|
||||
close: () => void
|
||||
): EditorChooserActions => {
|
||||
const log = (buttonClick: string) => {
|
||||
logEvent('chooseEditor', { buttonClick, variant });
|
||||
};
|
||||
|
||||
const startTrial = (buttonClick = 'startTrial') => {
|
||||
log(buttonClick);
|
||||
logMermaidChartClick('editorPicker');
|
||||
close();
|
||||
window.open(
|
||||
getCheckoutUrl({
|
||||
utmCampaign: resolveUtmCampaign(variant, buttonClick),
|
||||
utmMedium
|
||||
}),
|
||||
'_blank',
|
||||
'noopener'
|
||||
);
|
||||
};
|
||||
|
||||
const dismiss = (buttonClick: string) => {
|
||||
log(buttonClick);
|
||||
close();
|
||||
};
|
||||
|
||||
const openMermaidAiLive = (buttonClick: string) => {
|
||||
log(buttonClick);
|
||||
close();
|
||||
window.open(
|
||||
getMermaidAiLiveUrl({
|
||||
utmCampaign: resolveUtmCampaign(variant, buttonClick),
|
||||
utmMedium
|
||||
}),
|
||||
'_blank',
|
||||
'noopener'
|
||||
);
|
||||
};
|
||||
|
||||
return { log, startTrial, dismiss, openMermaidAiLive };
|
||||
};
|
||||
@@ -12,5 +12,6 @@ export const TID = {
|
||||
export const C = {
|
||||
aiLiveEditor: 'ai_live_editor',
|
||||
editorChooserDismissedKey: 'mermaid-editor-chooser-dismissed',
|
||||
editorChooserVariantKey: 'mermaid-editor-chooser-variant',
|
||||
utmSource: 'mermaid_live_editor'
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const editorChooserVariants = ['control', 'testA', 'testB', 'testC'] as const;
|
||||
export type EditorChooserVariant = (typeof editorChooserVariants)[number];
|
||||
@@ -1,8 +1,10 @@
|
||||
import { C } from '$/constants';
|
||||
import { env } from '$/util/env';
|
||||
import { editorChooserVariants, type EditorChooserVariant } from '$/util/experiments';
|
||||
|
||||
const mermaidAiDomain = 'mermaid.ai';
|
||||
const mermaidLiveDomain = 'mermaid.live';
|
||||
const netlifyPreviewDomain = 'netlify.app';
|
||||
|
||||
/**
|
||||
* Check if we're on mermaid.ai
|
||||
@@ -20,6 +22,13 @@ export const isOnMermaidLive = (): boolean => {
|
||||
return domain === mermaidLiveDomain || domain.endsWith(`.${mermaidLiveDomain}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we're on a Netlify preview/staging deploy (*.netlify.app).
|
||||
*/
|
||||
const isOnNetlifyPreview = (): boolean => {
|
||||
return window.location.hostname.endsWith(`.${netlifyPreviewDomain}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current URL has pako data (diagram content from a shared link).
|
||||
*/
|
||||
@@ -55,11 +64,15 @@ const isReferredFromMermaid = (): boolean => {
|
||||
* Check if the editor chooser modal should be shown.
|
||||
* Shows for new users who haven't dismissed it and aren't viewing a shared link.
|
||||
* Not shown on mobile (viewport width < 640px).
|
||||
* Can be forced open for QA via the `?editorChooser=1` query flag, which bypasses
|
||||
* the hostname and dismissed checks.
|
||||
*/
|
||||
export const shouldShowEditorChooser = (): boolean => {
|
||||
if (!env.isEnabledMermaidChartLinks) return false;
|
||||
if (!isOnMermaidAI() && !isOnMermaidLive()) return false;
|
||||
if (window.innerWidth < 640) return false;
|
||||
const forced = new URLSearchParams(window.location.search).get('editorChooser') === '1';
|
||||
if (forced) return true;
|
||||
if (!isOnMermaidAI() && !isOnMermaidLive() && !isOnNetlifyPreview()) return false;
|
||||
if (window.localStorage.getItem(C.editorChooserDismissedKey) === 'true') return false;
|
||||
if (hasPakoData()) return false;
|
||||
if (isReferredFromMermaid()) return false;
|
||||
@@ -72,3 +85,20 @@ export const shouldShowEditorChooser = (): boolean => {
|
||||
export const dismissEditorChooser = (): void => {
|
||||
window.localStorage.setItem(C.editorChooserDismissedKey, 'true');
|
||||
};
|
||||
|
||||
const isValidVariant = (value: string | null): value is EditorChooserVariant => {
|
||||
return value !== null && (editorChooserVariants as readonly string[]).includes(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the A/B/C test variant assigned to this user for the editor chooser modal.
|
||||
* Assigns a variant on first call (uniform random across variants), persists it
|
||||
* in localStorage, and returns the same value on subsequent calls.
|
||||
*/
|
||||
export const getEditorChooserVariant = (): EditorChooserVariant => {
|
||||
const stored = window.localStorage.getItem(C.editorChooserVariantKey);
|
||||
if (isValidVariant(stored)) return stored;
|
||||
const variant = editorChooserVariants[Math.floor(Math.random() * editorChooserVariants.length)];
|
||||
window.localStorage.setItem(C.editorChooserVariantKey, variant);
|
||||
return variant;
|
||||
};
|
||||
|
||||
@@ -113,6 +113,9 @@ export const logEvent = (
|
||||
name: AnalyticsEvent,
|
||||
data?: Record<string, string | number | boolean>
|
||||
): void => {
|
||||
if (browser && window.location.hostname === 'localhost') {
|
||||
console.log('[plausible]', name, data);
|
||||
}
|
||||
if (!plausible) {
|
||||
return;
|
||||
}
|
||||
|
||||
+12
-5
@@ -42,23 +42,30 @@ export const MCBaseURL = env.isEnabledMermaidChartLinks
|
||||
? 'https://mermaid.ai' // 'http://localhost:5174'
|
||||
: 'https://example.com';
|
||||
|
||||
export const getCheckoutUrl = ({
|
||||
const buildUtmParams = ({
|
||||
utmCampaign,
|
||||
utmMedium
|
||||
}: {
|
||||
utmCampaign: string;
|
||||
utmMedium: string;
|
||||
}): string => {
|
||||
const params = new URLSearchParams({
|
||||
coupon: 'arDfyFT8',
|
||||
tier: 'plus',
|
||||
}): URLSearchParams =>
|
||||
new URLSearchParams({
|
||||
utm_campaign: utmCampaign,
|
||||
utm_medium: utmMedium,
|
||||
utm_source: getUTMSource()
|
||||
});
|
||||
|
||||
export const getCheckoutUrl = (utm: { utmCampaign: string; utmMedium: string }): string => {
|
||||
const params = buildUtmParams(utm);
|
||||
params.set('coupon', 'arDfyFT8');
|
||||
params.set('tier', 'plus');
|
||||
return `${MCBaseURL}/app/user/billing/checkout?${params.toString()}`;
|
||||
};
|
||||
|
||||
export const getMermaidAiLiveUrl = (utm: { utmCampaign: string; utmMedium: string }): string => {
|
||||
return `${MCBaseURL}/live?${buildUtmParams(utm).toString()}`;
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
export const errorDebug = (limit = 1000) => {
|
||||
count += 1;
|
||||
|
||||
Reference in New Issue
Block a user