From 1daac45c87e45d73fe94816fcf8b627ac869cf90 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 22 Apr 2026 18:52:02 +0530 Subject: [PATCH 1/5] feat(MC-4486): Add variations to editor picker --- .vscode/settings.json | 1 + package.json | 2 + pnpm-lock.yaml | 20 ++ .../migration/EditorChooserControl.svelte | 118 ++++++++++++ .../migration/EditorChooserModal.svelte | 174 +++--------------- .../migration/EditorChooserTestA.svelte | 94 ++++++++++ .../migration/EditorChooserTestB.svelte | 78 ++++++++ .../migration/EditorChooserTestC.svelte | 99 ++++++++++ .../migration/PrivacyPolicyLink.svelte | 8 + .../migration/editorChooserActions.ts | 72 ++++++++ src/lib/constants.ts | 1 + src/lib/util/experiments.ts | 2 + src/lib/util/migration/domainMigration.ts | 24 ++- src/lib/util/stats.ts | 3 + src/lib/util/util.ts | 17 +- 15 files changed, 559 insertions(+), 154 deletions(-) create mode 100644 src/lib/components/migration/EditorChooserControl.svelte create mode 100644 src/lib/components/migration/EditorChooserTestA.svelte create mode 100644 src/lib/components/migration/EditorChooserTestB.svelte create mode 100644 src/lib/components/migration/EditorChooserTestC.svelte create mode 100644 src/lib/components/migration/PrivacyPolicyLink.svelte create mode 100644 src/lib/components/migration/editorChooserActions.ts create mode 100644 src/lib/util/experiments.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e97c47e5..cfba73fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "fsegurai", "gantt", "gitgraph", + "hugeicons", "KROKI", "localstorage", "mermaidchart", diff --git a/package.json b/package.json index 326abe38..0c43de79 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ec5430e..a8672051 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/lib/components/migration/EditorChooserControl.svelte b/src/lib/components/migration/EditorChooserControl.svelte new file mode 100644 index 00000000..13a48524 --- /dev/null +++ b/src/lib/components/migration/EditorChooserControl.svelte @@ -0,0 +1,118 @@ + + + + + Choose your editor + + You'll never see this again + + + +
+ +
+
+ Recommended +
+ +
+
+

Mermaid Plus

+

Unlock AI, storage and collaboration

+
+ +
+
+ + 10% off with code JS26 + +
+ + + + +
+ +
    + {#each plusFeatures as feature (feature.label)} +
  • + + {feature.label} +
  • + {/each} +
+
+
+ + +
+
+

Open Source

+

Code only, no login, always free

+
+ +
+

Mermaid has a new home

+ + + + +
+ +
    + {#each openSourceFeatures as feature (feature.label)} +
  • + + {feature.label} +
  • + {/each} +
+
+
+ + +
diff --git a/src/lib/components/migration/EditorChooserModal.svelte b/src/lib/components/migration/EditorChooserModal.svelte index 6b49af95..84edc78f 100644 --- a/src/lib/components/migration/EditorChooserModal.svelte +++ b/src/lib/components/migration/EditorChooserModal.svelte @@ -1,164 +1,42 @@ { - if (!v) handleStartFree(); + if (v) return; + if (!handled) actions.log('dismissed'); + handled = false; + dismissEditorChooser(); }}> - - - Choose your editor - - You'll never see this again - - - -
- -
-
- Recommended -
- -
-
-

Mermaid Plus

-

Unlock AI, storage and collaboration

-
- -
-
- - 10% off with code JS26 - -
- - -
- -
    -
  • - - Visual editor -
  • -
  • - - 300 AI credits -
  • -
  • - - Unlimited diagram storage -
  • -
  • - - Limitless diagram size -
  • -
  • - - View & comment collaboration -
  • -
-
-
- - -
-
-

Open Source

-

Code only, no login, always free

-
- -
-

Mermaid has a new home

- - -
- -
    -
  • - - Diagram stored in URL -
  • -
  • - - Code editor -
  • -
  • - - Open source -
  • -
  • - - Version history -
  • -
-
-
- - -
+ {#if variant === 'testA'} + + {:else if variant === 'testB'} + + {:else if variant === 'testC'} + + {:else} + + {/if}
diff --git a/src/lib/components/migration/EditorChooserTestA.svelte b/src/lib/components/migration/EditorChooserTestA.svelte new file mode 100644 index 00000000..25bfab1c --- /dev/null +++ b/src/lib/components/migration/EditorChooserTestA.svelte @@ -0,0 +1,94 @@ + + + + + Choose your editor + + Choose between the open source editor or Mermaid Plus + + + +
+ +
+
+ Mermaid Live Editor +
+
+

Open source

+

Code only, no login, always free

+
+ +
    + {#each openSourceFeatures as feature (feature)} +
  • + + {feature} +
  • + {/each} +
+ + +
+ + +
+
+ Mermaid Chart + + Recommended + +
+
+

Mermaid Plus

+

Unlock AI, storage and collaboration

+
+ +
    + {#each plusFeatures as feature (feature)} +
  • + + {feature} +
  • + {/each} +
+ + + + +
+
+ + +
diff --git a/src/lib/components/migration/EditorChooserTestB.svelte b/src/lib/components/migration/EditorChooserTestB.svelte new file mode 100644 index 00000000..a610d439 --- /dev/null +++ b/src/lib/components/migration/EditorChooserTestB.svelte @@ -0,0 +1,78 @@ + + + + + + Try the full Mermaid experience + + Free forever with Plus features free for 7 days. + + + + + +
+ + + + +
+ +
+

Trusted by 5M people and over 200k companies

+
+ {#each trustedLogos as logo (logo.name)} + + {/each} +
+
+ + +
diff --git a/src/lib/components/migration/EditorChooserTestC.svelte b/src/lib/components/migration/EditorChooserTestC.svelte new file mode 100644 index 00000000..ba2aa54a --- /dev/null +++ b/src/lib/components/migration/EditorChooserTestC.svelte @@ -0,0 +1,99 @@ + + + + + Mermaid + How do you want to create? + + Choose how to start your diagram + + + +
+ {#snippet optionButton(option: Option)} + + {/snippet} + {#each options as option (option.buttonClick)} + {#if option.external} + + {@render optionButton(option)} + + {:else} + {@render optionButton(option)} + {/if} + {/each} +
+ +

+ AI and visual editor are part of Mermaid.ai — free forever with Plus features free for 7 days. +

+ + +
diff --git a/src/lib/components/migration/PrivacyPolicyLink.svelte b/src/lib/components/migration/PrivacyPolicyLink.svelte new file mode 100644 index 00000000..656b1afe --- /dev/null +++ b/src/lib/components/migration/PrivacyPolicyLink.svelte @@ -0,0 +1,8 @@ +
+ + mermaid.ai Privacy Policy + +
diff --git a/src/lib/components/migration/editorChooserActions.ts b/src/lib/components/migration/editorChooserActions.ts new file mode 100644 index 00000000..700c9a27 --- /dev/null +++ b/src/lib/components/migration/editorChooserActions.ts @@ -0,0 +1,72 @@ +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 = { + 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' + ); + }; + + const dismiss = (buttonClick: string) => { + log(buttonClick); + close(); + }; + + const openMermaidAiLive = (buttonClick: string) => { + log(buttonClick); + close(); + window.open( + getMermaidAiLiveUrl({ + utmCampaign: resolveUtmCampaign(variant, buttonClick), + utmMedium + }), + '_blank' + ); + }; + + return { log, startTrial, dismiss, openMermaidAiLive }; +}; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f9b48755..2d346622 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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; diff --git a/src/lib/util/experiments.ts b/src/lib/util/experiments.ts new file mode 100644 index 00000000..2f37b4b5 --- /dev/null +++ b/src/lib/util/experiments.ts @@ -0,0 +1,2 @@ +export const editorChooserVariants = ['control', 'testA', 'testB', 'testC'] as const; +export type EditorChooserVariant = (typeof editorChooserVariants)[number]; diff --git a/src/lib/util/migration/domainMigration.ts b/src/lib/util/migration/domainMigration.ts index e8ce68ff..3e0ce105 100644 --- a/src/lib/util/migration/domainMigration.ts +++ b/src/lib/util/migration/domainMigration.ts @@ -1,5 +1,6 @@ import { C } from '$/constants'; import { env } from '$/util/env'; +import { editorChooserVariants, type EditorChooserVariant } from '$/util/experiments'; const mermaidAiDomain = 'mermaid.ai'; const mermaidLiveDomain = 'mermaid.live'; @@ -55,11 +56,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()) return false; if (window.localStorage.getItem(C.editorChooserDismissedKey) === 'true') return false; if (hasPakoData()) return false; if (isReferredFromMermaid()) return false; @@ -72,3 +77,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; +}; diff --git a/src/lib/util/stats.ts b/src/lib/util/stats.ts index 1aab01e6..9f658c79 100644 --- a/src/lib/util/stats.ts +++ b/src/lib/util/stats.ts @@ -113,6 +113,9 @@ export const logEvent = ( name: AnalyticsEvent, data?: Record ): void => { + if (browser && window.location.hostname === 'localhost') { + console.log('[plausible]', name, data); + } if (!plausible) { return; } diff --git a/src/lib/util/util.ts b/src/lib/util/util.ts index 2b272be1..72757903 100644 --- a/src/lib/util/util.ts +++ b/src/lib/util/util.ts @@ -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; From a61a69e4775c794099fa9b7b7f339c6fae3e0d21 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 22 Apr 2026 18:58:31 +0530 Subject: [PATCH 2/5] fix: Show picker on netlify --- src/lib/util/migration/domainMigration.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/util/migration/domainMigration.ts b/src/lib/util/migration/domainMigration.ts index 3e0ce105..c4b5159e 100644 --- a/src/lib/util/migration/domainMigration.ts +++ b/src/lib/util/migration/domainMigration.ts @@ -4,6 +4,7 @@ import { editorChooserVariants, type EditorChooserVariant } from '$/util/experim const mermaidAiDomain = 'mermaid.ai'; const mermaidLiveDomain = 'mermaid.live'; +const netlifyPreviewDomain = 'netlify.app'; /** * Check if we're on mermaid.ai @@ -21,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). */ @@ -64,7 +72,7 @@ export const shouldShowEditorChooser = (): boolean => { if (window.innerWidth < 640) return false; const forced = new URLSearchParams(window.location.search).get('editorChooser') === '1'; if (forced) return true; - if (!isOnMermaidAI() && !isOnMermaidLive()) return false; + if (!isOnMermaidAI() && !isOnMermaidLive() && !isOnNetlifyPreview()) return false; if (window.localStorage.getItem(C.editorChooserDismissedKey) === 'true') return false; if (hasPakoData()) return false; if (isReferredFromMermaid()) return false; From 79c4b3bf55ab06af830cb47bfe4ca1ee906e246a Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 22 Apr 2026 21:04:00 +0530 Subject: [PATCH 3/5] chore: Add noopener --- .vscode/settings.json | 2 ++ src/lib/components/DesktopEditor.svelte | 6 +++++- src/lib/components/migration/editorChooserActions.ts | 6 ++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index cfba73fb..c5fa7f01 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,8 @@ "mermaidchart", "mindmap", "NEWYEAR", + "noopener", + "noreferrer", "Pageview", "pako", "panmove", diff --git a/src/lib/components/DesktopEditor.svelte b/src/lib/components/DesktopEditor.svelte index 4979ab9c..2a920a1c 100644 --- a/src/lib/components/DesktopEditor.svelte +++ b/src/lib/components/DesktopEditor.svelte @@ -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(); }} /> diff --git a/src/lib/components/migration/editorChooserActions.ts b/src/lib/components/migration/editorChooserActions.ts index 700c9a27..c4c78411 100644 --- a/src/lib/components/migration/editorChooserActions.ts +++ b/src/lib/components/migration/editorChooserActions.ts @@ -47,7 +47,8 @@ export const createEditorChooserActions = ( utmCampaign: resolveUtmCampaign(variant, buttonClick), utmMedium }), - '_blank' + '_blank', + 'noopener' ); }; @@ -64,7 +65,8 @@ export const createEditorChooserActions = ( utmCampaign: resolveUtmCampaign(variant, buttonClick), utmMedium }), - '_blank' + '_blank', + 'noopener' ); }; From a9becf55094f602fa0712c80e4cdad94f932c1c5 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 22 Apr 2026 22:37:01 +0530 Subject: [PATCH 4/5] fix: copy --- src/lib/components/migration/EditorChooserTestB.svelte | 2 +- src/lib/components/migration/EditorChooserTestC.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/migration/EditorChooserTestB.svelte b/src/lib/components/migration/EditorChooserTestB.svelte index a610d439..64eff295 100644 --- a/src/lib/components/migration/EditorChooserTestB.svelte +++ b/src/lib/components/migration/EditorChooserTestB.svelte @@ -42,7 +42,7 @@ Try the full Mermaid experience - Free forever with Plus features free for 7 days. + Free forever, with Plus features free for 7 days. diff --git a/src/lib/components/migration/EditorChooserTestC.svelte b/src/lib/components/migration/EditorChooserTestC.svelte index ba2aa54a..44a90491 100644 --- a/src/lib/components/migration/EditorChooserTestC.svelte +++ b/src/lib/components/migration/EditorChooserTestC.svelte @@ -92,7 +92,7 @@

- AI and visual editor are part of Mermaid.ai — free forever with Plus features free for 7 days. + AI and visual editor are part of Mermaid.ai — free forever, with Plus features free for 7 days.

From 031477cda92142e4bb10e978763153f37c208875 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 23 Apr 2026 13:05:45 +0530 Subject: [PATCH 5/5] fix: Update tagline and layout --- src/lib/components/migration/EditorChooserTestB.svelte | 6 ++++-- src/lib/components/migration/EditorChooserTestC.svelte | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/components/migration/EditorChooserTestB.svelte b/src/lib/components/migration/EditorChooserTestB.svelte index 64eff295..6df834db 100644 --- a/src/lib/components/migration/EditorChooserTestB.svelte +++ b/src/lib/components/migration/EditorChooserTestB.svelte @@ -62,10 +62,12 @@ - + -
+

Trusted by 5M people and over 200k companies

{#each trustedLogos as logo (logo.name)} diff --git a/src/lib/components/migration/EditorChooserTestC.svelte b/src/lib/components/migration/EditorChooserTestC.svelte index 44a90491..bfc2bd02 100644 --- a/src/lib/components/migration/EditorChooserTestC.svelte +++ b/src/lib/components/migration/EditorChooserTestC.svelte @@ -92,7 +92,7 @@

- AI and visual editor are part of Mermaid.ai — free forever, with Plus features free for 7 days. + AI and visual editor are part of mermaid.ai. Start free today.