Compare commits

..
Author SHA1 Message Date
forkless 89d77f72ae remove Buildx setup, native ARM64 Docker already available
Docker Build / docker (push) Successful in 54s
Docker Build / docker (pull_request) Failing after 14s
2026-06-24 15:07:50 +02:00
forkless b07a7ba5d8 disable Mermaid Chart links in build
Docker Build / docker (push) Failing after 10s
2026-06-24 15:06:24 +02:00
forkless 0a7029973b use GITEAUSER/GITEATOKEN secrets for registry login
Docker Build / docker (push) Successful in 1m3s
2026-06-24 15:01:56 +02:00
forkless b0c4ff5fd5 fix: use CIUSER/CITOKEN secrets, drop GITEA-prefixed names
Docker Build / docker (push) Failing after 2m41s
2026-06-24 14:55:20 +02:00
forkless 26bcfba18f add Gitea Actions CI: build & push Docker image on push to master/develop
Docker Build / docker (push) Failing after 10m44s
2026-06-24 14:45:21 +02:00
Sidharth VinodandGitHub 335f4bbfde Merge pull request #1991 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2026-06-19 19:23:27 +00:00
renovate[bot]andGitHub bc235e7498 fix(deps): update all non-major dependencies 2026-06-19 09:54:22 +00:00
Sidharth VinodandGitHub 7f08e5b970 Merge pull request #1987 from mermaid-js/sidv/examplesSelection
feat: add example picker dropdown to sample diagrams
2026-06-10 18:43:03 +00:00
Sidharth VinodandClaude Fable 5 a257f78d8e feat: add example picker dropdown to sample diagrams
Each diagram type in the Sample Diagrams panel is now a split button:
clicking the diagram name loads its default example, and a dropdown
arrow on the right (shown when a diagram has more than one example)
opens a popover listing all examples for that diagram.

- getSampleDiagrams now returns all examples per diagram with the
  default example first, instead of flattening to the default's code
- loadSampleDiagram analytics now include the chosen example title

https://claude.ai/code/session_015rUgHChBEkquLu77fMxmQ6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:09:45 +05:30
Sidharth VinodandGitHub 9cd92e1fd6 Merge pull request #1988 from mermaid-js/sidv/runes-migration-fixes
fix: Address regressions and structural risks in the runes migration
2026-06-10 18:35:50 +00:00
Sidharth VinodandClaude Fable 5 c253054916 refactor: Make persisted() values raw state
The getter handed out a deep $state proxy, so in-place mutation of a
persisted array/record would update the UI while silently never being
written to localStorage. All consumers already replace .value wholesale;
$state.raw makes that the only semantics and drops the per-read proxy
overhead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:57:27 +05:30
Sidharth VinodandClaude Fable 5 d2f067a540 refactor: Route every input state mutation through one untracked gateway
The untrack()/persistAndProcess() pair was a per-function convention:
each update function had to remember both, a forgotten untrack would let
calling effects subscribe to input state (the bug fixed in 6a9a306e),
and a forgotten persistAndProcess would silently skip persistence and
re-validation. A single update(mutate) gateway now makes both structural,
and the scattered untrack calls (three of which were dead) are gone.

Also:
- Export inputState as Readonly<State> so writes outside the update
  functions are type errors instead of comment violations.
- Share the 'codeStore' key as a constant instead of two literals.
- Reuse validatedStateOf() in processState instead of duplicating the
  validated-state defaults.
- Use $state.raw for validatedCurrent: it is only ever replaced
  wholesale, so deep proxying every published state was pure overhead.

The new state.svelte.test.ts pins the invariants: update functions never
make a calling effect track input state, and every mutation persists.
Vitest needed resolve.conditions=['browser'] for those tests — it was
loading Svelte's server build, where $effect is a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:57:01 +05:30
Sidharth VinodandClaude Fable 5 59105628f4 fix: Treat a stored JSON null as a missing persisted value
readJSON returned any successfully parsed value, so a localStorage
entry holding the literal "null" came back as null instead of the
fallback — and a null inputState crashes module init on every load
until storage is cleared. The deleted MacFJA persist layer guarded
this with `null !== initialValue`; restore that behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:53:00 +05:30
Sidharth VinodandClaude Fable 5 144b4dc190 fix: Stop mobile editor from reverting keystrokes
currentText was $state, and the validated-state sync $effect both reads
and writes it. Every keystroke (which sets currentText in the CodeMirror
updateListener) therefore re-ran the effect while re-validation was
still in flight, dispatching a full-document replacement with the stale
validatedState text — visibly reverting the keystroke and resetting the
cursor until validation caught up.

The effect only needs to react to validatedState publishes, so make
currentText a plain variable, matching DesktopEditor.

No unit test: the repo has no component-test harness, and CodeMirror
cannot mount under jsdom without one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:51:57 +05:30
Sidharth VinodandClaude Fable 5 6a9a306ed7 fix: Untrack input state reads inside state update functions
toggleDarkTheme runs inside the layout $effect; without untrack the
effect starts depending on inputState.mermaid and re-adds the default
theme whenever a loaded config replaces it (caught by the unsafe-config
scrub e2e test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:44:16 +05:30
Sidharth VinodandClaude Fable 5 337e9f86ac refactor: Use $app/state instead of $app/stores in error page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:38:55 +05:30
Sidharth VinodandClaude Fable 5 c9cfaa10e7 refactor: Replace vendored svelte-persistent-store with shared persisted rune
History now uses the shared persist.svelte helpers, the MacFJA store
copy is deleted, and the esserializer dependency it required is dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:37:53 +05:30
Sidharth VinodandClaude Fable 5 ed06c07c06 refactor: Migrate hidden promotions store to the persisted rune
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:36:58 +05:30
Sidharth VinodandClaude Fable 5 a49503a1f0 refactor: Migrate the migrations store to the persisted rune
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:35:41 +05:30
Sidharth VinodandClaude Fable 5 81aaafc085 refactor: Migrate loading state from svelte/store to runes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:35:00 +05:30
Sidharth VinodandClaude Fable 5 b3c8bd6c28 refactor: Migrate editor state from svelte/store to runes
Replace the writable+derived store pipeline in state.ts with a
state.svelte.ts module: a deep $state inputState mutated only through
the exported update functions, each of which persists the snapshot and
re-validates it asynchronously into validatedState.current (with
urls.current derived from it). Store subscriptions in components become
$effect blocks, and the rough/grid toggles use function bindings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:34:07 +05:30
Sidharth VinodandClaude Fable 5 ca96ca8771 feat: Add shared runes-based localStorage persistence module
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:23:48 +05:30
Sidharth VinodandGitHub 219d8af74f Merge pull request #1984 from mermaid-js/sidv/fixViewURL
fix: base URL handling
2026-06-09 19:40:49 +05:30
Sidharth Vinod 49c5928202 fix: Resolve URLs in urlsStore 2026-06-09 19:32:08 +05:30
Sidharth VinodandGitHub 459c22cf09 Merge pull request #1982 from mermaid-js/sidv/rewrite-timeline-history
fix: Rewrite Timeline/Saved history state and UI handling
2026-06-08 15:01:44 +00:00
Sidharth VinodandClaude Opus 4.8 f25b3c82cd refactor: De-duplicate mode routing and drop dead getStateString
Code-review cleanup:
- Collapse the two mode switches (the historyState.entries getter and
  activeSlot) into a single slotFor(mode) helper; entries now reads
  `slotFor(mode)?.value ?? loader`.
- Remove the now-unused getStateString export from state.ts (its only
  consumer was rewritten off it in this PR).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:28:50 +05:30
Sidharth VinodandClaude Opus 4.8 2db9355b7b fix: Accept time:0 entries and precompute history entry URLs
Code-review follow-ups:
- validateEntry no longer rejects entries whose time is 0 (epoch); it now
  checks `typeof time === 'number'`, so restoring/uploading such entries
  works instead of silently dropping them.
- History.svelte serializes each entry's "open in new tab" URL once via a
  $derived list instead of calling serializeState (pako deflate) per row on
  every render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:22:40 +05:30
Sidharth VinodandClaude Opus 4.8 8dd9cb49a0 feat: Add "open in new tab" link to history entries
Each history entry now has a third action: a real anchor (rendered via
the Button's href, target="_blank") linking to the editor with that
entry's serialized state. Being a normal link, users can also copy it or
open it in a new tab via the context menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:03:31 +05:30
Sidharth VinodandClaude Opus 4.8 64e8822e32 fix: Make svelte-check pass and resolve TS6/Svelte 5.56 type errors
Adding the `svelte-check` CI gate surfaced pre-existing type errors and
warnings under develop's TypeScript 6 / Svelte 5.56 bump. Fixes them so
`pnpm check` reports 0 errors / 0 warnings:

- Add `lang="ts"` to Share/Privacy (and a script to PrivacyPolicyLink) so
  importers get real declarations instead of implicit `any`.
- Replace the deprecated `monaco.languages.json` with the new top-level
  `monaco.json` namespace (proper API, no casts).
- Navbar: use `resolve('/', {})` instead of the deprecated `base`.
- Type the promo `component` as `Component<{ closeBanner: Snippet }>` and
  MainMenu's `renderer` as `Snippet<[Omit<MenuItem, 'renderer'>]>`.
- Index-by-string casts in state.ts / Preset / DiagramDocumentationButton.
- Make Actions' clipboard handler accept an optional event.
- toggle-group: expose variant/size via getters to fix the
  state_referenced_locally warning.
- Make the History e2e save/delete cases change state via a sample
  diagram (deterministic) instead of editor typing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:25 +05:30
Sidharth Vinod a3a3f8ffbe Update .gitignore 2026-06-08 19:28:39 +05:30
Sidharth VinodandClaude Opus 4.8 e6333e9d7f refactor: Convert history state to Svelte 5 runes + add svelte-check
Migrate the Timeline/Saved history state module to idiomatic Svelte 5
runes and wire up type-checking.

- Rename history.ts -> historyState.svelte.ts and replace the Svelte
  stores with $state-backed reactive values, removing the get() calls
  (the only remaining one reads the external inputStateStore). A small
  localStorage-backed `persisted()` helper keeps the same keys, so user
  data is preserved. Consumers read via the reactive `historyState`
  getter object instead of store auto-subscriptions.
- The gist loader now replaces the in-memory revisions in one call
  (setLoaderEntries) instead of appending, so loading a new gist no
  longer accumulates stale revisions.
- Add svelte-check: new `check` script, a "Type check" step in the
  unit-tests CI workflow, and skipLibCheck so the check passes on the
  dependency .d.ts files. svelte-check reports 0 errors / 0 warnings.
- Add accessible labels to the History toggle and the per-item
  Restore/Delete buttons (a11y + testability).
- Unskip tests/history.spec.ts and rewrite it against the new UI:
  load-from-localStorage + restore, tab-highlight follows the mode,
  save/dedupe, auto-vs-manual isolation, and delete/clear. All pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:28:38 +05:30
Sidharth VinodandClaude Opus 4.8 3628acec02 fix: Rewrite Timeline/Saved history state and UI handling
The auto (Timeline) and manual (Saved) history handling had several bugs.
This rewrites the state module into a small, tested source-of-truth API and
turns History.svelte into a thin view.

Bugs fixed:
- Auto-saves no longer leak into the Saved list. addEntry now writes only to
  the store for its own type (previously every entry was also pushed to the
  manual store).
- Dedup is keyed on code + config via stateKey() instead of the whole input
  state, so volatile/view-only fields (renderCount, updateDiagram, pan/zoom)
  no longer cause spurious saves or hide real edits.
- The active-tab highlight tracks the history mode. History binds
  activeTabID={$historyModeStore} and Tabs derives the highlight reactively
  instead of mutating its prop once, which had frozen it on the first tab.
- Auto-save runs for the whole edit session via startAutoSave() in +page,
  with proper cleanup. Previously the interval lived in History.svelte, only
  ran while the panel was open, and was never cleared (leaking a new interval
  on every open).
- restoreEntries() routes each uploaded entry to the store matching its own
  type instead of dumping everything into one store by the first entry's type.

The persisted localStorage keys (autoHistoryStore, manualHistoryStore,
autoHistoryMode) are unchanged, so existing user data is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:27:23 +05:30
Sidharth VinodandGitHub ebced635c9 Merge pull request #1774 from mermaid-js/sidv/fixUndo
fix: #1753 Undo in Monaco
2026-06-08 13:16:56 +00:00
Sidharth VinodandCursor 1f0812496e fix: prevent state feedback loop when updating Monaco via executeEdits
Guard programmatic editor updates so sample diagrams and URL loads do not re-trigger onUpdate, which caused infinite state/URL churn and failing e2e tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 18:40:07 +05:30
Sidharth Vinod c62ebf6d09 fix: #1753 Undo in Monaco 2026-06-08 18:14:56 +05:30
Sidharth VinodandGitHub 6438d0074d Merge pull request #1719 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies to v10.34.1
2026-06-08 18:02:54 +05:30
Sidharth VinodandGitHub 130711a9d0 Merge pull request #1718 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies to ^24.12.4
2026-06-08 18:02:07 +05:30
renovate[bot]andGitHub 7c6e28e163 chore(deps): update all non-major dependencies to v10.34.1 2026-06-08 12:27:57 +00:00
renovate[bot]andGitHub 1b432bcaaa chore(deps): update all non-major dependencies to ^24.12.4 2026-06-08 12:27:15 +00:00
Sidharth VinodandGitHub da89994440 Merge pull request #1980 from mermaid-js/chore/update-all-deps
chore: update all dependencies to latest versions
2026-06-08 12:21:54 +00:00
Sidharth VinodandCursor c25675d418 chore: update all dependencies to latest versions
Bump the full dependency tree (Vite 8, Vitest 4, ESLint 10, Svelte 5.56,
mode-watcher 1.x, etc.) and adapt code for breaking API changes. Upgrade
Playwright CI image to v1.60 for Node 24 compatibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 17:47:38 +05:30
Sidharth VinodandGitHub a58cde9516 Merge pull request #1971 from mermaid-js/chore/node-24-lts
chore: Node.js 24 LTS and latest mermaid packages
2026-06-08 11:41:55 +00:00
Sidharth VinodandCursor ccf5a13018 fix: upgrade Playwright to 1.60 for Node 24.16 compatibility
Playwright <1.60 hangs on browser extraction under Node 24.16.0; bump
the test dependency and CI container image to unblock e2e.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 17:08:27 +05:30
Sidharth VinodandGitHub 4ab4e022a0 Merge branch 'develop' into chore/node-24-lts 2026-06-08 16:19:46 +05:30
Sidharth VinodandGitHub 52abdccc3a Merge pull request #1963 from IvanTheGeek/bug/1961_favicon-link-tags
fix(#1961): Correct favicon link tags
2026-06-08 08:22:51 +00:00
Ivan The Geek d134306482 fix: drop ico link tag — favicon.ico has opaque white corners
The .ico file is a single 32×32 frame with white (255,255,255,255)
pixels filling the rounded-corner regions instead of transparent
ones, so declaring it as an `<link rel="icon">` made Chrome render
the favicon with visible white corners on dark backgrounds. The
svg+xml type fix and apple-touch-icon link remain — those don't
involve the .ico.

Legacy browsers that need the .ico will still find it via the root
`/favicon.ico` auto-probe; regenerating the .ico from favicon.svg
so its content matches the rest is a separate asset change.
2026-06-07 13:34:40 -04:00
Sidharth VinodandCursor d040d8d84a chore: bump mermaid and related packages to latest
Upgrade mermaid 11.15.0, layout-elk 0.2.1, and layout-tidy-tree 0.2.2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 01:07:52 +05:30
Sidharth VinodandCursor c2cd0c0bfc chore: upgrade Node.js to 24.16.0 LTS
Pin CI, Docker, and engines to the current Active LTS and bump @types/node.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:44:47 +05:30
Sidharth VinodandGitHub 2ff56f67bc Merge pull request #1965 from mermaid-js/sidv/editor-chooser-live-2026
Ship editor chooser testB with live_2026 UTM campaign
2026-05-29 16:02:47 +05:30
Sidharth VinodandCursor 55d9bd043f refactor: Inline editor chooser modal content
Remove the separate testB component now that the A/B test is over.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:49:44 +05:30
Sidharth VinodandCursor adf3ddf015 feat: Ship editor chooser testB and use live_2026 UTM campaign
End the A/B test by always showing the testB modal, remove unused variant
components and assignment logic, and route CTA links with utm_campaign=live_2026.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:45:04 +05:30
Sidharth VinodandGitHub bfea7dd4db Merge pull request #1964 from mermaid-js/sidv/close-broken-link-issues
chore: Auto-close issues with only "Broken link" in title
2026-05-28 00:42:19 +05:30
Sidharth VinodandCursor 7cd24ddcb3 chore: Clarify live editor vs Mermaid syntax issues in auto-close comment
Direct syntax error reports to mermaid-js/mermaid and limit this repo to live editor bugs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 00:41:27 +05:30
Sidharth VinodandCursor 241f99bacf chore: Auto-close issues with only "Broken link" in title
Add a GitHub Action that closes low-effort issues using the default pre-filled title and prompts reporters to include the failing URL and details.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 00:38:52 +05:30
Ivan The GeekandIvan The Geek 472a3283c8 fix(#1961): Correct favicon link tags
Three small markup fixes in src/app.html:

- Update the icon link's `type` from `image/png` to `image/svg+xml` so
  it matches its `favicon.svg` href. The MIME mismatch was introduced
  in #1065 when the href was switched from `.png` to `.svg` without
  updating `type`; browsers ignore the attribute and trust the
  Content-Type so it rendered fine, but the markup was incorrect.
- Add an explicit `image/x-icon` link for `favicon.ico`. The file
  already ships in `static/`, but it was previously discoverable only
  via the root `/favicon.ico` auto-probe.
- Add an `apple-touch-icon` link pointing at `favicon.png` (512×512)
  so iOS "Add to Home Screen" uses the Mermaid logo instead of a
  low-quality page screenshot.

No behavior change for existing desktop users.
2026-05-25 13:38:32 -04:00
Sidharth VinodandGitHub 7b43e33971 Merge pull request #1951 from ZenUml/chore/mermaid-zenuml-0.2.3
[codex] bump mermaid-zenuml to 0.2.3
2026-05-18 20:17:21 +05:30
MrCoder 6af70cb44c chore: bump mermaid-zenuml to 0.2.3 2026-05-14 16:56:18 +10:00
Sidharth Vinod 1fabea49cf chore: Update mermaid-examples 2026-04-23 14:40:49 +05:30
Sidharth VinodandGitHub 1bd6355ea8 Merge pull request #1938 from mermaid-js/sidv/editorChooserExperiments
feat(MC-4486): Add variations to editor picker
2026-04-23 14:32:44 +05:30
Sidharth VinodandGitHub 29eb7b01fc Merge pull request #1939 from mermaid-js/sidv/EnhancedEditButton
chore(MC-4489): Add enhanced edit button
2026-04-23 14:16:34 +05:30
Sidharth Vinod b446c37315 chore: Add enhanced edit button 2026-04-23 13:31:19 +05:30
Sidharth Vinod 031477cda9 fix: Update tagline and layout 2026-04-23 13:05:45 +05:30
Sidharth Vinod a9becf5509 fix: copy 2026-04-22 22:37:01 +05:30
Sidharth Vinod 79c4b3bf55 chore: Add noopener 2026-04-22 21:04:00 +05:30
Sidharth Vinod a61a69e477 fix: Show picker on netlify 2026-04-22 18:58:31 +05:30
Sidharth Vinod 1daac45c87 feat(MC-4486): Add variations to editor picker 2026-04-22 18:52:02 +05:30
Alois KlinkandGitHub e3562168ad Merge pull request #1929 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to ^11.14.0
2026-04-10 12:42:42 +00:00
renovate[bot]andGitHub 7fa7a48325 fix(deps): update dependency mermaid to ^11.14.0 2026-04-10 12:39:46 +00:00
Alois KlinkandGitHub 4925de94a9 Merge commit from fork
fix: improve sanitization of mermaid config
2026-04-10 21:35:24 +09:00
Sidharth Vinod 679e444bf1 chore: Remove canonical 2026-04-02 17:34:24 +05:30
Alois Klink 46a7a533ab fix: copy mermaid config sanitization check
Copy the XSS prevention check from the `sanitize` function in mermaid
upstream's `config.ts` file.

See: https://github.com/mermaid-js/mermaid/blob/9745f325cb9e1967640f0e85da193a2f820634f1/packages/mermaid/src/config.ts#L178-L183
2026-04-02 19:00:54 +09:00
Alois Klink 67aacdebe4 fix: sanitize config for __ keys
According to upstream, this is supposed to be fore prototype pollution
prevention.

See: https://github.com/mermaid-js/mermaid/blob/9745f325cb9e1967640f0e85da193a2f820634f1/packages/mermaid/src/config.ts#L169-L174
2026-04-02 19:00:54 +09:00
Alois Klink 7e9cdfca73 fix: sanitize all secure keys in config
Check and remove all the secure keys in the site Mermaid Config,
not just `securityLevel`. Unfortunately, the logic of Mermaid's
`sanitize` function in `config.ts` is a bit convoluted. For instance,
the `secure` config restricts keys deeply in the object.

See: https://github.com/mermaid-js/mermaid/blob/9745f325cb9e1967640f0e85da193a2f820634f1/packages/mermaid/src/config.ts#L155-L190
2026-04-02 19:00:54 +09:00
Alois Klink 48b9560e8e fix: sanitize config loaded from gist/config URL
Currently, the `config` in the codeState in the hash is sanitized for
unsafe values, however the `?config` URL parameter or configs loaded
from a GitHub Gist are not.

Reported-by: Chai Cheng Xun @QiaoNPC
2026-04-02 18:59:09 +09:00
Sidharth VinodandGitHub f9f1e27bc3 Merge pull request #1923 from mermaid-js/sidv/visualEditor
feat: Add visual editor button
2026-04-01 22:20:22 +05:30
Sidharth Vinod 62699769d5 fix: Use {} instead of undefined 2026-04-01 22:19:02 +05:30
Sidharth Vinod 5608dc5661 feat: Add visual editor button 2026-04-01 21:50:18 +05:30
Sidharth VinodandGitHub 29e92a9702 Merge pull request #1917 from mermaid-js/sidv/fixBasePath
fix(#1914): Resolve base path
2026-04-01 15:13:59 +00:00
Sidharth Vinod 0e0da3b1e9 chore: Reduce Error debounce interval 2026-04-01 20:43:15 +05:30
Sidharth Vinod d273e1d722 fix: Asset links 2026-03-30 21:30:13 +05:30
Sidharth Vinod c799dff042 fix(#1914): Resolve redirectURL considering base path 2026-03-30 21:24:33 +05:30
Alois Klink ae7ad1b93e refactor: move config sanitization into function
Moves the code that prompts the user to remove unsafe MermaidConfig
settings into it's own function, so that it can be reused.
2026-03-30 17:06:32 +09:00
Sidharth Vinod c675dc02e9 fix: Avoid double popups 2026-03-25 17:03:04 +09:00
Sidharth Vinod ab760f35fa feat: Retain search query in URL to get insights into traffic flow 2026-03-18 22:05:07 +09:00
Sidharth VinodandGitHub 8a436791de Merge pull request #1908 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to ^11.13.0
2026-03-16 07:41:08 -07:00
Sidharth VinodandGitHub d16a1776e6 Merge pull request #1909 from mermaid-js/sidv/correctPrivacyPolicy
feat: Configure Privacy Policy
2026-03-16 07:40:41 -07:00
Sidharth Vinod ab720e0267 fix: local test change 2026-03-16 23:33:20 +09:00
Sidharth Vinod d7db71e38a chore: Add new mermaid home button 2026-03-16 23:29:49 +09:00
Sidharth Vinod b466e8430e feat: Show mermaid.live privacy policy only on mermaid.live
As the OSS project only controls the mermaid.live domain, we're adding an env variable to let anyone who is self hosting live editor to set their own privacy policy, or to hide the button if they want to.
2026-03-16 23:29:13 +09:00
renovate[bot]andGitHub ea404df79b fix(deps): update dependency mermaid to ^11.13.0 2026-03-16 01:36:21 +00:00
Sidharth Vinod 15cdb7b64a chore: Show editor picker once to existing users as well 2026-03-12 13:35:42 +09:00
Sidharth Vinod 340911f271 chore: Remove panZoom event 2026-03-05 23:28:34 +05:30
Sidharth VinodandGitHub 44d15c0f5e Merge pull request #1901 from mermaid-js/claude/hide-editor-picker-mobile-2h7p0
Hide editor picker on mobile
2026-03-03 14:09:58 -08:00
Claude c36e51fb9f Hide editor picker dialog on mobile in shouldShowEditorChooser
Return false when viewport width is below 640px (Tailwind sm breakpoint),
matching the isMobile check used elsewhere in the app.

https://claude.ai/code/session_01KoPM7CZKWdwdZPm4MneGVE
2026-03-03 22:02:10 +00:00
Sidharth Vinod 82f7371fed fix: Do not show editor selector to users coming from docs site, as they have seen one already 2026-03-04 01:50:29 +05:30
Sidharth Vinod e56451e027 chore: Update coupon code 2026-03-04 01:43:34 +05:30
Sidharth VinodandGitHub 1ed126e684 Merge pull request #1898 from mermaid-js/sidv/fixTests
tests: Tweak failing tests
2026-03-03 11:41:35 -08:00
Sidharth Vinod a85bc09092 fix: Track mermaid.ai outflow 2026-03-04 01:10:12 +05:30
Sidharth Vinod 5cc29dfebe tests: Tweak failing tests 2026-03-04 00:32:16 +05:30
Sidharth VinodandGitHub 6079e5e1ba Merge pull request #1896 from mermaid-js/sidv/popup
Editor Chooser popup
2026-03-03 10:33:43 -08:00
Sidharth Vinod dabbf5700a test: Disable popup in tests 2026-03-03 23:45:51 +05:30
Sidharth Vinod 13e5e834a2 fix: Respect MERMAID_IS_ENABLED_MERMAID_CHART_LINKS for popup 2026-03-03 15:56:47 +05:30
Sidharth Vinod bf1a7581e8 chore: Add editor chooser dialog for first time users 2026-03-03 15:54:37 +05:30
Sidharth Vinod d092480cf3 chore: Auto redirect to new domain 2026-03-03 13:22:36 +05:30
Sidharth VinodandGitHub 94f7dafc72 Merge pull request #1886 from chouheiwa/fix/cjk-text-visibility-in-svg-export
fix: ensure CJK text visibility in SVG export
2026-02-16 21:11:50 +05:30
Sidharth VinodandGitHub 8d7db5501a Merge pull request #1883 from saurabhg772244/saurabh/add-mermaid-inline-ai
add mermaid inline AI
2026-02-16 20:44:51 +05:30
saurabhg772244 f6cf766986 added dark mode for icon 2026-02-16 19:36:20 +05:30
Sidharth VinodandGitHub 4bfe42458e Merge pull request #1887 from mermaid-js/sidv/promo
chore: Update promo URL
2026-02-16 16:46:25 +05:30
Sidharth Vinod f6f9668044 fix: Update couponID 2026-02-16 16:13:33 +05:30
Sidharth Vinod 2b13fde292 chore: Update promo URL 2026-02-16 15:55:21 +05:30
chouheiwa 5aed4ea7a0 fix: use viewBox dimensions for PNG export canvas size
In rough mode, SVG width/height are set to 100%, so getBoundingClientRect
returns the container size instead of the actual diagram size. Use viewBox
dimensions when available to get the correct aspect ratio for PNG export.
2026-02-16 16:19:22 +08:00
chouheiwa 5dd1ec6b63 fix: apply foreignObject clipping fix to PNG export path
Move fixForeignObjectClipping into getBase64SVG so it covers both
SVG and PNG export flows. Previously only getSvgElement (SVG export)
applied the fix, while exportImage (PNG export) passed the svg element
directly to getBase64SVG, bypassing the fix entirely.
2026-02-16 11:12:40 +08:00
chouheiwa 49f800bacb fix: apply foreignObject clipping fix to all languages in rough mode SVG export 2026-02-16 09:18:17 +08:00
chouheiwa 83ba64c8c8 fix: ensure CJK text visibility in SVG export 2026-02-15 17:31:32 +08:00
Sidharth VinodandGitHub 49629cfb61 Update Canonicals 2026-02-13 19:30:59 +05:30
saurabhg772244 6160617458 Fix AI inline popup issues and remove quick edit functionality 2026-02-13 13:54:39 +05:30
saurabhg772244 1166479277 remove unused global style 2026-02-12 20:02:48 +05:30
saurabhg772244 958e7aa62c close popup on outside click and escape button 2026-02-12 19:51:54 +05:30
saurabhg772244 aa3479d508 code refactor and fix icon hover issue 2026-02-12 15:52:21 +05:30
saurabhg772244 830df8c44b add mermaid inline AI 2026-02-12 13:13:16 +05:30
Sidharth VinodandGitHub 629d9639d2 Merge pull request #1882 from mermaid-js/sidv/promo
feat: Add new year promo
2026-02-02 15:33:56 +05:30
Sidharth Vinod 1a529f598f feat: Add new year promo 2026-02-02 15:33:02 +05:30
Sidharth Vinod f3d580b8eb fix: Update tiny tree 2026-01-29 14:22:08 +05:30
Sidharth VinodandGitHub 7414ac0926 Merge pull request #1878 from mermaid-js/sidv/updateMcDomain
feat: Hide tooltip based on domain
2026-01-28 12:02:01 +05:30
Sidharth VinodandGitHub c05fa21dcc Merge pull request #1877 from mermaid-js/sidv/updateDocsURL
chore: Make docs URL configurable during build
2026-01-28 12:01:52 +05:30
Sidharth VinodandGitHub 61e194517d Merge pull request #1875 from mermaid-js/sidv/removePlayground
chore: Remove playground toggle
2026-01-28 12:01:29 +05:30
Sidharth Vinod 9946dff29d feat: Hide tooltip based on domain 2026-01-28 11:52:30 +05:30
Sidharth Vinod d72e91a7b8 chore: Make docs URL configurable during build 2026-01-28 11:37:12 +05:30
Sidharth Vinod 832cf7d4e7 fix: lint errors 2026-01-28 00:35:02 +05:30
Sidharth Vinod b92fda6279 Merge branch 'develop' into sidv/removePlayground
* develop:
  fix: lint errors
  chore: update dependencies
2026-01-28 00:33:21 +05:30
Sidharth Vinod 7f551dbbe6 fix: lint errors 2026-01-28 00:32:44 +05:30
Sidharth Vinod 5dd2fe4bd0 chore: update dependencies 2026-01-28 00:32:32 +05:30
Sidharth Vinod d9e70427af chore: Remove playground toggle 2026-01-28 00:25:17 +05:30
Sidharth Vinod 739ef6dfe2 chore: remove trackOnLocalhost 2025-10-15 15:59:05 +05:30
Sidharth Vinod 48c560c979 fix: Catch panZoom errors 2025-10-15 15:58:04 +05:30
Sidharth Vinod b1367c2178 fix(#1791): Disable plausible init when analytics url is empty 2025-10-15 15:57:41 +05:30
Sidharth Vinod ef6aa4b09d feat: Add layout-tidy-tree 2025-10-15 15:25:43 +05:30
Sidharth Vinod ea6ae3d7e2 feat: debounce error section 2025-10-08 19:57:20 +05:30
Sidharth VinodandGitHub 01cd13c712 Merge pull request #1798 from junqwoopark/bug/1797_fix_export_background_color
fix(export): Remove redundant hsl() from background color
2025-09-18 20:21:25 +05:30
Sidharth VinodandGitHub e41499eda3 Merge pull request #1799 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to ^11.12.0
2025-09-18 20:08:22 +05:30
renovate[bot]andGitHub 6c08c490b1 fix(deps): update dependency mermaid to ^11.12.0 2025-09-18 09:41:15 +00:00
junqwoopark b237dd64eb fix(export): Remove redundant hsl() from background color 2025-09-07 20:59:34 +09:00
Sidharth VinodandGitHub e943ffad49 Merge pull request #1788 from PetrLaskevic/develop
fix: light background with dark diagram theme when downloading SVG ([#1777](https://github.com/mermaid-js/mermaid-live-editor/issues/1777))
2025-09-03 12:37:03 +00:00
Sidharth VinodandGitHub 0e203d3ebd Merge pull request #1795 from mermaid-js/sidv/mainMenu
Update Menu items
2025-09-03 12:31:15 +00:00
Sidharth Vinod c532b06b1a fix: Hover 2025-09-03 17:23:19 +05:30
Sidharth Vinod 7d709fecc5 fix: Add hamburger icon 2025-09-03 17:22:09 +05:30
Sidharth VinodandGitHub b87538cfe4 Merge branch 'develop' into sidv/mainMenu 2025-09-03 17:14:09 +05:30
Sidharth VinodandGitHub 7172054b91 Merge pull request #1793 from mermaid-js/sidv/updateDeps2
Update Deps
2025-09-03 17:13:43 +05:30
Sidharth Vinod c70bfe8cfa chore: Upgrade vitest 2025-09-03 16:59:41 +05:30
Sidharth Vinod fe7d04457a chore: lint fix 2025-09-03 16:49:35 +05:30
Sidharth Vinod 2c717b670e chore: Update menu 2025-09-03 16:47:14 +05:30
Sidharth Vinod cb062d49b1 chore: Update utils 2025-09-03 16:42:08 +05:30
Sidharth Vinod def809f6bd chore: Remove postcss 2025-09-03 16:38:03 +05:30
Sidharth Vinod 7da0081ace Update tailwind and shadcn 2025-09-03 16:36:09 +05:30
Sidharth Vinod ee6f595880 chore: Remove .eslintrc.cjs configuration file 2025-09-03 01:43:25 +05:30
Sidharth Vinod d991139b0a chore: Update eslint 2025-09-03 01:41:51 +05:30
Sidharth Vinod eb9121de12 chore: Update deps 2025-09-02 22:57:16 +05:30
PetrLaskevic 2aaa2e872f fix: light background with dark diagram theme when downloading SVG ([#1777](https://github.com/mermaid-js/mermaid-live-editor/issues/1777)) 2025-08-21 20:04:28 +02:00
Sidharth VinodandGitHub 6f26aec073 Merge pull request #1786 from aschaber1/develop
ci(docker-publish): :contruction_worker: fix quotes
2025-08-20 12:29:32 +05:30
Alexander Schaber f9a9686d76 ci(docker-publish): :contruction_worker: fix quotes 2025-08-20 08:49:04 +02:00
Sidharth VinodandGitHub 6b7a4803a4 Merge pull request #1761 from aschaber1/develop
ci(docker-publish): add arm64 builds and attestations
2025-08-20 12:06:34 +05:30
Sidharth Vinod 5817555d91 chore: Update browserlist 2025-08-20 11:59:49 +05:30
Sidharth VinodandGitHub 5a7ee057c3 Merge pull request #1783 from mermaid-js/sidv/updateMermaid
chore: Update mermaid version
2025-08-20 11:51:41 +05:30
Sidharth Vinod 75126f180d test: Update svg size 2025-08-20 11:47:59 +05:30
Sidharth Vinod 74777b3791 chore: Update mermaid version 2025-08-20 11:42:39 +05:30
Alexander Schaber c9c235b85f ci(docker-publish): 👷 Update Docker publish workflow metadata tagging
- Replace manual version checks with built-in conditions in docker/metadata-action
2025-08-18 18:54:38 +02:00
Alexander Schaber d7ce5aeb42 ci(docker-publish): 👷 Update workflow to trigger certain steps only on push events
- Modify conditions to ensure `docker/login-action` runs only on push
- Add condition to gate `docker/publish-action` execution on push event
2025-08-18 18:18:10 +02:00
Alexander Schaber 8221a1ca48 ci(docker-publish): 👷 Enhance Docker publish workflow security and permissions
- Configure required permissions for package registry and attestations
- Enable build provenance attestation using actions/attest-build-provenance@v2
2025-08-18 18:18:10 +02:00
Alexander Schaber 0ebad9e207 ci(docker-publish): 👷 enable arm64 builds 2025-08-18 18:18:10 +02:00
Sidharth VinodandGitHub ce50b65cde Merge pull request #1775 from mermaid-js/sidv/privacy
chore: Highlight external services
2025-08-11 19:49:24 +05:30
Sidharth Vinod ceaeb1eb23 chore: Organize imports 2025-08-05 01:08:14 +05:30
Sidharth Vinod cf3957c2a2 feat: Example highlight in Privacy 2025-08-05 01:00:18 +05:30
Sidharth Vinod 9fd3a032a9 refactor: Simplify labelPrefix logic 2025-08-05 00:47:24 +05:30
Sidharth Vinod f8842d2ce2 chore: Remove redundant prefix 2025-08-05 00:43:06 +05:30
Sidharth Vinod ee14854139 fix: Remove hardcoded domains 2025-08-05 00:42:01 +05:30
Sidharth Vinod 4d50ec39f3 chore: Clarify privacy statement 2025-08-05 00:28:47 +05:30
Sidharth Vinod 41874f6d44 feat: Wrap all 3rd party services in tooltip 2025-08-05 00:27:37 +05:30
Sidharth VinodandGitHub 2275404e9d Merge pull request #1701 from mermaid-js/sidv/examples
Add examples for all diagrams
2025-08-04 07:19:03 +00:00
Sidharth VinodandGitHub 956a584322 Merge branch 'develop' into sidv/examples 2025-08-04 12:45:45 +05:30
Sidharth Vinod cc308b81c9 test: Update size to match new diagram 2025-08-04 12:45:26 +05:30
Sidharth VinodandGitHub 19b82c3d44 Merge pull request #1769 from mermaid-js/sidv/disposeEditorModels
fix: Dispose editor models
2025-08-02 19:40:47 +00:00
Sidharth Vinod ae928c3713 chore: Run tests on merge queue 2025-08-03 01:10:09 +05:30
Sidharth Vinod 1a2d752ca4 test: Update diagram names 2025-08-02 19:29:46 +05:30
Sidharth Vinod d3e2d137ea Merge branch 'develop' into sidv/examples
* develop:
  fix: #1766 hide external buttons if env var is not set
  chore: Rename layout to ts
  fix(deps): update dependency mermaid to ^11.9.0
2025-08-02 19:19:44 +05:30
Sidharth VinodandGitHub 1da5a3c193 Merge pull request #1757 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to ^11.9.0
2025-08-02 13:47:49 +00:00
Sidharth VinodandGitHub 273a2c06c0 Merge pull request #1770 from mermaid-js/sidv/disableExternalButtons
fix: Disable external buttons based on env
2025-08-02 13:47:08 +00:00
Sidharth Vinod 3ace70a439 fix: #1766 hide external buttons if env var is not set 2025-08-02 19:14:32 +05:30
Sidharth Vinod bc4b24f049 chore: Rename layout to ts 2025-08-02 19:13:46 +05:30
Sidharth Vinod b5c5c8e7b0 fix: Dispose editor models to avoid model already exists issue when switching from mobile to desktop view 2025-08-02 18:58:58 +05:30
renovate[bot]andGitHub 6b09a7ef2d fix(deps): update dependency mermaid to ^11.9.0 2025-07-21 01:24:27 +00:00
Sidharth Vinod 58aa3c0b67 Merge branch 'develop' into sidv/examples
* develop:
  feat: Add Treemap support (mermaid 11.8.1)
  chore: Update text-size
  Chore: Update banner ad
  Enable MC Links
  fix: Trace path
  fix: Hide AI Repair button in config tab
  fix(deps): update all non-major dependencies
  Update as per PR suggestion
  chore: Cleanup tagline logic
  chore(deps): update dependency lucide-svelte to ^0.507.0
  chore: Update announcement bar
2025-07-16 19:59:34 +05:30
Sidharth VinodandGitHub b577f4b2d4 Merge pull request #1750 from mermaid-js/sidv/11.8.1
feat: Add Treemap support (mermaid 11.8.1)
2025-07-10 07:03:31 +00:00
Sidharth Vinod 1dbf0ab5f6 feat: Add Treemap support (mermaid 11.8.1) 2025-07-07 21:16:54 +05:30
Sidharth VinodandGitHub 99dd67e359 Merge pull request #1742 from mermaid-js/rajat-ht/chore-update-banner-ad
Chore: Update banner ad
2025-06-20 10:24:47 +05:30
rajat-ht 7475360115 chore: Update text-size 2025-06-19 16:06:29 +05:30
rajat-ht 470c704542 Chore: Update banner ad 2025-06-18 20:01:43 +05:30
Sidharth VinodandGitHub 0683cdbaa2 Merge pull request #1726 from mermaid-js/sidv/hideAIRepair
fix: Hide AI Repair button in config tab
2025-05-27 13:53:04 +00:00
Sidharth Vinod 569f3ca232 Enable MC Links 2025-05-27 18:43:17 +05:30
Sidharth Vinod 4052221168 fix: Trace path 2025-05-27 18:25:18 +05:30
Sidharth VinodandAlois Klink 400f3ecf95 fix: Hide AI Repair button in config tab
Co-authored-by: Alois Klink <alois@aloisklink.com>
2025-05-26 23:14:45 +05:30
Sidharth VinodandGitHub 5ce27dd981 Merge pull request #1714 from mermaid-js/renovate/all-minor-patch
chore(deps): update dependency lucide-svelte to ^0.507.0
2025-05-06 05:13:58 +00:00
Sidharth VinodandGitHub 91fc0895b2 Merge pull request #1712 from mermaid-js/update-announcement-bar
chore: Update announcement bar
2025-05-06 10:43:47 +05:30
Sidharth VinodandGitHub 48c23374a4 Merge pull request #1713 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2025-05-06 10:43:31 +05:30
renovate[bot]andGitHub 829ee28cf9 fix(deps): update all non-major dependencies 2025-05-06 01:45:13 +00:00
rajat-ht 39876254ca Update as per PR suggestion 2025-05-05 23:50:20 +05:30
Sidharth Vinod 0b5a038c9d chore: Cleanup tagline logic 2025-05-05 21:40:43 +05:30
renovate[bot]andGitHub cc702bb556 chore(deps): update dependency lucide-svelte to ^0.507.0 2025-05-05 01:48:03 +00:00
rajat-ht c9e61440f4 chore: Update announcement bar 2025-05-02 18:18:52 +05:30
Sidharth Vinod 4d1c6e4702 Merge branch 'develop' into sidv/examples
* develop:
  chore(deps): update all non-major dependencies
  fix(deps): update all non-major dependencies
  fix: #1659 Frozen UI with comment regex
2025-05-02 02:39:16 +05:30
Sidharth VinodandGitHub 881648a87e Merge pull request #1703 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-05-02 00:58:57 +05:30
renovate[bot]andGitHub d3c8efd712 chore(deps): update all non-major dependencies 2025-05-01 16:01:56 +00:00
Sidharth VinodandGitHub 9777e1782f Merge pull request #1702 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2025-05-01 15:59:58 +00:00
renovate[bot]andGitHub 9391b17366 fix(deps): update all non-major dependencies 2025-05-01 13:15:09 +00:00
Sidharth VinodandGitHub 6b42be2841 Merge pull request #1710 from mermaid-js/sidv/1659
fix: #1659 Frozen UI with comment regex
2025-05-01 18:43:11 +05:30
Sidharth Vinod 6a07387688 fix: #1659 Frozen UI with comment regex 2025-05-01 17:57:40 +05:30
Sidharth Vinod d963baf96e Merge branch 'develop' into sidv/examples
* develop: (32 commits)
  feat: Support jsonSchema to validate config
  link: live version
  fix: Fallback page
  chore(deps): update dependency vite to v5.4.18 [security]
  fix: Fallback to sveltekit on 404 in GH pages
  chore(deps): update all non-major dependencies
  fix: New diagram link
  chore(deps): update dependency @sveltejs/kit to v2.20.6 [security]
  feat: Wait for fontawesome to load
  chore(deps): update all non-major dependencies
  feat: Support pan while zooming
  Update src/lib/util/panZoom.ts
  chore: Track PWA install device type
  chore: Track PWA installs
  chore: Add analytics for mobileViewToggle
  fix: Reduce gap in mobile
  fix: SVG size to avoid
  chore: Unify code update logic
  chore: Cleanup deps
  feat: Add syntax highlighting
  ...
2025-04-20 13:42:04 +05:30
Sidharth Vinod 70ea4f59a0 chore: Update @mermaid-js/examples version 2025-04-20 13:41:17 +05:30
Sidharth VinodandGitHub 4350d7e13a Merge pull request #1700 from mermaid-js/sidv/jsonSchema
feat: Support jsonSchema to validate config
2025-04-19 13:52:47 +00:00
Sidharth Vinod 2eaf136c75 feat: Support jsonSchema to validate config 2025-04-19 12:11:59 +05:30
Sidharth VinodandGitHub 5c35d5e589 Merge pull request #1698 from jsoref/docs/link
chore: Improve screen reader link for `live version`
2025-04-18 12:25:05 -07:00
Josh Soref e90ef3af9e link: live version
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-04-18 08:39:00 -04:00
Sidharth Vinod 0479af715b Merge branch 'develop' of https://github.com/mermaid-js/mermaid-live-editor into develop
* 'develop' of https://github.com/mermaid-js/mermaid-live-editor:
  chore(deps): update dependency vite to v5.4.18 [security]
  chore(deps): update all non-major dependencies
2025-04-16 16:26:21 +05:30
Sidharth Vinod 8aafb909a6 fix: Fallback page 2025-04-16 16:26:15 +05:30
Sidharth VinodandGitHub c9f6816fb6 Merge pull request #1685 from mermaid-js/renovate/npm-vite-vulnerability
chore(deps): update dependency vite to v5.4.18 [security]
2025-04-16 10:47:30 +00:00
renovate[bot]andGitHub e1e7552462 chore(deps): update dependency vite to v5.4.18 [security] 2025-04-16 10:47:13 +00:00
Sidharth VinodandGitHub 64787974d8 Merge pull request #1690 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-04-16 03:46:18 -07:00
Sidharth Vinod 1052420e34 fix: Fallback to sveltekit on 404 in GH pages 2025-04-16 16:14:47 +05:30
renovate[bot]andGitHub a54971bc56 chore(deps): update all non-major dependencies 2025-04-16 10:12:03 +00:00
Sidharth VinodandGitHub f27f4b336c Merge pull request #1691 from mermaid-js/renovate/npm-sveltejs-kit-vulnerability
chore(deps): update dependency @sveltejs/kit to v2.20.6 [security]
2025-04-16 03:10:35 -07:00
Sidharth Vinod bf25d3cc60 fix: New diagram link 2025-04-16 15:39:57 +05:30
renovate[bot]andGitHub 87af8a8c30 chore(deps): update dependency @sveltejs/kit to v2.20.6 [security] 2025-04-14 19:30:27 +00:00
Sidharth VinodandGitHub df6c1cceee Merge pull request #1688 from mermaid-js/sidv/faLoad
feat: Wait for fontawesome to load before rendering
2025-04-14 14:31:59 +00:00
Sidharth VinodandGitHub 987e09d8bc Merge pull request #1680 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-04-13 12:41:21 +00:00
Sidharth VinodandAlois Klink 6ae68fc546 feat: Wait for fontawesome to load
Co-authored-by: Alois Klink <alois@aloisklink.com>
2025-04-13 18:03:59 +05:30
renovate[bot]andGitHub 22444a2000 chore(deps): update all non-major dependencies 2025-04-13 08:27:39 +00:00
Sidharth VinodandGitHub 92f029830f Merge pull request #1687 from mermaid-js/sidv/mobile
feat: Add mobile support
2025-04-13 08:25:59 +00:00
Sidharth Vinod 8b13bf48e7 feat: Support pan while zooming 2025-04-13 13:50:30 +05:30
285baa4c83 Update src/lib/util/panZoom.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-13 01:16:56 -07:00
Sidharth Vinod 7c22576783 chore: Track PWA install device type 2025-04-13 13:41:26 +05:30
Sidharth Vinod e11017672f chore: Track PWA installs 2025-04-13 13:40:50 +05:30
Sidharth Vinod f4a736db62 chore: Add analytics for mobileViewToggle 2025-04-13 13:38:02 +05:30
Sidharth Vinod 2b242befe1 fix: Reduce gap in mobile 2025-04-13 13:34:51 +05:30
Sidharth VinodandAlois the eagle Klink 3039d5cb47 fix: SVG size to avoid
Co-authored-by: Alois "the eagle" Klink <alois@aloisklink.com>
2025-04-13 13:16:44 +05:30
Sidharth Vinod 4bf6dc0db6 chore: Unify code update logic 2025-04-13 13:12:30 +05:30
Sidharth Vinod 795663aafa chore: Cleanup deps 2025-04-13 12:01:15 +05:30
Sidharth Vinod bd1ca7e2ab feat: Add syntax highlighting 2025-04-13 11:57:53 +05:30
Sidharth Vinod 89f648ab8f chore: Update theme-color 2025-04-13 10:13:38 +05:30
Sidharth Vinod ff1a0d97d3 chore: Update og:image 2025-04-13 10:13:11 +05:30
Sidharth Vinod a11aae3573 chore: Update theme color 2025-04-13 10:02:11 +05:30
Sidharth Vinod 6c8402948a feat: Support touch pan & zoom 2025-04-13 09:51:39 +05:30
Sidharth Vinod a5df32931c fix: Responsive breakpoints, remove zoom icons in mobile 2025-04-13 09:51:22 +05:30
Sidharth Vinod 835231d291 fix: Promo bar alignment 2025-04-13 09:50:35 +05:30
Sidharth Vinod 4c31e1fe5d fix: Orientation in PWA manifest 2025-04-13 09:50:13 +05:30
Sidharth VinodandAlois Klink ebff808b31 fix: Privacy popup size
Co-authored-by: Alois Klink <alois@aloisklink.com>
2025-04-13 01:31:31 +05:30
Sidharth Vinod 224296c1d8 fix: padding 2025-04-13 01:27:38 +05:30
Sidharth Vinod 4ec52d9282 chore: Simplify pane logic 2025-04-13 01:25:18 +05:30
Sidharth Vinod 74ad9f44d0 feat: Add mobile support 2025-04-12 20:00:52 +05:30
Sidharth Vinod a0e54ff7bd chore: Use mermaid examples package to populate samples 2025-04-12 17:27:24 +05:30
Sidharth VinodandGitHub 96dddeb34d Merge pull request #1681 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-04-12 09:07:24 +00:00
renovate[bot]andGitHub 6528a0a26a chore(deps): update all non-major dependencies 2025-04-11 22:52:35 +00:00
Sidharth Vinod c530984c97 chore: Simplify URL params setting 2025-04-03 23:24:54 +05:30
Sidharth VinodandGitHub 643a7ca760 Merge pull request #1676 from mermaid-js/sidv/consolidateUTMSource
fix: Consolidate UTM source
2025-04-03 10:49:37 -07:00
Sidharth Vinod aa0d62ba11 fix: Consolidate UTM source 2025-04-03 23:16:20 +05:30
Sidharth VinodandGitHub 110b95d854 Merge pull request #1674 from mermaid-js/promo-links-v2
Update promo bar urls
2025-04-03 17:03:28 +00:00
Steph db0e97b988 update promo bar urls 2025-04-03 08:59:14 -07:00
Sidharth VinodandGitHub a2965bc3ca Merge pull request #1672 from mermaid-js/trackConversion
MC-2931 Add UTM params to MC URLs
2025-04-02 12:21:31 -07:00
Sidharth Vinod a24c66b554 Merge branch 'develop' into trackConversion
* develop:
  fix URLS
2025-04-02 23:54:51 +05:30
Sidharth Vinod b17e136225 MC-2931 Add utm params to MC URLs 2025-04-02 23:50:12 +05:30
Sidharth VinodandGitHub 3be0ad92f5 Merge pull request #1670 from mermaid-js/update-promo-links
Promo bars - fix URLS
2025-04-02 10:56:07 -07:00
Steph a35b1afa41 fix URLS 2025-04-02 08:59:02 -07:00
Sidharth VinodandGitHub 4d1a0ae07b Merge pull request #1667 from mermaid-js/renovate/npm-vite-vulnerability
chore(deps): update dependency vite to v5.4.16 [security]
2025-04-02 02:31:12 -07:00
renovate[bot]andGitHub b7b8d02dd0 chore(deps): update dependency vite to v5.4.16 [security] 2025-04-02 09:30:26 +00:00
Sidharth VinodandGitHub 12b0a27cf8 Merge pull request #1653 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-04-02 09:29:16 +00:00
renovate[bot]andGitHub cd5a858dcc chore(deps): update all non-major dependencies 2025-04-02 09:23:00 +00:00
Sidharth VinodandGitHub 034fccaebb Merge pull request #1663 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to ^11.6.0
2025-04-02 02:20:34 -07:00
Sidharth VinodandGitHub 8fd44b8d5b Merge pull request #1652 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-04-02 02:20:17 -07:00
Sidharth VinodandGitHub a5c4036927 Merge pull request #1665 from mermaid-js/announcement-bar
Update announcement bar
2025-04-02 02:19:48 -07:00
Sidharth Vinod 72e8e583a9 chore: Update fade delay 2025-04-02 14:46:59 +05:30
renovate[bot]andGitHub 5befc1221c fix(deps): update dependency mermaid to ^11.6.0 2025-04-02 09:15:27 +00:00
renovate[bot]andGitHub bc57d8660e chore(deps): update all non-major dependencies 2025-04-02 09:15:01 +00:00
Sidharth VinodandGitHub 5aa0d4a921 Merge branch 'develop' into announcement-bar 2025-04-02 02:14:03 -07:00
Sidharth Vinod 8320bb0651 fix: Type issue on lint 2025-04-02 14:43:45 +05:30
Sidharth VinodandGitHub 50180ed72a Merge branch 'develop' into announcement-bar 2025-04-02 01:58:16 -07:00
Sidharth Vinod 1f702e4960 fix: Update download image size to match CI 2025-04-02 14:24:58 +05:30
Steph af4ef4865b update announcement bar 2025-04-01 23:29:33 -07:00
Sidharth Vinod 37716245f7 fix #1655: Reset pan & zoom state if invalid 2025-03-26 09:36:28 -07:00
Sidharth VinodandGitHub 097f6f0f34 Merge pull request #1633 from mermaid-js/sidv/redesign
Redesign
2025-03-24 10:40:44 -07:00
Sidharth Vinod 0eda1c0b8b fix: Temp hack for zenuml 2025-03-24 10:34:42 -07:00
Sidharth Vinod 3124112006 fix: OptimizeDeps 2025-03-20 12:29:12 -07:00
Sidharth Vinod ee446eb2b8 fix: Dark mode code icon 2025-03-20 12:21:26 -07:00
Sidharth Vinod 866ca5f747 chore: Tweak popup text 2025-03-20 12:17:15 -07:00
Sidharth Vinod 41019ffa9a chore: Minor design tweaks 2025-03-20 12:07:28 -07:00
Sidharth Vinod 64e156085b test: Fix tests 2025-03-19 19:54:42 -07:00
Sidharth Vinod 501c983b99 test: Fix tests 2025-03-19 19:10:51 -07:00
Sidharth Vinod 3005deffc5 chore: Fix lint error 2025-03-18 13:00:08 -07:00
Sidharth Vinod f166156b86 Merge branch 'develop' into sidv/redesign
* develop:
  chore: Update script name
  chore: Remove cypress
  chore: Ignore cypress
  chore: Update test exclude
  chore: Tweak test config
  chore: Ignore cypress
  chore: Fix pw issues
  chore: Add playwright tests
2025-03-18 12:59:32 -07:00
Sidharth Vinod 9250ac3eaa feat: Change Switch to Button for grid and rough 2025-03-18 12:58:25 -07:00
Sidharth Vinod 6111a40dc3 chore: Center empty history text 2025-03-18 12:57:50 -07:00
Sidharth VinodandGitHub 93d02aadc0 Merge pull request #1649 from mermaid-js/sidv/pw
chore: Add playwright tests
2025-03-18 12:57:23 -07:00
Sidharth Vinod c97c6c2199 chore: Fix icon path 2025-03-18 11:55:05 -07:00
Sidharth Vinod b3093087f6 chore: Fix icon path 2025-03-18 11:54:09 -07:00
Sidharth Vinod d29ad6345b fix: Update MC Icon 2025-03-18 11:51:19 -07:00
Sidharth Vinod 7c725556b0 chore: Tweak render delay 2025-03-18 11:42:49 -07:00
Sidharth Vinod df26e5176f chore: Fix spacing in main menu 2025-03-18 11:16:30 -07:00
Sidharth Vinod 3b2c7caf07 fix: New diagram button, add MC links 2025-03-18 11:11:31 -07:00
Sidharth Vinod d039f6534a feat: Disable mermaidchart on unsupported diagrams 2025-03-18 10:51:08 -07:00
Sidharth Vinod 4d08026b77 fix: Add collapse icon to card 2025-03-18 10:50:37 -07:00
Sidharth Vinod 9d1df6126b chore: Fix timeline height 2025-03-18 10:12:11 -07:00
Sidharth Vinod ac308717f9 feat: Update theme icon animation 2025-03-17 23:45:14 -07:00
Sidharth Vinod 4c0de566f0 chore: Update script name 2025-03-17 22:16:30 -07:00
Sidharth Vinod 324dd0b4b2 chore: Remove cypress 2025-03-17 21:27:33 -07:00
Sidharth Vinod 5ed07028c7 chore: Ignore cypress 2025-03-17 20:04:15 -07:00
Sidharth Vinod 9f06808026 chore: Update test exclude 2025-03-17 20:03:07 -07:00
Sidharth Vinod 23c93e9e92 chore: Tweak test config 2025-03-17 19:59:34 -07:00
Sidharth Vinod 65faee55b7 chore: Ignore cypress 2025-03-17 19:51:38 -07:00
Sidharth Vinod 87e0b91543 chore: Fix pw issues 2025-03-17 19:51:04 -07:00
Sidharth Vinod bb084867fa chore: Add playwright tests 2025-03-17 19:47:13 -07:00
Sidharth Vinod b7da3f80f7 chore: Remove testing-library
It wasn't used for any relevant tests, and svelte5 support is spotty.
2025-03-17 18:15:23 -07:00
Sidharth Vinod 893f8625d5 chore: Update snapshots 2025-03-17 13:30:36 -07:00
Sidharth Vinod 355f1929b2 chore: Sort classes 2025-03-17 13:27:44 -07:00
Sidharth VinodandSteph 059a1a98ab Update docs button target
Co-authored-by: Steph <35910788+huynhicode@users.noreply.github.com>
2025-03-17 13:04:47 -07:00
Sidharth Vinod ea0fc8a621 Merge branch 'develop' into sidv/redesign
* develop:
  chore(deps): update all non-major dependencies
  fix(deps): update all non-major dependencies
2025-03-17 12:47:24 -07:00
Sidharth VinodandGitHub 631cc43f2b Merge pull request #1647 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-03-17 12:44:07 -07:00
Sidharth Vinod d0b2cf49da chore: Update imports, cleanup classes 2025-03-17 12:43:32 -07:00
Sidharth Vinod bbb1fd2dae chore: Update security verbiage 2025-03-17 12:42:38 -07:00
Sidharth Vinod 176698a299 chore: Set MC Popup to bottom 2025-03-17 12:42:04 -07:00
Sidharth Vinod 4dbe52126e chore: Cleanup editor 2025-03-17 12:41:40 -07:00
Sidharth Vinod 3fd8c43212 chore: Unify copy button 2025-03-17 12:41:18 -07:00
renovate[bot]andGitHub 75c4764da8 chore(deps): update all non-major dependencies 2025-03-17 19:10:34 +00:00
Sidharth VinodandGitHub afd5aaaada Merge pull request #1646 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2025-03-17 12:09:32 -07:00
Sidharth Vinod cd6f1ca613 fix: Reduce sync threshold, increase wait time 2025-03-17 11:19:05 -07:00
Sidharth Vinod 8da3cdedd9 fix: Use McWrapper, fix PNG export 2025-03-17 11:18:40 -07:00
Sidharth Vinod a72e13bb5c fix: FA icons 2025-03-17 10:16:05 -07:00
Sidharth Vinod 9ddc37add5 chore: Fix png export background 2025-03-17 09:39:51 -07:00
Sidharth Vinod cb267911f5 chore: Fix primary dark mode 2025-03-17 09:30:14 -07:00
renovate[bot]andGitHub d6eb0533c6 fix(deps): update all non-major dependencies 2025-03-17 16:23:21 +00:00
Sidharth Vinod b76fa8106e Merge branch 'develop' into sidv/redesign
* develop:
  chore: Update deps
  chore(deps): update all non-major dependencies
  chore: Prettier ignore pnpm-lock
  chore: Update build dependencies
  chore: Update actions
  chore: Update node version
  chore: Switch from yarn to pnpm
  fix(deps): update dependency mermaid to v11.5.0
2025-03-17 09:21:04 -07:00
Sidharth VinodandGitHub 4a8dbfadba Merge pull request #1644 from mermaid-js/sidv/pnpm
chore: Switch from yarn to pnpm
2025-03-16 19:49:53 -07:00
Sidharth Vinod 15df6a2fcb chore: Update deps 2025-03-16 19:47:10 -07:00
Sidharth Vinod 506128c5df Merge branch 'develop' into sidv/pnpm
* develop:
  chore(deps): update all non-major dependencies
  fix(deps): update dependency mermaid to v11.5.0
2025-03-16 19:45:42 -07:00
Sidharth VinodandGitHub 1195ea94b3 Merge pull request #1642 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-03-16 19:43:06 -07:00
renovate[bot]andGitHub 90894781cd chore(deps): update all non-major dependencies 2025-03-17 02:42:56 +00:00
Sidharth VinodandGitHub 18cd3c4591 Merge pull request #1643 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to v11.5.0
2025-03-16 19:42:04 -07:00
Sidharth Vinod 4091da60c6 chore: Prettier ignore pnpm-lock 2025-03-16 19:41:19 -07:00
Sidharth Vinod 4a143ae8e2 chore: Update build dependencies 2025-03-16 19:40:25 -07:00
Sidharth Vinod 2159375e81 chore: Update actions 2025-03-16 19:37:05 -07:00
Sidharth Vinod 5742878799 chore: Update node version 2025-03-16 19:33:45 -07:00
Sidharth Vinod 592c3addac chore: Switch from yarn to pnpm 2025-03-16 19:19:10 -07:00
renovate[bot]andGitHub 9beb411c14 fix(deps): update dependency mermaid to v11.5.0 2025-03-17 00:35:39 +00:00
Sidharth Vinod 21be4caf82 Merge branch 'develop' into sidv/redesign
* develop:
  chore: Fix coverage dependency
  chore: sort gitignore
  chore: Add redirect to home on 404 error
  chore: Update subdomain
  chore: Update import
  chore: Remove redundant config
  chore: Add CNAME
  chore: Remove lint after build
  chore: Use official action to deploy to pages
  build: update Browserslist db
  fix: use `<output>` and `<pre>` to format errors
  Revert "feat: Sanitize error message"
2025-03-16 17:34:39 -07:00
Sidharth Vinod 6e7c7467a3 fix: PanZoom 2025-03-16 17:32:14 -07:00
Sidharth VinodandGitHub b5be8ca4fc Merge pull request #1639 from mermaid-js/browserslist-update
build: update Browserslist db
2025-03-15 20:35:47 -07:00
Sidharth Vinod 66b4a427b7 chore: Fix coverage dependency 2025-03-15 20:35:03 -07:00
Sidharth Vinod 3a65f2382d chore: sort gitignore 2025-03-15 20:31:15 -07:00
Sidharth Vinod c677afc04a chore: Add redirect to home on 404 error 2025-03-15 20:30:41 -07:00
Sidharth Vinod 4973083133 chore: Update subdomain 2025-03-15 20:09:21 -07:00
Sidharth Vinod 9288094c30 chore: Update import 2025-03-15 20:00:13 -07:00
Sidharth Vinod fe428c7cea chore: Remove redundant config 2025-03-15 19:55:50 -07:00
Sidharth Vinod 423fe7e0d6 chore: Add CNAME 2025-03-15 19:49:00 -07:00
Sidharth Vinod c9905606a1 chore: Remove lint after build 2025-03-15 19:32:37 -07:00
Sidharth VinodandGitHub e0cbcd21a8 Merge pull request #1637 from mermaid-js/release-promotion
Release live editor
2025-03-15 19:29:41 -07:00
Sidharth Vinod 214f6d0862 chore: Use official action to deploy to pages 2025-03-15 19:25:47 -07:00
GitHub Action 80526e58f7 build: update Browserslist db 2025-03-15 02:45:22 +00:00
Sidharth Vinod b2bb5b2bef chore: Minor cleanup 2025-03-14 19:37:25 -07:00
Sidharth Vinod c9efb9e017 fix: body style 2025-03-14 19:06:21 -07:00
Sidharth Vinod 34b078e7e8 fix: Grid color 2025-03-14 19:06:06 -07:00
Sidharth VinodandGitHub e9bc0c24de Merge pull request #1496 from aloisklink/fix/stop-parsing-syntax-error-as-html
Use `<output>` and `<pre>` to format mermaid errors
2025-03-15 07:30:21 +05:30
Sidharth Vinod bfc7535d54 chore: Remove outOfSync 2025-03-14 18:50:25 -07:00
Sidharth Vinod dcdc19d15e chore: Simplify class def 2025-03-14 18:16:44 -07:00
Sidharth Vinod a9db7e0513 Merge branch 'develop' into sidv/redesign
* develop:
  fix: Flickering when rendering, PanZoom & Rough mode combined
  chore: Simplify queueing logic
  fix: Blank screen on initial load
2025-03-14 18:14:36 -07:00
Sidharth VinodandGitHub d92c1e5e4c Merge pull request #1636 from mermaid-js/release-promotion
Release live editor
2025-03-15 06:39:13 +05:30
Sidharth Vinod 99255ab693 Merge branch 'develop' into pr/aloisklink/1496
* develop: (101 commits)
  fix: Flickering when rendering, PanZoom & Rough mode combined
  chore: Simplify queueing logic
  fix: Blank screen on initial load
  chore: Add delay for playground toggle
  fix: Record correct diagram type, send playground toggle event immediately.
  chore(deps): update dependency autoprefixer to v10.4.21
  chore(deps): update all non-major dependencies
  chore(deps): update dependency typescript to v5.8.2
  chore(deps): update all non-major dependencies
  build: update Browserslist db
  Use netlify to redirect users
  chore: Add sitemap to robots.txt
  fix: Add redirect for /index.html
  chore: Add sitemap
  fix: Do not index /view page
  chore(deps): update all non-major dependencies
  feat: Pause banner animation when mouse is on the banner
  fix: Layout for banner
  use snippets
  update urls and styling
  ...
2025-03-14 18:08:52 -07:00
Sidharth VinodandGitHub ec40c0369c Merge pull request #1635 from mermaid-js/sidv/panZoomRough
fix: Flickering when rendering, PanZoom & Rough mode combined
2025-03-15 06:36:16 +05:30
Sidharth Vinod a56be6ae14 fix: Flickering when rendering, PanZoom & Rough mode combined 2025-03-14 18:00:12 -07:00
Sidharth VinodandGitHub e29170a1aa Merge pull request #1634 from mermaid-js/release-promotion
Release live editor
2025-03-15 06:02:18 +05:30
Sidharth Vinod d7cd301e15 chore: Simplify queueing logic 2025-03-14 17:26:29 -07:00
Sidharth Vinod 393a5f659e fix: Blank screen on initial load
Queued the rendering pipeline
2025-03-14 15:51:37 -07:00
Sidharth Vinod 71a336c41f fix: Rendering race condition 2025-03-14 15:40:14 -07:00
Sidharth Vinod e394525376 fix: Diagram rendering 2025-03-14 15:20:12 -07:00
Sidharth Vinod 6175561449 chore: Simplify reactivity for tabs 2025-03-14 13:58:31 -07:00
Sidharth Vinod feb3ba88a5 format: eslint + prettier 2025-03-14 11:24:44 -07:00
Sidharth Vinod da75a4951a format: prettier 2025-03-14 11:03:35 -07:00
Sidharth Vinod 02ef7eab78 chore: Simplify docs button 2025-03-14 11:03:10 -07:00
Sidharth Vinod f951ad055b feat: Remove autoSync, add grid toggle 2025-03-13 23:35:59 -07:00
Sidharth Vinod 9e453e84f6 feat: MermaidChart tooltip 2025-03-13 23:17:06 -07:00
Sidharth Vinod 4e7c42f59a fix: Icons 2025-03-13 23:16:56 -07:00
Sidharth Vinod 44e44ede97 feat: MermaidChart tooltip 2025-03-13 22:03:16 -07:00
Sidharth Vinod b624ecb859 feat: Error popup 2025-03-13 20:21:38 -07:00
Sidharth Vinod b13e351460 feat: Full screen view 2025-03-13 19:59:21 -07:00
Sidharth Vinod 30abb2303b fix: Imports 2025-03-13 19:59:08 -07:00
Sidharth Vinod bb29c4af7f fix: Share modal 2025-03-13 19:51:46 -07:00
Sidharth Vinod 30cb61b966 feat: Share modal 2025-03-13 19:50:41 -07:00
Sidharth Vinod 7abe8f82b1 format: prettier 2025-03-13 17:42:52 -07:00
Sidharth Vinod 393e7efbda chore: Simplify history pane 2025-03-13 17:03:19 -07:00
Sidharth Vinod 83715bb250 Merge branch 'develop' into sidv/redesign
* develop:
  chore: Add delay for playground toggle
  fix: Record correct diagram type, send playground toggle event immediately.
  chore(deps): update dependency autoprefixer to v10.4.21
  chore(deps): update all non-major dependencies
  chore(deps): update dependency typescript to v5.8.2
  chore(deps): update all non-major dependencies
  build: update Browserslist db
2025-03-13 15:21:54 -07:00
Sidharth Vinod 6a55d67c4e feat: Tweak links 2025-03-13 15:18:38 -07:00
Sidharth Vinod f16180ec59 feat: Responsiveness 2025-03-13 15:14:42 -07:00
Sidharth Vinod 2883031609 feat: MainMenu 2025-03-13 14:47:13 -07:00
Sidharth Vinod b921101cd5 feat: MainMenu 2025-03-13 13:39:49 -07:00
Sidharth Vinod 1e89e2b4be feat: Privacy popup 2025-03-13 13:22:22 -07:00
Sidharth Vinod 050dafa409 feat: Navbar 2025-03-13 13:07:13 -07:00
Sidharth Vinod 0532e403bf feat: Add history section 2025-03-13 13:01:47 -07:00
Sidharth Vinod 0f3cfbdeac fix: Card icons 2025-03-13 10:26:31 -07:00
Sidharth Vinod 21644297d1 fix: Toolbar spacing and colors 2025-03-13 10:26:14 -07:00
Sidharth Vinod 3d279652b4 format: prettier 2025-03-13 10:23:38 -07:00
Sidharth Vinod c3faf2ff42 fix: Reduce overlap of diagram and toolbars 2025-03-13 10:22:36 -07:00
Sidharth Vinod 72616fca7a fix: Responsiveness of editor 2025-03-13 10:21:02 -07:00
Sidharth VinodandGitHub 51b187be6a Merge pull request #1620 from mermaid-js/release-promotion
Release live editor
2025-03-13 21:54:54 +05:30
Sidharth VinodandGitHub e91e190601 Merge pull request #1629 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-03-13 16:24:09 +00:00
Sidharth VinodandGitHub 40c66ce6e3 Merge pull request #1628 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update dependency autoprefixer to v10.4.21
2025-03-13 16:23:53 +00:00
Sidharth VinodandGitHub 57fc6ede1e Merge pull request #1632 from mermaid-js/sidv/FixAnalytics
fix: Analytics
2025-03-13 21:53:18 +05:30
Sidharth Vinod 62f60867f2 chore: Add delay for playground toggle 2025-03-13 09:22:30 -07:00
Sidharth Vinod 91331bda7e fix: Record correct diagram type, send playground toggle event immediately. 2025-03-13 09:11:46 -07:00
renovate[bot]andGitHub 86c7425271 chore(deps): update dependency autoprefixer to v10.4.21 2025-03-13 15:50:33 +00:00
Sidharth Vinod 73dd347bab Merge branch 'develop' of https://github.com/mermaid-js/mermaid-live-editor into develop
* 'develop' of https://github.com/mermaid-js/mermaid-live-editor:
  chore(deps): update dependency typescript to v5.8.2
  chore(deps): update all non-major dependencies
  build: update Browserslist db
2025-03-13 08:46:19 -07:00
renovate[bot]andGitHub b9060ddc31 chore(deps): update all non-major dependencies 2025-03-13 15:02:07 +00:00
Sidharth Vinod fcc7a03650 feat: Migrate to Material icons with Iconify, partial 2025-03-12 21:17:36 -07:00
Sidharth Vinod 2fae6f7798 feat: Floating toolbars 2025-03-11 22:32:59 -07:00
Sidharth Vinod d8cdf2f42c feat: Floating toolbars 2025-03-10 21:28:08 -07:00
Sidharth Vinod f608761fa2 feat: Dark mode 2025-03-10 21:27:50 -07:00
Sidharth Vinod e33aed9cc6 fix: Action panel 2025-03-07 17:00:16 +09:00
Sidharth Vinod 3c2acccf52 fix: Action & Preset stacking 2025-03-07 11:13:30 +07:00
Sidharth Vinod 0cd51750b8 fix: Imports 2025-03-07 03:04:42 +05:30
Sidharth Vinod f6ce710c1b fix: Theme, actions 2025-03-07 03:04:24 +05:30
Sidharth Vinod 433a301776 fix: Preset spacing 2025-03-06 23:17:08 +05:30
Sidharth Vinod bb184a20ca fix: Card padding 2025-03-06 18:53:41 +05:30
Sidharth Vinod 7ca468d42f fix: Actions panel 2025-03-06 18:48:40 +05:30
Sidharth Vinod 5d2f1e4973 fix: Remove overlap of promo 2025-03-06 18:21:18 +05:30
Sidharth Vinod db2e8e0297 fix: Change font 2025-03-06 18:15:20 +05:30
Sidharth Vinod 235d3c252d fix: Sample diagrams 2025-03-06 18:15:08 +05:30
Sidharth Vinod 9934429c4f fix: Cards 2025-03-06 14:30:18 +05:30
Sidharth Vinod 1f55da1f45 fix: Resize handle, diagram view 2025-03-06 12:41:52 +05:30
Sidharth Vinod 9c5ef434dc fix: Resizing, card 2025-03-06 12:24:19 +05:30
Sidharth Vinod 4968e224ae chore: Migrate navbar 2025-03-06 11:58:43 +05:30
Sidharth VinodandGitHub ac812ca1e4 Merge pull request #1619 from mermaid-js/renovate/all-minor-patch
chore(deps): update dependency typescript to v5.8.2
2025-03-03 12:31:36 +00:00
renovate[bot]andGitHub c1ecd9cd04 chore(deps): update dependency typescript to v5.8.2 2025-03-03 06:25:23 +00:00
Sidharth VinodandGitHub 68371dae63 Merge pull request #1617 from mermaid-js/browserslist-update
build: update Browserslist db
2025-03-03 06:24:29 +00:00
Sidharth VinodandGitHub 009e4f2390 Merge pull request #1618 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-03-03 06:24:19 +00:00
renovate[bot]andGitHub 50696445cd chore(deps): update all non-major dependencies 2025-03-03 02:46:19 +00:00
GitHub Action bd7ca5a25a build: update Browserslist db 2025-03-01 02:52:22 +00:00
Sidharth Vinod b8aa272bc6 fix: Promo bar 2025-03-01 00:17:09 +05:30
Sidharth Vinod 66566bd6af chore: Add shadcn/button 2025-03-01 00:16:50 +05:30
Sidharth Vinod 3075b63d4a chore: Add shadcn 2025-03-01 00:16:38 +05:30
Sidharth VinodandGitHub c20996488d Merge pull request #1615 from mermaid-js/release-promotion
Release live editor
2025-02-25 07:47:29 +01:00
Sidharth VinodandGitHub 0871de7838 Merge pull request #1613 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-02-25 07:41:56 +01:00
Sidharth VinodandGitHub 20cadcab79 Merge pull request #1614 from mermaid-js/sidv/indexing
Indexing fixes
2025-02-25 07:41:44 +01:00
Sidharth Vinod fb5894e093 Use netlify to redirect users 2025-02-25 12:08:54 +05:30
Sidharth Vinod fa0498f75d chore: Add sitemap to robots.txt 2025-02-25 08:54:26 +05:30
Sidharth Vinod cdc7f9faa4 fix: Add redirect for /index.html 2025-02-25 08:50:03 +05:30
Sidharth Vinod 51591ba075 chore: Add sitemap 2025-02-25 08:49:46 +05:30
Sidharth Vinod 78ccc2ff4c fix: Do not index /view page 2025-02-25 08:49:25 +05:30
renovate[bot]andGitHub fe8e51fa79 chore(deps): update all non-major dependencies 2025-02-24 19:55:23 +00:00
Sidharth VinodandGitHub e0b660d56b Merge pull request #1610 from mermaid-js/release-promotion
Release live editor
2025-02-21 22:43:23 +05:30
Sidharth VinodandGitHub 03404e8e59 Merge pull request #1611 from mermaid-js/promos
Update promos
2025-02-21 17:06:40 +00:00
Sidharth Vinod edc36351d4 feat: Pause banner animation when mouse is on the banner 2025-02-21 21:23:21 +05:30
Sidharth Vinod 66c155bab7 fix: Layout for banner 2025-02-21 21:02:47 +05:30
Steph ee3f4420f7 use snippets 2025-02-21 05:41:03 -08:00
Sidharth VinodandGitHub b35ce74065 Merge pull request #1606 from mermaid-js/browserslist-update
build: update Browserslist db
2025-02-21 13:44:41 +05:30
Sidharth VinodandGitHub 31616cadc0 Merge pull request #1607 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-02-21 08:14:21 +00:00
Sidharth VinodandGitHub 7389a9fdad Merge pull request #1608 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2025-02-21 13:44:08 +05:30
Steph 96c8333bbb update urls and styling 2025-02-20 16:01:49 -08:00
renovate[bot]andGitHub f45b04e16a chore(deps): update all non-major dependencies 2025-02-20 12:31:22 +00:00
renovate[bot]andGitHub 5c0c471ff6 chore(deps): update all non-major dependencies 2025-02-18 18:48:44 +00:00
GitHub Action fccfd6fbe2 build: update Browserslist db 2025-02-15 02:40:36 +00:00
Sidharth VinodandGitHub 9a70c08a6f Merge pull request #1602 from mermaid-js/release-promotion
Release live editor
2025-02-14 13:46:19 +05:30
Sidharth VinodandGitHub 77234d95b3 Merge pull request #1605 from mermaid-js/update-promos
Update promos
2025-02-14 13:45:46 +05:30
Sidharth Vinod 1df59ae5dd chore: Cleanup class 2025-02-14 13:39:04 +05:30
Sidharth Vinod 7e043e7e7e Merge branch 'update-promos' of https://github.com/mermaid-js/mermaid-live-editor into update-promos
* 'update-promos' of https://github.com/mermaid-js/mermaid-live-editor:
  update interval
2025-02-14 13:27:30 +05:30
Sidharth Vinod 8ddf56191d chore: Add padding to link 2025-02-14 13:27:22 +05:30
Steph 942840fb62 update interval 2025-02-13 15:41:19 -08:00
Sidharth Vinod bb224b486a fix: Position of text when switching, data structure for links 2025-02-13 18:26:26 +05:30
Steph 23bb5d63dc update urls 2025-02-13 03:57:41 -08:00
Steph abf80da2c1 wip 2025-02-13 03:46:28 -08:00
Sidharth VinodandGitHub 276b49e554 Merge pull request #1601 from mermaid-js/sidv/hidePromotions
Timebox Hidden Promotions
2025-02-05 19:04:05 +05:30
Sidharth VinodandGitHub bb7600f8c5 Merge pull request #1579 from mermaid-js/renovate/all-minor-patch
fix(deps): update all non-major dependencies (minor)
2025-02-04 23:44:47 +05:30
Sidharth Vinod 0b1dd4b06d chore: Cleanup 2025-02-04 23:41:48 +05:30
Sidharth Vinod dc815cdc4d feat: Add hide duration for promotions 2025-02-04 23:39:59 +05:30
Sidharth VinodandGitHub c5eaf340b4 Merge pull request #1595 from mermaid-js/release-promotion
Release live editor
2025-02-04 21:00:21 +05:30
renovate[bot]andGitHub 4cead1811f fix(deps): update all non-major dependencies 2025-02-04 15:28:23 +00:00
Sidharth Vinod 1b7065609b Merge branch 'master' into develop
* master:
  update end date
  update-promo
2025-02-04 20:57:19 +05:30
Sidharth VinodandGitHub e98a7b8647 Merge pull request #1585 from mermaid-js/renovate/patch-mermaid-packages
fix(deps): update dependency mermaid to v11.4.1
2025-02-04 20:56:23 +05:30
renovate[bot]andGitHub 922f82cc31 fix(deps): update dependency mermaid to v11.4.1 2025-02-04 15:26:02 +00:00
Sidharth VinodandGitHub f5f5038c69 Merge pull request #1599 from mermaid-js/browserslist-update
build: update Browserslist db
2025-02-04 20:55:23 +05:30
Sidharth VinodandGitHub d6288608be Merge pull request #1600 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-02-04 20:55:09 +05:30
renovate[bot]andGitHub d36571491f chore(deps): update all non-major dependencies 2025-02-04 12:46:37 +00:00
Sidharth Vinod a7e14c77cb Update banner timing 2025-02-04 18:03:13 +05:30
GitHub Action dab0e8880f build: update Browserslist db 2025-02-01 02:41:49 +00:00
Sidharth VinodandGitHub f2e2fc4e11 Merge pull request #1583 from mermaid-js/renovate/npm-sveltejs-kit-vulnerability
chore(deps): update dependency @sveltejs/kit to v2.8.3 [security]
2025-01-27 19:05:15 +05:30
Sidharth VinodandGitHub 54b7655015 Merge pull request #1592 from mermaid-js/renovate/npm-vite-vulnerability
chore(deps): update dependency vite to v5.4.12 [security]
2025-01-27 19:05:01 +05:30
Sidharth VinodandGitHub 681f434574 Merge pull request #1596 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2025-01-27 19:04:49 +05:30
renovate[bot]andGitHub 2d7252c640 chore(deps): update all non-major dependencies 2025-01-27 00:36:08 +00:00
renovate[bot]andGitHub e97c727170 chore(deps): update dependency vite to v5.4.12 [security] 2025-01-26 04:39:11 +00:00
renovate[bot]andGitHub 4233a1e21e chore(deps): update dependency @sveltejs/kit to v2.8.3 [security] 2025-01-26 04:39:02 +00:00
Sidharth VinodandGitHub 8735b0f52b Merge pull request #1575 from mermaid-js/browserslist-update
build: update Browserslist db
2025-01-26 10:07:45 +05:30
Sidharth VinodandGitHub 975e34b2fd Merge pull request #1578 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2025-01-26 10:07:34 +05:30
renovate[bot]andGitHub c77fc8c979 fix(deps): update all non-major dependencies 2025-01-23 20:30:38 +00:00
GitHub Action f3b62ce674 build: update Browserslist db 2025-01-15 02:39:27 +00:00
Sidharth VinodandGitHub cc562549a9 Merge pull request #1590 from mermaid-js/update-promo-bar
Update promo bar
2025-01-06 12:17:16 +05:30
Steph 2438363c03 update end date 2025-01-03 12:02:44 -08:00
Steph bd9625a3f9 update-promo 2025-01-03 10:39:00 -08:00
GitHub Action 7df3df748b build: update Browserslist db 2025-01-01 02:51:07 +00:00
GitHub Action e37253645c build: update Browserslist db 2024-12-15 03:02:55 +00:00
GitHub Action 502e0b5fc3 build: update Browserslist db 2024-12-01 03:09:36 +00:00
GitHub Action 13d7f878f1 build: update Browserslist db 2024-11-15 02:52:27 +00:00
Sidharth VinodandGitHub bb0ab7c7e2 Merge pull request #1574 from mermaid-js/release-promotion
Release live editor
2024-11-14 20:44:46 +05:30
Sidharth VinodandGitHub 939ef8b56e Merge pull request #1572 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2024-11-14 20:42:14 +05:30
renovate[bot]andGitHub 1e6e994413 chore(deps): update all non-major dependencies 2024-11-14 15:05:37 +00:00
Sidharth VinodandGitHub ba1c0da871 Merge pull request #1573 from mermaid-js/playground
MC Playground Toggle
2024-11-14 20:34:40 +05:30
Sidharth Vinod 5b103acb11 chore: Update snapshots 2024-11-14 19:09:42 +05:30
Sidharth Vinod b658ceedc9 chore: Update URL 2024-11-14 17:27:10 +05:30
Sidharth Vinod dc114d55c9 fix: Unit test 2024-11-14 13:53:28 +05:30
Sidharth Vinod 90716ef8d5 chore: Update labels 2024-11-14 13:50:46 +05:30
Sidharth Vinod f954dfab61 feat: Retain toggle label positions 2024-11-13 21:07:57 +05:30
Sidharth VinodandGitHub 877cbdb6f9 Merge pull request #1570 from mermaid-js/release-promotion
Release live editor
2024-11-12 00:14:29 +05:30
Sidharth Vinod 09e705c3ae Merge branch 'develop' into playground
* develop:
  fix: Add fallback for state
  update whiteboard tagline
  remove august promo
  update dates
  update taglines
  remove product hunt promo
  add and update product hunt promo for whiteboard
  add product hunt badge to navbar
2024-11-11 16:56:59 +05:30
Sidharth Vinod 83f3c07f10 Merge branch 'master' into develop
* master:
  update whiteboard tagline
  remove august promo
  update dates
  update taglines
  remove product hunt promo
  add and update product hunt promo for whiteboard
  add product hunt badge to navbar
2024-11-11 16:55:26 +05:30
Sidharth Vinod 3e3a0162bf fix: Add fallback for state 2024-11-11 16:54:03 +05:30
Sidharth Vinod 8c1c348f68 chore: Cleanup 2024-11-11 01:47:43 +05:30
Sidharth Vinod f7f3e35fcf feat: Add dropdown navbar items 2024-11-11 01:45:12 +05:30
Sidharth Vinod 5acd3faacc fix: Add fallback for state 2024-11-08 22:46:54 +05:30
Sidharth Vinod 4c24b52e12 feat: Add mermaidchart playground toggle 2024-11-08 22:30:47 +05:30
Sidharth VinodandGitHub 8fd965624f Merge pull request #1565 from mermaid-js/sidv/svelte5
Svelte 5
2024-11-08 22:16:16 +05:30
Sidharth Vinod e2d7fd46ac chore: Update action versions 2024-11-08 22:00:52 +05:30
Sidharth Vinod 09616ed156 chore: Fix unit test 2024-11-08 21:42:57 +05:30
Sidharth Vinod 118f03385e Merge branch 'develop' into sidv/svelte5
* develop:
  chore(deps): update all non-major dependencies
  chore(deps): update dependency vite to v5.4.6 [security]
  fix(deps): update all non-major dependencies
  test: increase file size limit
  fix(deps): update dependency mermaid to v11.4.0
  build: update Browserslist db
2024-11-08 21:36:10 +05:30
Sidharth VinodandGitHub ee156c7e69 Merge pull request #1543 from mermaid-js/renovate/npm-vite-vulnerability
chore(deps): update dependency vite to v5.4.6 [security]
2024-11-08 21:19:18 +05:30
Sidharth VinodandGitHub e80f0865f5 Merge pull request #1539 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2024-11-08 21:19:01 +05:30
renovate[bot]andGitHub aa9e9c8a23 chore(deps): update all non-major dependencies 2024-11-08 10:15:08 +00:00
renovate[bot]andGitHub 75697b7ce1 chore(deps): update dependency vite to v5.4.6 [security] 2024-11-08 10:14:47 +00:00
Sidharth VinodandGitHub 98f7b1b3d1 Merge pull request #1546 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2024-11-08 15:44:00 +05:30
renovate[bot]andGitHub 84db2c7c6a fix(deps): update all non-major dependencies 2024-11-08 10:01:04 +00:00
Sidharth VinodandGitHub d8401a8f13 Merge pull request #1563 from mermaid-js/release-promotion
Release live editor
2024-11-08 15:29:57 +05:30
Sidharth VinodandGitHub 6816bd986c Merge pull request #1567 from mermaid-js/browserslist-update
build: update Browserslist db
2024-11-08 15:29:32 +05:30
Sidharth VinodandGitHub 95c858936d Merge pull request #1568 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to v11.4.0
2024-11-08 15:29:20 +05:30
Sidharth Vinod c67b84a458 test: increase file size limit 2024-11-08 14:52:57 +05:30
Sidharth VinodandGitHub 765c35265f Merge pull request #1564 from mermaid-js/update-whiteboard-tagline
Promo bar: update Whiteboard tagline
2024-11-06 11:39:57 +05:30
renovate[bot]andGitHub ecc34aabc5 fix(deps): update dependency mermaid to v11.4.0 2024-11-04 02:04:17 +00:00
GitHub Action 6517053b8e build: update Browserslist db 2024-11-01 02:54:51 +00:00
Sidharth Vinod 190bac8c70 Svelte 5 migration 2024-10-30 23:38:20 +05:30
Steph cfdb4fbf61 update whiteboard tagline 2024-10-30 09:20:01 -07:00
Sidharth Vinod 8cf4ac432d Allow props 2024-10-30 01:33:03 +05:30
Sidharth VinodandGitHub 5ba65bc032 Merge pull request #1557 from mermaid-js/browserslist-update
build: update Browserslist db
2024-10-30 00:33:48 +05:30
Sidharth VinodandGitHub 2fe3df56b7 Merge pull request #1559 from mermaid-js/promo-updates
Remove Product Hunt promos and Update taglines
2024-10-22 20:10:23 +05:30
Steph 659e82e9ae remove august promo 2024-10-22 07:35:08 -07:00
Steph 76c937a878 update dates 2024-10-22 00:12:07 -07:00
Steph 98cbcf1abe update taglines 2024-10-22 00:07:40 -07:00
Steph 6bdabf9325 remove product hunt promo 2024-10-21 23:32:40 -07:00
GitHub Action 4c06db65f4 build: update Browserslist db 2024-10-15 02:49:53 +00:00
Sidharth VinodandGitHub cc963db471 Merge pull request #1554 from mermaid-js/release-promotion
Release live editor
2024-10-05 09:49:22 +05:30
Sidharth VinodandGitHub 8190a9433f Merge pull request #1528 from bluprince13/feature/1511_ability-to-disable-data-share-buttons
Disable buttons/links to share data with other websites depending on environment variables
2024-10-04 22:55:04 +05:30
Sidharth VinodandGitHub 4415849a6d Merge branch 'develop' into feature/1511_ability-to-disable-data-share-buttons 2024-10-04 14:45:39 +05:30
Sidharth VinodandGitHub d5123c6a26 Merge pull request #1553 from mermaid-js/release-promotion
Release live editor
2024-10-03 21:19:19 +05:30
Sidharth Vinod 60e2b307e9 Bump mermaid@11.3.0 2024-10-03 21:17:14 +05:30
Sidharth VinodandGitHub 1c04d7d77b Merge pull request #1551 from mermaid-js/browserslist-update
build: update Browserslist db
2024-10-03 21:16:21 +05:30
Sidharth VinodandGitHub f59a2dc6d7 Merge pull request #1552 from mermaid-js/promo-whiteboard
Product Hunt promo - Whiteboard
2024-10-03 15:55:12 +05:30
Steph 3b0fae39c9 add and update product hunt promo for whiteboard 2024-10-03 01:50:54 -07:00
GitHub Action 6e262b3ea4 build: update Browserslist db 2024-10-01 02:54:11 +00:00
Sidharth Vinod 267c6160be chore: Remove unused destructuring 2024-09-29 12:46:27 +05:30
8fb2114831 Update src/lib/util/promos/promo.ts
Co-authored-by: Sidharth Vinod <github@sidharth.dev>
2024-09-28 18:19:51 +01:00
7bae587c8c Update src/routes/edit/+page.svelte
Co-authored-by: Sidharth Vinod <github@sidharth.dev>
2024-09-28 18:19:39 +01:00
Vipin Ajayakumar f66331852e Disable buttons/links to share data with other websites depending on env variables 2024-09-28 16:12:43 +01:00
Sidharth VinodandGitHub c06828dbf2 Merge pull request #1541 from mermaid-js/release-promotion
Release live editor
2024-09-16 22:15:28 +05:30
Sidharth Vinod e4092a6be2 chore: Update mermaid to 11.2.1 2024-09-16 22:14:18 +05:30
Sidharth VinodandGitHub 9ffa715d03 Merge pull request #1540 from mermaid-js/release-promotion
Release live editor
2024-09-16 11:31:39 +05:30
Sidharth VinodandGitHub 8fdd959079 Merge pull request #1538 from mermaid-js/browserslist-update
build: update Browserslist db
2024-09-16 11:20:22 +05:30
Sidharth VinodandGitHub 887545323d Merge pull request #1534 from nnmrts/develop
update mermaid dependencies
2024-09-16 05:45:47 +00:00
Sidharth Vinod bc72088fe9 chore: Fix test 2024-09-16 11:14:16 +05:30
GitHub Action 2077d37d3a build: update Browserslist db 2024-09-15 02:45:48 +00:00
Nano Miratus 4781d46c03 update dependencies 2024-09-09 21:38:28 +02:00
Sidharth VinodandGitHub 4a08f6dad5 Merge pull request #1527 from mermaid-js/add-product-hunt-badge
Add Product Hunt badge
2024-09-09 09:15:51 +05:30
Steph 806699d478 add product hunt badge to navbar 2024-09-06 08:20:05 -07:00
Sidharth VinodandGitHub 07595b54a6 Merge pull request #1526 from mermaid-js/release-promotion
Release live editor
2024-09-05 19:21:15 +05:30
Sidharth VinodandGitHub 370a03f708 Merge pull request #1524 from mermaid-js/browserslist-update
build: update Browserslist db
2024-09-05 09:57:54 +05:30
Sidharth VinodandGitHub e88bb8ba30 Merge pull request #1508 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2024-09-05 09:57:41 +05:30
Sidharth VinodandGitHub b3a3a0ed51 Merge pull request #1525 from mermaid-js/update-promo-bar-button-style
Update promo bar styling
2024-09-05 09:57:24 +05:30
Steph fbf2bcb202 update promo bar styling 2024-09-04 18:19:40 -07:00
renovate[bot]andGitHub 762f8f00a3 fix(deps): update all non-major dependencies 2024-09-04 19:07:59 +00:00
GitHub Action 21ba23d95d build: update Browserslist db 2024-09-01 02:47:28 +00:00
Sidharth VinodandGitHub 4d4dde003b Merge pull request #1519 from mermaid-js/release-promotion
Release live editor
2024-08-28 18:05:15 +05:30
Ashish JainandGitHub eaf8eb4245 Merge pull request #1518 from mermaid-js/sidv/elkLoader
feat: Add layout-elk
2024-08-28 12:24:21 +00:00
Sidharth Vinod d023a1142b feat: Add layout-elk 2024-08-28 17:43:37 +05:30
Sidharth VinodandGitHub 571c58e5b9 Merge pull request #1513 from mermaid-js/release-promotion
Release live editor
2024-08-24 15:59:30 +05:30
Sidharth VinodandGitHub 3fbe4a80c8 Merge pull request #1512 from mermaid-js/sidv/MermaidV11
chore: Release mermaid v11
2024-08-24 15:59:02 +05:30
Sidharth Vinod 2cdc8ed09e Merge branch 'develop' into sidv/MermaidV11
* develop:
  chore(deps): update all non-major dependencies
2024-08-24 14:39:05 +05:30
Sidharth VinodandGitHub 48333cab3b Merge pull request #1509 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2024-08-24 14:37:35 +05:30
Sidharth Vinod dd7ff496c7 Update lockfile 2024-08-24 14:36:59 +05:30
Sidharth Vinod 34207c76b3 Merge branch 'develop' into sidv/MermaidV11
* develop:
  build: update Browserslist db
2024-08-24 14:35:51 +05:30
renovate[bot]andGitHub 15bb56bd7e chore(deps): update all non-major dependencies 2024-08-24 08:55:48 +00:00
Sidharth VinodandGitHub 634921dd65 Merge pull request #1510 from mermaid-js/browserslist-update
build: update Browserslist db
2024-08-24 14:23:49 +05:30
Sidharth Vinod c77ccfeaba chore: Release mermaid v11 2024-08-24 14:23:06 +05:30
GitHub Action 79651594ed build: update Browserslist db 2024-08-15 02:29:13 +00:00
Sidharth VinodandGitHub c40d83d068 Merge pull request #1504 from mermaid-js/release-promotion
Release live editor
2024-08-07 22:11:50 +05:30
Sidharth VinodandGitHub c3fae5817b Merge pull request #1499 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2024-08-07 16:40:48 +00:00
Sidharth VinodandGitHub f736e1545e Merge pull request #1506 from mermaid-js/update-promo-bar
update promo bar
2024-08-07 10:59:09 +05:30
Sidharth Vinod 2916eaedaa chore: Simplify promotion 2024-08-07 10:22:45 +05:30
renovate[bot]andGitHub d529226922 chore(deps): update all non-major dependencies 2024-08-06 16:32:55 +00:00
Sidharth VinodandGitHub 910fdd76d8 Merge pull request #1505 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2024-08-06 22:02:11 +05:30
Steph 9dc4f5773c update promo bar 2024-08-06 04:27:25 -07:00
renovate[bot]andGitHub 4aad53032f chore(deps): update all non-major dependencies 2024-08-06 07:30:33 +00:00
Sidharth VinodandGitHub 2815f35334 Merge pull request #1503 from mermaid-js/browserslist-update
build: update Browserslist db
2024-08-02 12:43:19 +00:00
Sidharth VinodandGitHub 82b9bfe64a Merge branch 'develop' into fix/stop-parsing-syntax-error-as-html 2024-08-02 18:12:38 +05:30
Sidharth VinodandGitHub 96b20fc12c Merge pull request #1498 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2024-08-02 12:42:09 +00:00
renovate[bot]andGitHub 36f9cfbb4a chore(deps): update all non-major dependencies 2024-08-01 13:01:01 +00:00
GitHub Action a01fe0b044 build: update Browserslist db 2024-08-01 02:35:24 +00:00
Sidharth VinodandGitHub 29d0feaab1 Merge pull request #1501 from mermaid-js/product-hunt-updates
Remove Product Hunt text and badge
2024-07-31 10:12:56 +05:30
Steph 6e16716376 remove product hunt text and badge 2024-07-29 00:27:48 -07:00
Alois KlinkandGitHub b58577b72d fix: use <output> and <pre> to format errors
Use the `<output>` and `<pre>` HTML elements to format mermaid errors,
instead just adding the error message as HTML.

Using `<pre>` avoids the need for sanitizing any HTML (or using a
`monospace` font in CSS, since `<pre>` does this automatically).

Using `<output>` means that users using screenreaders should have a
better experience when using the mermaid live editor.

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2024-07-26 17:30:39 +09:00
Alois Klink 0671a9b2f1 Revert "feat: Sanitize error message"
This reverts commit 996a20d90e.

A better way to do fix this bug would be to not parse the syntax error
as HTML in the first place.
2024-07-26 17:20:15 +09:00
Sidharth VinodandGitHub ff092df72f Merge pull request #1495 from mermaid-js/release-promotion
Release live editor
2024-07-25 18:39:20 +05:30
Sidharth VinodandGitHub 28ef0e07d3 Merge pull request #1494 from mermaid-js/sidv/sanitizeError
feat: Sanitize error message
2024-07-25 18:38:12 +05:30
Sidharth Vinod 996a20d90e feat: Sanitize error message 2024-07-24 11:26:57 +05:30
Sidharth VinodandGitHub 5b1979d420 Merge pull request #1485 from mermaid-js/release-promotion
Release live editor
2024-07-24 10:46:45 +05:30
Sidharth VinodandGitHub eb62ba0b16 Merge pull request #1490 from mermaid-js/renovate/all-minor-patch
chore(deps): update dependency eslint-plugin-svelte to v2.43.0
2024-07-24 10:46:06 +05:30
Sidharth VinodandGitHub 46154948bb Merge pull request #1489 from mermaid-js/renovate/patch-all-minor-patch
fix(deps): update all non-major dependencies (patch)
2024-07-24 10:45:48 +05:30
Sidharth VinodandGitHub cbeb633895 Merge pull request #1492 from mermaid-js/update-product-hunt-content
Update Product Hunt content
2024-07-24 10:45:03 +05:30
Steph 1da5938ff2 update product hunt content 2024-07-23 16:28:50 -07:00
renovate[bot]andGitHub 5621afc874 fix(deps): update all non-major dependencies 2024-07-22 23:52:38 +00:00
renovate[bot]andGitHub d4602888f7 chore(deps): update dependency eslint-plugin-svelte to v2.43.0 2024-07-22 00:15:02 +00:00
Sidharth VinodandGitHub 3a3641b9d0 Merge pull request #1486 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2024-07-17 05:17:27 +00:00
Sidharth VinodandGitHub 84d666f558 Merge pull request #1487 from mermaid-js/browserslist-update
build: update Browserslist db
2024-07-17 05:17:19 +00:00
renovate[bot]andGitHub 2133625261 chore(deps): update all non-major dependencies 2024-07-16 18:35:04 +00:00
GitHub Action 34ae0ab1c9 build: update Browserslist db 2024-07-15 02:33:10 +00:00
Sidharth VinodandGitHub a782f60d08 Merge pull request #1484 from mermaid-js/master
Master
2024-07-14 09:32:22 +05:30
Sidharth VinodandGitHub 32a90daaeb Merge pull request #1477 from mermaid-js/release-promotion
Release live editor
2024-07-14 09:31:45 +05:30
Sidharth VinodandGitHub e9c11a9e1e Merge pull request #1469 from mermaid-js/dependabot/npm_and_yarn/ws-8.17.1
build(deps): bump ws from 8.16.0 to 8.17.1
2024-07-14 03:58:43 +00:00
Sidharth VinodandGitHub 0c5002bdad Merge pull request #1471 from mermaid-js/renovate/patch-all-minor-patch
chore(deps): update all non-major dependencies (patch)
2024-07-14 03:58:33 +00:00
Sidharth VinodandGitHub 394206340b Merge pull request #1478 from mermaid-js/browserslist-update
build: update Browserslist db
2024-07-14 09:28:08 +05:30
Sidharth VinodandGitHub 6db6b6d8cd Merge pull request #1481 from allxiao/patch-1
Update manifest.json to align the PWA scope with the editor URL, fix #1480
2024-07-14 09:27:54 +05:30
renovate[bot]andGitHub c4b848efd4 chore(deps): update all non-major dependencies 2024-07-13 12:49:17 +00:00
Menghua XiaoandGitHub e66630f6e1 Update manifest.json to align the URL with the PWA scope, fix #1480 2024-07-09 12:28:13 +08:00
GitHub Action 903106d34b build: update Browserslist db 2024-07-01 02:34:12 +00:00
dependabot[bot]andGitHub d1da3ac52c build(deps): bump ws from 8.16.0 to 8.17.1
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-27 06:07:58 +00:00
Sidharth VinodandGitHub 3b8209b3fe Merge pull request #1475 from mermaid-js/dependabot/npm_and_yarn/zenuml/core-3.23.26
build(deps): bump @zenuml/core from 3.17.3 to 3.23.26
2024-06-27 06:05:55 +00:00
Sidharth VinodandGitHub ea2d8e843c Merge pull request #1472 from mermaid-js/renovate/all-minor-patch
chore(deps): update all non-major dependencies (minor)
2024-06-27 06:05:45 +00:00
Sidharth VinodandGitHub 8909b9e54d Merge pull request #1476 from mermaid-js/add-product-hunt-badge
Add Product Hunt badge
2024-06-27 11:20:47 +05:30
steph 62906cba24 add product hunt badge to navbar 2024-06-26 22:42:28 -07:00
dependabot[bot]andGitHub 2a4a700614 build(deps): bump @zenuml/core from 3.17.3 to 3.23.26
Bumps [@zenuml/core](https://github.com/mermaid-js/zenuml-core) from 3.17.3 to 3.23.26.
- [Commits](https://github.com/mermaid-js/zenuml-core/compare/v3.17.3...v3.23.26)

---
updated-dependencies:
- dependency-name: "@zenuml/core"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-26 19:09:53 +00:00
renovate[bot]andGitHub 8a2bfcf02e chore(deps): update all non-major dependencies 2024-06-24 14:30:04 +00:00
Sidharth VinodandGitHub e9a0378c2c Merge pull request #1470 from mermaid-js/promo-bar
Promo bar - update end date
2024-06-19 23:22:38 +05:30
Steph e703aec424 update endDate on promo 2024-06-19 10:49:13 -07:00
Sidharth VinodandGitHub 582c9662f2 Merge pull request #1468 from mermaid-js/release-promotion
Release live editor
2024-06-17 10:56:21 +05:30
Sidharth VinodandGitHub 6d67f0cd42 Merge pull request #1300 from mermaid-js/sidv/errorHighlight
feat: Add error highlight
2024-06-17 04:44:36 +00:00
Sidharth Vinod aca7f07aa8 Merge branch 'develop' into sidv/errorHighlight
* develop: (148 commits)
  Remove error lines from View
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  Remove stalebot
  build: update Browserslist db
  build: update Browserslist db
  chore: Add isRough to stats
  Update snapshots
  feat: Add rough mode
  fix(deps): update dependency mermaid to v10.9.1
  chore(deps): update all non-major dependencies
  build: update Browserslist db
  chore(deps): update dependency svelte to v4.2.16
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  build: update Browserslist db
  build: update Browserslist db
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  ...
2024-06-17 10:10:01 +05:30
Sidharth VinodandGitHub 368e741675 Merge pull request #1426 from mermaid-js/dependabot/npm_and_yarn/katex-0.16.10
build(deps): bump katex from 0.16.9 to 0.16.10
2024-06-17 04:38:48 +00:00
Sidharth VinodandGitHub 9695512981 Merge pull request #1299 from mermaid-js/sidv/errorDialogue
feat: Move error dialogue to bottom of editor.
2024-06-17 04:38:33 +00:00
Sidharth Vinod 3baba33e65 Remove error lines from View 2024-06-16 20:22:23 +05:30
Sidharth Vinod 49edf2e8e2 Merge branch 'develop' into sidv/errorDialogue
* develop: (139 commits)
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  Remove stalebot
  build: update Browserslist db
  build: update Browserslist db
  chore: Add isRough to stats
  Update snapshots
  feat: Add rough mode
  fix(deps): update dependency mermaid to v10.9.1
  chore(deps): update all non-major dependencies
  build: update Browserslist db
  chore(deps): update dependency svelte to v4.2.16
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  build: update Browserslist db
  build: update Browserslist db
  chore(deps): update all non-major dependencies
  chore(deps): update all non-major dependencies
  Update README.md
  ...
2024-06-16 20:18:09 +05:30
Sidharth VinodandGitHub beca75bbba Merge branch 'develop' into dependabot/npm_and_yarn/katex-0.16.10 2024-06-16 20:08:41 +05:30
Sidharth VinodandGitHub ea0553342c Merge pull request #1458 from mermaid-js/release-promotion
Release live editor
2024-05-23 18:27:12 +05:30
Sidharth VinodandGitHub 8b673659d6 Merge pull request #1448 from mermaid-js/release-promotion
Release live editor
2024-05-21 12:40:59 +05:30
Sidharth VinodandGitHub 994a87b103 Merge branch 'master' into release-promotion 2024-05-21 12:40:44 +05:30
Sidharth VinodandGitHub f7046c27c5 Merge pull request #1437 from mermaid-js/release-promotion
Release live editor
2024-05-12 09:22:11 +05:30
Sidharth VinodandGitHub fdd2581b64 Merge pull request #1430 from mermaid-js/release-promotion
Release live editor
2024-04-05 19:07:24 +05:30
Sidharth VinodandGitHub 8fd1c154d6 Merge pull request #1417 from mermaid-js/release-promotion
Release live editor
2024-04-03 00:42:40 +05:30
dependabot[bot]andGitHub f89b2f5698 build(deps): bump katex from 0.16.9 to 0.16.10
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.9 to 0.16.10.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.9...v0.16.10)

---
updated-dependencies:
- dependency-name: katex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 20:31:18 +00:00
Sidharth VinodandGitHub 6de7cbc520 Merge pull request #1407 from mermaid-js/release-promotion
Release live editor
2024-03-05 23:09:29 +05:30
Sidharth VinodandGitHub f7ae8d19e6 Merge pull request #1409 from mermaid-js/update-announcement-bar
Announcement Bar - update link
2024-03-05 23:02:31 +05:30
steph 611fda5eff update link 2024-02-28 21:42:02 -08:00
Sidharth VinodandGitHub c0101cdd4f Merge pull request #1400 from mermaid-js/release-promotion
Release live editor
2024-02-12 23:09:19 +05:30
Sidharth VinodandGitHub 4f66f40779 Merge pull request #1397 from mermaid-js/release-promotion
Release live editor
2024-02-09 11:19:44 +05:30
Sidharth VinodandGitHub 552df747fd Merge pull request #1395 from mermaid-js/release-promotion
Release live editor
2024-02-08 08:04:47 +05:30
Sidharth VinodandGitHub 2e25c8475a Merge pull request #1392 from mermaid-js/release-promotion
Release live editor
2024-02-03 11:09:38 +05:30
Sidharth VinodandGitHub 0abf806f24 Merge branch 'master' into release-promotion 2024-02-03 11:09:05 +05:30
Sidharth VinodandGitHub f4d24c5ab8 Merge pull request #1387 from mermaid-js/release-promotion
Release live editor
2024-01-30 23:42:44 +05:30
Sidharth VinodandGitHub d0da4a07b4 Merge pull request #1374 from mermaid-js/release-promotion
Release live editor
2024-01-23 13:46:03 +05:30
Sidharth VinodandGitHub d8ba3747bf Merge pull request #1373 from mermaid-js/release-promotion
Release live editor
2024-01-15 12:31:41 +05:30
Sidharth VinodandGitHub 9a15de1467 Merge pull request #1370 from mermaid-js/release-promotion
Release live editor
2024-01-11 22:44:43 +05:30
Sidharth VinodandGitHub 9e0fa86127 Merge pull request #1369 from mermaid-js/release-promotion
Release live editor
2024-01-11 22:15:16 +05:30
Sidharth Vinod bb526e1534 test: Ignore monaco internal error. 2023-09-26 12:11:48 +05:30
Sidharth Vinod 8cb7bffc28 chore: Fix test without ID 2023-09-26 11:32:40 +05:30
Sidharth Vinod 4fda534c6a chore: Add id to fix test 2023-09-26 10:56:49 +05:30
Sidharth Vinod f64eaaa528 refactor: Remove unnecessary div 2023-09-26 10:54:42 +05:30
Sidharth Vinod 4994ecf1dd feat: Add error highlight 2023-09-15 13:17:58 +05:30
Sidharth Vinod 3ce91d9d22 chore: Simplify classes 2023-09-15 11:38:07 +05:30
Sidharth Vinod 182b488552 Increase max height 2023-09-15 11:21:22 +05:30
Sidharth Vinod 17f27de9bd feat: Move error dialogue to bottom of editor. 2023-09-15 11:12:44 +05:30
185 changed files with 14418 additions and 10917 deletions
+9 -2
View File
@@ -1,2 +1,9 @@
MERMAID_DOMAIN="mermaid.live" MERMAID_DOMAIN=''
MERMAID_ANALYTICS_URL="https://p.mermaid.live" MERMAID_BASE_PATH=''
MERMAID_DOCS_URL='https://mermaid.js.org'
MERMAID_ANALYTICS_URL=''
MERMAID_RENDERER_URL='https://mermaid.ink'
MERMAID_KROKI_RENDERER_URL='https://kroki.io'
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS='true'
# cp .env .env.local to make local changes
-8
View File
@@ -1,8 +0,0 @@
docs/**
.svelte-kit/**
static/**
build/**
node_modules/**
coverage/**
__snapshots__/**
snapshots.js
-97
View File
@@ -1,97 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'plugin:unicorn/recommended',
'plugin:svelte/recommended',
'plugin:svelte/prettier',
'prettier'
],
plugins: ['tailwindcss', '@typescript-eslint', 'es', 'vitest', 'no-only-tests', 'unicorn'],
ignorePatterns: [
'docs/*',
'*.cjs',
'*.js',
'*.md',
'snapshots.js',
'svelte.config.js',
'renovate.json',
'package.json',
'tsconfig.json'
],
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
},
{
files: ['*.ts'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'prettier'
]
}
],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
tsconfigRootDir: __dirname,
project: './tsconfig.json',
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2020: true
},
rules: {
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'es/no-regexp-lookbehind-assertions': 'error',
curly: ['error', 'all'],
'no-only-tests/no-only-tests': 'error',
'unicorn/no-null': 'off',
'unicorn/filename-case': [
'error',
{
case: 'camelCase'
}
],
'unicorn/filename-case': 'off',
'unicorn/prevent-abbreviations': [
'error',
{
allowList: {
ctx: true,
db: true,
doc: true,
env: true,
fn: true,
i: true,
param: true,
req: true,
res: true,
str: true,
searchParams: true,
temp: true,
ImportMetaEnv: true
}
}
]
}
};
+49
View File
@@ -0,0 +1,49 @@
name: Docker Build
on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Gitea Container Registry
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: gitea.forkless.com
username: ${{ secrets.GITEAUSER }}
password: ${{ secrets.GITEATOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: gitea.forkless.com/forkless/mermaid-live-editor
tags: |
type=raw,value=latest,enable=${{ github.ref_name == 'master' }}
type=raw,value=nightly,enable=${{ github.ref_name == 'develop' }}
type=sha,prefix={{branch}}-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
target: mermaid
push: ${{ github.event_name == 'push' }}
pull: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
MERMAID_RENDERER_URL=${{ vars.MERMAID_RENDERER_URL || 'https://mermaid.ink' }}
MERMAID_KROKI_RENDERER_URL=${{ vars.MERMAID_KROKI_RENDERER_URL || 'https://kroki.io' }}
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS=false
@@ -0,0 +1,60 @@
name: Close broken link issues
on:
issues:
types: [opened, edited]
permissions:
issues: write
jobs:
close-broken-link-issues:
if: github.event.issue.state == 'open'
runs-on: ubuntu-latest
steps:
- name: Close issues with only "Broken link" in the title
uses: actions/github-script@v7
with:
script: |
const title = context.payload.issue.title.trim();
const isBrokenLinkOnlyTitle = /^broken link\.?$/i.test(title);
if (!isBrokenLinkOnlyTitle) {
return;
}
const { owner, repo } = context.repo;
const issueNumber = context.payload.issue.number;
const newIssueUrl = `https://github.com/${owner}/${repo}/issues/new?assignees=&labels=bug&template=bug_report.md&title=Broken%20link`;
const mermaidIssuesUrl = 'https://github.com/mermaid-js/mermaid/issues/new';
const commentBody = [
`@${context.payload.issue.user.login} This issue was automatically closed because the title only contains "Broken link" without additional details.`,
'',
'**Please only open issues here if something is broken in the live editor itself** (e.g. a link fails to load, the editor UI misbehaves, or a feature of mermaid.live does not work).',
'',
'**Do not open issues here for syntax errors or invalid Mermaid diagram code.** Those belong in the main Mermaid repository: [mermaid-js/mermaid](https://github.com/mermaid-js/mermaid/issues/new).',
'',
'If you are reporting a live editor bug, please include:',
'- The full URL that failed to load',
'- What you expected to happen',
'- Any error messages you saw',
'',
`You can [open a new issue in this repo](${newIssueUrl}) with this information, or report diagram syntax issues in the [Mermaid repo](${mermaidIssuesUrl}).`
].join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: commentBody
});
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned'
});
+35 -14
View File
@@ -14,28 +14,49 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: actions/setup-node@v3 - name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: with:
node-version: 18 node-version-file: '.node-version'
cache: yarn cache: pnpm
- name: Setup Pages
uses: actions/configure-pages@v5
with:
static_site_generator: sveltekit
- name: Build & Deploy - name: Build & Deploy
env: env:
MERMAID_DOMAIN: 'mermaid.live' MERMAID_DOMAIN: 'mermaid.live'
MERMAID_ANALYTICS_URL: 'https://p.mermaid.live' MERMAID_ANALYTICS_URL: 'https://p.mermaid.live'
MERMAID_RENDERER_URL: 'https://mermaid.ink'
MERMAID_KROKI_RENDERER_URL: 'https://kroki.io'
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS: 'true'
run: | run: |
export DEPLOY=true export DEPLOY=true
[ "$GITHUB_EVENT_NAME" != "pull_request" ] && rm -rf docs/_app/ [ "$GITHUB_EVENT_NAME" != "pull_request" ] && rm -rf docs/_app/
yarn install pnpm install
version=$(yarn version --patch --no-git-tag-version | grep "New version" | cut -d':' -f 2) pnpm build
yarn build
yarn run lint
cd ..
- name: Deploy - name: Upload artifact
uses: peaceiris/actions-gh-pages@v3 uses: actions/upload-pages-artifact@v3
if: ${{ github.ref == 'refs/heads/master' }}
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} path: ./docs
publish_dir: ./docs
keep_files: true deploy:
if: ${{ github.ref == 'refs/heads/master' }}
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+17 -6
View File
@@ -17,32 +17,43 @@ on:
jobs: jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3 - uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: github.event_name != 'pull_request' if: github.event_name == 'push'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Get release version
run: |
RELEASE_VERSION=$([ "${{ github.ref_name }}" = "master" ] && echo "latest" || echo "nightly")
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_ENV}"
- uses: docker/metadata-action@v5 - uses: docker/metadata-action@v5
id: meta id: meta
with: with:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository }}
tags: | tags: |
type=raw,value=${{ env.RELEASE_VERSION }} type=raw,value=latest,enable=${{ github.ref_name == 'master' }}
type=raw,value=nightly,enable=${{ github.ref_name == 'develop' }}
- uses: docker/build-push-action@v5 - uses: docker/build-push-action@v5
id: build
with: with:
context: . context: .
target: mermaid target: mermaid
push: ${{ github.event_name == 'push' }} push: ${{ github.event_name == 'push' }}
platforms: linux/amd64,linux/arm64
pull: true pull: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
- name: Generate Build Attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
if: github.event_name == 'push'
+31 -27
View File
@@ -5,47 +5,51 @@ on:
branches: branches:
- master - master
- develop - develop
merge_group:
jobs: jobs:
cypress-run: playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: container:
fail-fast: false image: mcr.microsoft.com/playwright:v1.60.0-jammy
matrix:
# run 3 copies of the current job in parallel
containers: [1, 2, 3]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: actions/cache@v3 - uses: actions/cache@v4
id: yarn-and-build-cache id: pnpm-and-build-cache
with: with:
path: | path: |
~/.cache/Cypress
build build
node_modules node_modules
key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-node_modules-build- ${{ runner.os }}-node_modules-build-
- uses: actions/setup-node@v3 - name: Install pnpm
with: uses: pnpm/action-setup@v4
node-version: 18
cache: 'yarn'
# Install NPM dependencies, cache them correctly - uses: actions/setup-node@v4
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v3
with: with:
build: yarn build node-version-file: '.node-version'
start: yarn preview cache: 'pnpm'
wait-on: 'http://localhost:3000'
record: true - name: Install dependencies
headless: true run: pnpm install
parallel: true
- name: Build
run: pnpm build
- name: Run Playwright tests
run: pnpm test:e2e
env: env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CI: true
CYPRESS_CI: true
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: test-results
path: test-results/
retention-days: 7
+21 -11
View File
@@ -5,6 +5,7 @@ on:
branches: branches:
- master - master
- develop - develop
merge_group:
jobs: jobs:
unit-tests: unit-tests:
@@ -15,22 +16,31 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: actions/cache@v3 - uses: actions/cache@v3
id: yarn-and-build-cache id: pnpm-and-build-cache
with: with:
path: | path: |
build build
node_modules node_modules
key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-node_modules-build- ${{ runner.os }}-node_modules-build-
- uses: actions/setup-node@v3 - name: Install pnpm
with: uses: pnpm/action-setup@v4
node-version: 18
cache: 'yarn'
- name: Lint & Test - uses: actions/setup-node@v4
run: | with:
yarn install node-version-file: '.node-version'
yarn lint cache: 'pnpm'
yarn test:unit
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm check
- name: Run unit tests
run: pnpm test:unit
+13 -9
View File
@@ -1,14 +1,18 @@
node_modules/
coverage/
.cache/
build/
yarn-error.log
.npmrc
.DS_Store .DS_Store
.cache/
.env.local
.npmrc
/.svelte-kit /.svelte-kit
/build /build
/coverage
/docs
/functions /functions
/node_modules
/snapshots.js /snapshots.js
/cypress/downloads
/cypress/videos # Playwright
/cypress/screenshots /test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
.playwright-mcp/
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
yarn pre-commit pnpm pre-commit
+1 -1
View File
@@ -1 +1 @@
20.14.0 24.16.0
+1
View File
@@ -1 +1,2 @@
engine-strict=true engine-strict=true
auto-install-peers=true
+1
View File
@@ -6,3 +6,4 @@ node_modules/**
coverage/** coverage/**
__snapshots__/** __snapshots__/**
snapshots.js snapshots.js
pnpm-lock.yaml
+2 -2
View File
@@ -4,6 +4,6 @@
"bracketSameLine": true, "bracketSameLine": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"tailwindConfig": "./tailwind.config.cjs", "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"] "tailwindStylesheet": "./src/app.css"
} }
+10
View File
@@ -0,0 +1,10 @@
{
"servers": {
"svelte": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@sveltejs/mcp"]
}
},
"inputs": []
}
+30 -4
View File
@@ -2,32 +2,58 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"cSpell.blockCheckingWhenLineLengthGreaterThan": 150, "cSpell.blockCheckingWhenLineLengthGreaterThan": 150,
"cSpell.words": [ "cSpell.words": [
"appinstalled",
"asyncable", "asyncable",
"Browserslist", "Browserslist",
"ckppp", "ckppp",
"corg", "corg",
"cssnano", "cssnano",
"daisyui",
"esserializer", "esserializer",
"fsegurai",
"gantt", "gantt",
"gitgraph", "gitgraph",
"hugeicons",
"KROKI", "KROKI",
"localstorage", "localstorage",
"mermaidchart", "mermaidchart",
"mindmap", "mindmap",
"NEWYEAR",
"noopener",
"noreferrer",
"Pageview", "Pageview",
"pako", "pako",
"panmove",
"panstart",
"panzoom", "panzoom",
"pinchmove",
"pinchstart",
"pzoom", "pzoom",
"roughjs", "roughjs",
"sankey",
"Serde", "Serde",
"serdes", "serdes",
"Stackable",
"tailwindcss", "tailwindcss",
"uparrow" "treemap",
"uparrow",
"xychart",
"zenuml"
], ],
"vitest.commandLine": "yarn test:unit", "vitest.commandLine": "pnpm test:unit",
"vitest.enable": true, "vitest.enable": true,
"testing.autoRun.mode": "rerun", "testing.autoRun.mode": "rerun",
"svelte.enable-ts-plugin": true, "svelte.enable-ts-plugin": true,
"githubPullRequests.ignoredPullRequestBranches": ["develop"] "githubPullRequests.ignoredPullRequestBranches": ["develop"],
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"tailwindCSS.classAttributes": ["class", "className", ".*Classes"],
"tailwindCSS.experimental.classRegex": [
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"tailwindCSS.emmetCompletions": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
}
} }
+1
View File
@@ -0,0 +1 @@
mermaid.live
+11 -7
View File
@@ -1,16 +1,16 @@
FROM docker.io/library/node:20-alpine3.18 AS mermaid-live-editor-dependencies FROM docker.io/library/node:24.16.0-alpine3.22 AS mermaid-live-editor-dependencies
RUN apk --no-cache add build-base git python3 && \ RUN apk --no-cache add build-base git python3 && \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
RUN yarn global add node-gyp RUN corepack enable pnpm
WORKDIR /app WORKDIR /app
COPY ./package.json . COPY ./package.json .
COPY ./yarn.lock . COPY ./pnpm-lock.yaml .
RUN yarn install RUN pnpm install
FROM mermaid-live-editor-dependencies AS mermaid-live-editor-builder FROM mermaid-live-editor-dependencies AS mermaid-live-editor-builder
@@ -18,16 +18,20 @@ ARG MERMAID_RENDERER_URL
ARG MERMAID_KROKI_RENDERER_URL ARG MERMAID_KROKI_RENDERER_URL
ARG MERMAID_ANALYTICS_URL ARG MERMAID_ANALYTICS_URL
ARG MERMAID_DOMAIN ARG MERMAID_DOMAIN
ARG MERMAID_IS_ENABLED_MERMAID_CHART_LINKS
ARG MERMAID_PRIVACY_POLICY_URL
ARG MERMAID_HIDE_PRIVACY_POLICY
ARG MERMAID_BASE_PATH
COPY . ./ COPY . ./
RUN yarn build RUN pnpm build
FROM mermaid-live-editor-builder AS mermaid-dev FROM mermaid-live-editor-builder AS mermaid-dev
ENTRYPOINT ["yarn", "dev"] ENTRYPOINT ["pnpm", "dev"]
FROM nginx:1.25-alpine3.18 AS mermaid FROM nginx:1.28-alpine3.21 AS mermaid
COPY ./nginx.conf /etc/nginx/conf.d/default.conf COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=mermaid-live-editor-builder /app/docs /usr/share/nginx/html COPY --from=mermaid-live-editor-builder /app/docs /usr/share/nginx/html
+34 -15
View File
@@ -1,10 +1,7 @@
[![Mermaid Live Editor](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/detailed/2ckppp/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/2ckppp/runs) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [![Netlify Status](https://api.netlify.com/api/v1/badges/27fa023d-7c73-4a3f-9791-b3b657a47100/deploy-status)](https://app.netlify.com/sites/mermaidjs/deploys) [![Join our Discord!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=discord&label=discord)](https://discord.gg/sKeNQX4Wtj)
[![Netlify Status](https://api.netlify.com/api/v1/badges/27fa023d-7c73-4a3f-9791-b3b657a47100/deploy-status)](https://app.netlify.com/sites/mermaidjs/deploys)
# Contributors are welcome! # Mermaid Live Editor
If you want to speed up the progress for mermaid-live-editor, join the slack channel and contact knsv.
# mermaid-live-editor
Edit, preview and share mermaid charts/diagrams. Edit, preview and share mermaid charts/diagrams.
@@ -17,7 +14,11 @@ Edit, preview and share mermaid charts/diagrams.
## Live demo ## Live demo
You can try out a live version [here](https://mermaid.live/). You can try out a [live version](https://mermaid.live/).
# Contributors are welcome!
If you want to speed up the progress for mermaid-live-editor, join the Discord channel and contact knsv.
## Docker ## Docker
@@ -27,15 +28,22 @@ You can try out a live version [here](https://mermaid.live/).
docker run --platform linux/amd64 --publish 8000:8080 ghcr.io/mermaid-js/mermaid-live-editor docker run --platform linux/amd64 --publish 8000:8080 ghcr.io/mermaid-js/mermaid-live-editor
``` ```
The published docker image is built using our default environment variables. You cannot override them when running the image. If you need to customize them, you will need to build the image yourself.
### To configure renderer URL ### To configure renderer URL
When building set the MERMAID_RENDERER_URL build argument to the rendering service. When building set the MERMAID_RENDERER_URL build argument to the rendering
Default is `https://mermaid.ink` service.
Example:
Default is`https://mermaid.ink`.
Set to empty string to disable PNG and SVG links under Actions
### To configure Kroki Instance URL ### To configure Kroki Instance URL
When building set the MERMAID_KROKI_RENDERER_URL build argument to your Kroki instance. When building set the MERMAID_KROKI_RENDERER_URL build argument to your Kroki
instance.
Default is `https://kroki.io` Default is `https://kroki.io`
Set to empty string to disable Kroki link under Actions
### To configure Analytics ### To configure Analytics
@@ -43,6 +51,18 @@ When building set the MERMAID_ANALYTICS_URL build argument to your plausible ins
Default is empty, disabling analytics. Default is empty, disabling analytics.
### To enable Mermaid Chart links and promotion
When building set the MERMAID_IS_ENABLED_MERMAID_CHART_LINKS build argument to `true`
Default is empty, disabling button to save to Mermaid Chart and promotional banner.
### To update the Security modal
The modal shown on clicking the security link assumes analytics, renderer, Kroki
and Mermaid chart are enabled. You can update it by modifying `Privacy.svelte`
if you wish.
### Development ### Development
```bash ```bash
@@ -81,15 +101,14 @@ https://docs.github.com/en/get-started/quickstart/fork-a-repo
## Requirements ## Requirements
- [volta](https://volta.sh/) to manage node versions. - [Node.js](https://nodejs.org/en/) current LTS version
- [Node.js](https://nodejs.org/en/). `volta install node` - [pnpm](https://pnpm.io/) package manager. Install with `corepack enable pnpm`
- [yarn](https://yarnpkg.com/) package manager. `volta install yarn`
## Development ## Development
```sh ```sh
yarn install pnpm install
yarn dev -- --open pnpm dev -- --open
``` ```
This app is created with Svelte Kit. This app is created with Svelte Kit.
+1 -1
View File
@@ -6,7 +6,7 @@
# git clone https://github.com/mermaid-js/docs.git # git clone https://github.com/mermaid-js/docs.git
set -e set -e
rm -rf docs rm -rf docs
yarn release pnpm release
pushd . pushd .
if [ ! -d ../docs ]; then if [ ! -d ../docs ]; then
echo "Clone https://github.com/mermaid-js/docs to parent folder before continuing." echo "Clone https://github.com/mermaid-js/docs to parent folder before continuing."
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://tw3.shadcn-svelte.com/registry/new-york"
}
-37
View File
@@ -1,37 +0,0 @@
import { defineConfig } from 'cypress';
import fs from 'fs';
import path from 'path';
export default defineConfig({
projectId: '2ckppp',
viewportWidth: 1440,
viewportHeight: 768,
snapshotFileName: './cypress/snapshots.js',
defaultCommandTimeout: 5000,
requestTimeout: 5000,
retries: {
runMode: 2,
openMode: 0
},
e2e: {
setupNodeEvents(on, config) {
on('task', {
readAndDeleteFile({ fileNamePattern, folder, mode }) {
const fileNameRegex = new RegExp(fileNamePattern);
const files = fs.readdirSync(folder);
const filename = files.find((file) => file.match(fileNameRegex));
const filePath = path.join(folder, filename);
try {
if (mode === 'size') {
return fs.statSync(filePath).size;
}
return fs.readFileSync(filePath, 'utf8');
} finally {
fs.rmSync(filePath);
}
}
});
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.spec.ts'
}
});
-10
View File
@@ -1,10 +0,0 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": "off"
},
"env": {
"cypress/globals": true
}
}
-45
View File
@@ -1,45 +0,0 @@
import { typeInEditor, verifyFileSizeGreaterThan } from './util';
describe('Check actions', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Actions').click();
});
it('should update markdown code', () => {
cy.get('#markdown')
.invoke('val')
.then((oldText) => {
typeInEditor('C --> HistoryTest', { bottom: true, newline: true });
cy.get('#markdown')
.invoke('val')
.then((newText) => {
expect(oldText).to.not.eq(newText);
});
});
});
it.skip('should load gists from URL', () => {
cy.get('#gist').type('https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a');
cy.contains('Load Gist').click();
cy.contains('Go shopping!!');
});
it('should download png and svg', () => {
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 34_000);
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 10_000);
// Verify downloaded file is different for different diagrams
cy.contains('Sample Diagrams').click();
cy.contains('ER').click();
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 40_000);
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 11_000);
});
});
-115
View File
@@ -1,115 +0,0 @@
import { typeInEditor, cmd } from './util';
describe('Auto sync tests', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
cy.url().should('contain', '/edit#pako');
cy.window().should('have.property', 'editorLoaded', true);
});
it('should dim diagram when code is edited', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#errorContainer').should('not.exist');
typeInEditor(' C --> Test', { bottom: true });
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'Diagram out of sync.');
cy.getLocalStorage('codeStore').snapshot();
});
it('should update diagram when shortcut is used', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'Diagram out of sync.');
typeInEditor(`${cmd}{enter}`);
cy.get('#errorContainer').should('not.exist');
cy.get('#view').should('not.have.class', 'outOfSync');
});
it('should show/hide sync button with auto sync', () => {
cy.get('[data-cy=sync]').should('not.exist');
cy.contains('Auto sync').click();
cy.get('[data-cy=sync]').should('exist');
cy.get('#autoSync').check();
cy.get('[data-cy=sync]').should('not.exist');
});
it('should not dim diagram when code is in sync', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.get('[data-cy=sync]').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#autoSync').check();
typeInEditor('ing');
cy.get('#view').should('not.have.class', 'outOfSync');
cy.getLocalStorage('codeStore').snapshot();
});
it('should automatically defer rendering when complex diagrams are edited', () => {
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(`
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i`);
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'It will be updated automatically.');
// The class should be removed automatically after 1 second.
cy.get('#view').should('not.have.class', 'outOfSync');
});
it('supports commenting code out/in', () => {
cy.get('#editor').contains('Car').click({ force: true });
cy.get('#editor').get('textarea').type(`${cmd}/`, { force: true });
cy.get('#view').contains('Car').should('not.exist');
typeInEditor(`{uparrow}${cmd}/`);
cy.get('#view').contains('Car').should('exist');
});
it('supports editing code when code is incorrect', () => {
cy.visit(
'/edit#pako:eNpljjEKwzAMRa8SNOcEnlt6gK5eVFvYJsgOqkwpIXevg9smEE1PnyfxF3DFExgISW-CczQ2D21cYU7a-SGYXRwyvTp9jUhuKlVP-eHy7zA-leQsMEmg_QOM0BLG5FujZVMsaCQmC6ahR5ks2Lw2r84ela4-aREwKpVGwKrl_s7ut3fnkjAIcg_XDzuaUhs'
);
cy.get('#errorContainer').should('not.exist');
typeInEditor(`branch test`, { bottom: true, newline: true });
cy.get('#editor').contains('branch test').should('exist');
cy.get('#errorContainer')
.contains(
'Error: Trying to checkout branch which is not yet created. (Help try using "branch master")'
)
.should('exist');
});
it('should update diagram after entire text is removed', () => {
// https://github.com/mermaid-js/mermaid-live-editor/issues/1102
typeInEditor(`${cmd} a {backspace}`);
typeInEditor('graph LR');
typeInEditor(' {enter} A-->Car');
cy.get('#view').contains('Car').should('exist');
});
});
describe('Pan and Zoom', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
});
it('should toggle pan and zoom', () => {
cy.get('#svg-pan-zoom-reset-pan-zoom').should('not.exist');
cy.contains('Pan & Zoom').click();
cy.get('#svg-pan-zoom-reset-pan-zoom').should('exist');
cy.contains('Pan & Zoom').click();
cy.get('#svg-pan-zoom-reset-pan-zoom').should('not.exist');
});
});
-36
View File
@@ -1,36 +0,0 @@
describe('Editor docs tests', () => {
beforeEach(() => {
cy.on('uncaught:exception', () => {
return false;
});
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Sample Diagrams').click();
});
it('Test default loading', () => {
cy.get(`[data-cy=docs][href^="https://mermaid.js.org/"]`).should('exist');
});
it('Test to see if the correct URL loads when changing from one diagram to other', () => {
cy.contains('Flow').click();
cy.get(`[data-cy=docs][href$="/syntax/flowchart.html"]`).should('exist');
cy.contains('Config').click();
cy.get(`[data-cy=docs][href$="/syntax/flowchart.html#configuration"]`).should('exist');
cy.contains('Sequence').click();
cy.get(`[data-cy=docs][href$="/syntax/sequenceDiagram.html#configuration"]`).should('exist');
cy.contains('Code').click();
cy.get(`[data-cy=docs][href$="/syntax/sequenceDiagram.html"]`).should('exist');
});
it("Test to check URLs for a case where config URL doesn't exist", () => {
cy.contains('State').click();
cy.get(`[data-cy=docs][href$="/syntax/stateDiagram.html"]`).should('exist');
cy.contains('Config').click();
cy.get(`[data-cy=docs][href$="/syntax/stateDiagram.html"]`).should('exist');
});
});
-103
View File
@@ -1,103 +0,0 @@
import { typeInEditor, verifyFileSnapshot } from './util';
describe('Save History', () => {
beforeEach(() => {
cy.clock(new Date(2022, 0, 1).getTime());
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('History').click();
});
afterEach(() => {
cy.clock().invoke('restore');
});
it('should load history from localstorage', () => {
cy.setLocalStorage(
'manualHistoryStore',
'[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"manual","id":"d7ea820e-21dd-418a-b984-fd58acde09df","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"b749ffc6-522b-4a44-86cf-7c1ffc3146b3","name":"helpful-ocean"}]'
);
cy.setLocalStorage(
'autoHistoryStore',
'[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"auto","id":"69ea820e-522b-4a44-86cf-fd58acde09df","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"x749ffc6-21dd-418a-b984-7c1ffc3146b3","name":"needy-mosquito"}]'
);
cy.reload();
cy.contains('History').click();
cy.get('#historyList').find('li').should('have.length', 2);
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').contains('helpful-ocean');
cy.get('#historyList').contains('hollow-art');
cy.contains('Restore').click();
cy.contains('Halloween');
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 2);
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').contains('needy-mosquito');
cy.get('#historyList').contains('barking-dog');
cy.contains('Restore').click();
cy.contains('New Year');
});
it('should save when clicked', () => {
cy.get('#historyList').find('li').should('have.length', 0);
cy.get('#historyList').contains('No items in History');
cy.get('#saveHistory').click();
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#saveHistory').click();
cy.on('window:alert', (str) => {
expect(str).to.equal('State already saved.');
});
cy.on('window:confirm', () => true);
typeInEditor(' C --> HistoryTest');
cy.get('#saveHistory').click();
cy.get('#historyList').find('li').should('have.length', 2);
});
it('should be able to restore and delete', () => {
cy.get('#saveHistory').click();
typeInEditor(' C --> HistoryTest');
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').find('li').should('have.length', 1);
cy.contains('HistoryTest');
cy.contains('Restore').click();
cy.contains('HistoryTest').should('not.exist');
cy.contains('Delete').click();
cy.get('#historyList').find('li').should('have.length', 0);
cy.get('#historyList').contains('No items in History');
cy.get('#saveHistory').click();
typeInEditor(' C --> HistoryTest');
cy.get('#saveHistory').click();
cy.get('#editor').type('ing');
cy.get('#clearHistory').click();
cy.on('window:alert', (str) => {
expect(str).to.equal('Clear all saved items?');
});
cy.on('window:confirm', () => true);
cy.get('#historyList').contains('No items in History');
});
// TODO: Fix #639
xit('should auto save history', () => {
typeInEditor(' C --> HistoryTest');
cy.tick(70_000);
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#editor').type('ing');
cy.tick(70_000);
cy.get('#historyList').find('li').should('have.length', 2);
for (let i = 0; i < 31; i++) {
cy.get('#editor').type('.');
cy.tick(70_000);
}
cy.get('#historyList').find('li').should('have.length', 30);
});
it('should download history', () => {
cy.get('#saveHistory').click();
cy.get(`#downloadHistory`).click();
verifyFileSnapshot('history', 'json', 'A[Christmas] -->|Get money| B(Go shopping)');
});
});
-136
View File
@@ -1,136 +0,0 @@
import { toBase64 } from 'js-base64';
describe('Site Loads', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
cy.url().should('include', '/edit#pako');
});
it('Check Home page load', () => {
cy.url().should('include', '/edit');
cy.contains('History').click();
cy.getLocalStorage('codeStore').snapshot();
});
it('should keep code after reload', () => {
cy.get('#editor').contains('Car');
cy.reload();
cy.get('#editor').contains('Car');
});
it('Check Redirect from old URL', () => {
cy.visit(
'/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ'
);
cy.url().should('include', '/edit#pako:eNp');
});
it('should load sample diagrams when clicked', () => {
cy.contains('Sample Diagrams').click();
cy.contains('Pie').click();
cy.contains('pie title Pets adopted by volunteers');
cy.contains('Class').click();
cy.contains('classDiagram');
});
(Cypress.env('CI') === 'true' ? describe : describe.skip)('github', () => {
it('should load diagram from gist', () => {
cy.visit(`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`);
cy.contains('History').click();
cy.contains('Go shopping!!');
cy.contains('Revisions');
cy.contains('sidharthv96 v8f8f1e2');
cy.contains('sidharthv96 v7851e19');
cy.getLocalStorage('codeStore').snapshot();
});
it('should load diagram from gist revision', () => {
cy.visit(
'/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19'
);
cy.contains('History').click();
cy.contains('Party');
cy.contains('Revisions');
cy.contains('sidharthv96 v7851e19');
cy.getLocalStorage('codeStore').snapshot();
});
it('should load diagram from raw files', () => {
cy.visit(
'/edit?code=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd&config=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json'
);
cy.contains('Party');
cy.getLocalStorage('codeStore').snapshot();
});
});
// Disabled temporarily. Should be enabled after the issue is fixed in Mermaid.
// it('should prevent setting the "securityLevel" option via URL', () => {
// const b64State = toBase64(
// `{"code":"graph TD\\nA[\\"<img src='https://via.placeholder.com/64' width=64 />\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","autoSync":true,"updateDiagram":true}`,
// true
// );
// cy.on('window:confirm', () => true);
// cy.visit(`/edit#${b64State}`);
// cy.contains('Config').click();
// cy.contains('forest');
// cy.contains('securityLevel').should('not.exist');
// cy.get('#view').find('img').should('not.exist');
// cy.get('#view').contains('<img');
// cy.get('#view').contains(`src='https://via.placeholder.com/64'`);
// });
it.skip('should allow persisting "securityLevel" using confirm dialogue', () => {
const b64State = toBase64(
`{"code":"graph TD\\nA[\\"<img src='https://dummyimage.com/64' width=64/>\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","autoSync":true,"updateDiagram":true}`,
true
);
cy.on('window:confirm', () => false);
cy.visit(`/edit#${b64State}`);
cy.get('#editor').type(' ');
cy.contains('Config').click();
cy.contains('forest');
cy.contains('securityLevel');
cy.get('#view').find('img').should('be.visible');
});
it('should show troubleshooting steps if loading fails', () => {
cy.visit('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg');
cy.reload(true);
cy.contains('Please Click here to Raise an issue in github.');
});
});
describe('Verify types of URLs', () => {
it('should load compressed URL', () => {
cy.visit(
'/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ'
);
cy.contains('New Year');
cy.visit(
'/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ'
);
cy.contains('Animal');
});
describe('Uncompressed URLs', () => {
it('should load URL without specifier', () => {
cy.visit(
'/edit/#eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('TestLabel');
cy.visit(
'/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('Animal');
});
it('should load URL with "base64" specifier', () => {
cy.visit(
'/edit/#base64:eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('TestLabel');
});
});
});
-56
View File
@@ -1,56 +0,0 @@
describe('Test themes', () => {
describe('Test light themes', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: false
});
}
});
cy.contains('Theme').click();
});
it('should set light theme as default', () => {
cy.contains('light').parent().should('have.class', 'bordered');
cy.contains('dark').parent().should('not.have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
it('should change themes when clicked', () => {
cy.contains('light').parent().should('have.class', 'bordered');
cy.contains('cupcake').click();
cy.contains('cupcake').parent().should('have.class', 'bordered');
cy.contains('light').parent().should('not.have.class', 'bordered');
cy.contains('dark').parent().should('not.have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
});
describe('Test dark mode', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: true
});
}
});
cy.contains('Theme').click();
});
it('should set dark theme as default', () => {
cy.contains('light').parent().should('not.have.class', 'bordered');
cy.contains('dark').parent().should('have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
});
});
-55
View File
@@ -1,55 +0,0 @@
export const cmd = `{${Cypress.platform === 'darwin' ? 'meta' : 'ctrl'}}`;
interface EditorOptions {
bottom?: boolean;
newline?: boolean;
}
export const typeInEditor = (
text: string,
{ bottom = true, newline = false }: EditorOptions = {}
) => {
cy.window().should('have.property', 'editorLoaded', true);
cy.get('#editor').click();
cy.get('#editor').within(() => {
if (bottom) {
cy.get('textarea').type('{pageDown}', { force: true });
}
if (newline) {
cy.get('textarea').type('{enter}', { force: true });
}
cy.get('textarea').type(text, { force: true });
});
};
const downloadsFolder = Cypress.config('downloadsFolder');
export const verifyFileSizeGreaterThan = (
fileType: 'history' | 'diagram',
extension: string,
size: number
) => {
cy.get('#view').should('not.have.class', 'outOfSync');
cy.task('readAndDeleteFile', {
folder: downloadsFolder,
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
mode: 'size'
}).then((fileSize: number) => {
expect(fileSize).to.be.gt(size);
expect(fileSize).to.be.lt(size * 1.3);
});
};
export const verifyFileSnapshot = (
fileType: 'history' | 'diagram',
extension: string,
content: string
) => {
cy.task('readAndDeleteFile', {
folder: downloadsFolder,
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
mode: 'content'
}).then((fileContent: number) => {
expect(fileContent).to.contain(content);
});
};
-5
View File
@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
-52
View File
@@ -1,52 +0,0 @@
module.exports = {
"Site Loads": {
"Check Home page load": {
"1": "{\"code\":\"flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true}"
},
"Check Redirect from old URL": {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true}"
},
"should load diagram from gist": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a\"}}}"
},
"should load diagram from gist revision": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19\"}}}"
},
"should load diagram from raw files": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}"
}
},
"__version": "12.17.4",
"Auto sync tests": {
"should dim diagram when code is edited": {
"1": "{\"code\":\"flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":false,\"updateDiagram\":false}"
},
"should not dim diagram when code is in sync": {
"1": "{\"code\":\"flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Testing\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":false}"
}
},
"Test themes": {
"should set light theme as default": {
"1": "{\"theme\":\"light\",\"isDark\":false}"
},
"should change themes when clicked": {
"1": "{\"theme\":\"cupcake\",\"isDark\":false}"
},
"should set dark theme as default": {
"1": "{\"theme\":\"dark\",\"isDark\":true}"
},
"Test light themes": {
"should set light theme as default": {
"1": "{\"theme\":\"light\",\"isDark\":false}"
},
"should change themes when clicked": {
"1": "{\"theme\":\"cupcake\",\"isDark\":false}"
}
},
"Test dark mode": {
"should set dark theme as default": {
"1": "{\"theme\":\"dark\",\"isDark\":true}"
}
}
}
}
-38
View File
@@ -1,38 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import { register } from '@cypress/snapshot';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
register();
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
snapshot(): void;
}
}
}
import 'cypress-localstorage-commands';
-27
View File
@@ -1,27 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false;
}
});
-7
View File
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"types": ["cypress", "cypress-localstorage-commands", "node"]
},
"include": ["**/*.ts"]
}
+74
View File
@@ -0,0 +1,74 @@
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import sortKeysPlugin from 'eslint-plugin-sort-keys';
import svelte from 'eslint-plugin-svelte';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.strict,
...ts.configs.stylistic,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
plugins: {
unicorn: eslintPluginUnicorn
},
rules: {
'unicorn/no-null': 'off',
'unicorn/filename-case': 'off'
}
},
{
files: ['src/**'],
plugins: {
'sort-keys': sortKeysPlugin
},
rules: {
'sort-keys/sort-keys-fix': ['error', 'asc', { minKeys: 5 }]
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
},
rules: {
'svelte/no-unused-props': 'off'
}
},
{
files: ['**/components/ui/**'],
rules: {
'unicorn/prefer-export-from': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/explicit-length-check': 'off',
'sort-keys/sort-keys-fix': 'off'
}
}
);
+10
View File
@@ -1,3 +1,13 @@
[build.environment] [build.environment]
MERMAID_ANALYTICS_URL = 'https://p.mermaid.live' MERMAID_ANALYTICS_URL = 'https://p.mermaid.live'
MERMAID_DOCS_URL = 'https://mermaid.js.org'
MERMAID_DOMAIN = 'mermaid.live' MERMAID_DOMAIN = 'mermaid.live'
MERMAID_RENDERER_URL = 'https://mermaid.ink'
MERMAID_KROKI_RENDERER_URL = 'https://kroki.io'
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS ='true'
[[redirects]]
from = "/index.html"
to = "/edit"
status = 301
force = true
+108 -69
View File
@@ -5,96 +5,135 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"dev:force": "MERMAID_LOCAL=true yarn dev --force", "dev:force": "MERMAID_LOCAL=true pnpm dev --force",
"dev:test": "yarn dev", "dev:test": "pnpm dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "prettier --check --cache . && eslint --ignore-path .gitignore .", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"lint:fix": "prettier --write --cache . && eslint --fix --ignore-path .gitignore .", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check --cache . && eslint .",
"lint:fix": "prettier --write --cache . && eslint --fix .",
"format": "prettier --write --cache .", "format": "prettier --write --cache .",
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"postinstall": "husky install && svelte-kit sync && (git config blame.ignoreRevsFile .git-blame-ignore-revs || true)", "postinstall": "husky install && svelte-kit sync && (git config blame.ignoreRevsFile .git-blame-ignore-revs || true)",
"test:unit": "vitest", "test:unit": "vitest",
"test:ui": "vitest --ui", "test:unit:ui": "vitest --ui",
"test:coverage": "vitest run --coverage", "test:unit:coverage": "vitest run --coverage",
"test:browser": "cypress run", "test": "pnpm test:unit && pnpm test:e2e",
"test": "test:unit && test:browser", "test:e2e": "playwright test",
"cy": "cypress open" "test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}, },
"devDependencies": { "devDependencies": {
"@cypress/snapshot": "2.1.7", "@eslint/compat": "^2.1.0",
"@fortawesome/fontawesome-free": "^6.5.1", "@eslint/eslintrc": "^3.3.5",
"@sveltejs/adapter-static": "3.0.2", "@eslint/js": "^10.0.1",
"@sveltejs/kit": "2.5.16", "@fortawesome/fontawesome-free": "^7.2.0",
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@iconify-json/hugeicons": "^1.2.29",
"@testing-library/svelte": "4.2.3", "@iconify-json/logos": "^1.2.11",
"@iconify-json/material-symbols": "^1.2.79",
"@iconify-json/mdi": "^1.2.3",
"@playwright/test": "^1.60.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.63.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.1",
"@types/hammerjs": "^2.0.46",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/pako": "2.0.3", "@types/node": "^24.13.2",
"@types/uuid": "9.0.8", "@types/pako": "2.0.4",
"@typescript-eslint/eslint-plugin": "6.21.0", "@vitest/coverage-v8": "^4.1.9",
"@typescript-eslint/parser": "6.21.0", "@vitest/ui": "^4.1.9",
"@vitest/ui": "^1.1.3", "autoprefixer": "^10.5.0",
"autoprefixer": "^10.4.14", "bits-ui": "^2.18.1",
"c8": "7.14.0", "c8": "11.0.0",
"chai": "^4.3.7", "chai": "^6.2.2",
"cssnano": "^6.0.0", "clsx": "^2.1.1",
"cypress": "12.17.4", "cssnano": "^8.0.2",
"cypress-localstorage-commands": "2.2.6", "dotenv": "^17.4.2",
"eslint": "8.57.0", "eslint": "^10.4.1",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-cypress": "2.15.2",
"eslint-plugin-es": "^4.1.0", "eslint-plugin-es": "^4.1.0",
"eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-postcss-modules": "^2.0.0", "eslint-plugin-sort-keys": "^2.3.5",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^3.19.0",
"eslint-plugin-tailwindcss": "^3.13.1", "eslint-plugin-tailwindcss": "^3.18.3",
"eslint-plugin-unicorn": "^50.0.1", "eslint-plugin-unicorn": "^65.0.1",
"eslint-plugin-vitest": "^0.5.0", "globals": "^17.6.0",
"esserializer": "^1.3.11", "husky": "^9.1.7",
"husky": "^8.0.3", "jsdom": "^29.1.1",
"jsdom": "^21.1.2", "lint-staged": "^17.0.7",
"lint-staged": "^15.2.0", "lucide-svelte": "^1.0.1",
"node-html-parser": "^6.1.5", "node-html-parser": "^7.1.0",
"postcss": "^8.4.33", "paneforge": "1.0.2",
"postcss-load-config": "5.1.0", "prettier": "^3.8.4",
"prettier": "^3.1.0", "prettier-plugin-svelte": "^4.1.1",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-tailwindcss": "^0.8.0",
"prettier-plugin-tailwindcss": "^0.6.0", "svelte": "^5.56.3",
"svelte": "^4.2.8", "svelte-check": "^4.6.0",
"svelte-preprocess": "^5.1.3", "svelte-preprocess": "^6.0.5",
"tailwindcss": "^3.4.1", "svelte-sonner": "^1.1.1",
"tslib": "^2.6.2", "tailwind-merge": "^3.6.0",
"typescript": "^5.3.3", "tailwind-variants": "^3.2.2",
"vite": "^5.0.11", "tailwindcss": "^4.3.1",
"vitest": "^1.1.3", "tslib": "^2.8.1",
"tw-animate-css": "^1.4.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1",
"unplugin-icons": "^23.0.1",
"vite": "^8.0.16",
"vite-plugin-devtools-json": "^1.0.0",
"vitest": "^4.1.9",
"vitest-dom": "^0.1.1" "vitest-dom": "^0.1.1"
}, },
"dependencies": { "dependencies": {
"@mermaid-js/mermaid-zenuml": "^0.2.0", "@codemirror/lang-json": "^6.0.2",
"daisyui": "2.52.0", "@codemirror/lang-markdown": "^6.5.0",
"dayjs": "^1.11.7", "@codemirror/lang-yaml": "^6.1.3",
"js-base64": "3.7.7", "@codemirror/language": "^6.12.3",
"lodash-es": "^4.17.21", "@codemirror/state": "^6.6.0",
"mermaid": "10.9.1", "@codemirror/view": "^6.43.1",
"monaco-editor": "0.49.0", "@fontsource-variable/recursive": "^5.2.8",
"@fsegurai/codemirror-theme-vscode-dark": "^6.2.6",
"@fsegurai/codemirror-theme-vscode-light": "^6.2.6",
"@mermaid-js/examples": "^1.2.0",
"@mermaid-js/layout-elk": "^0.2.1",
"@mermaid-js/layout-tidy-tree": "^0.2.2",
"@mermaid-js/mermaid-zenuml": "^0.2.3",
"codemirror": "^6.0.2",
"dayjs": "^1.11.21",
"hammerjs": "^2.0.8",
"js-base64": "3.7.8",
"lodash-es": "^4.18.1",
"mermaid": "^11.15.0",
"mode-watcher": "^1.1.0",
"monaco-editor": "0.55.1",
"pako": "2.1.0", "pako": "2.1.0",
"plausible-tracker": "^0.3.8", "plausible-tracker": "^0.3.9",
"random-word-slugs": "0.1.7", "random-word-slugs": "0.1.7",
"svg-pan-zoom": "3.6.1", "svg-pan-zoom": "3.6.2",
"svg2roughjs": "^3.2.0", "svg2roughjs": "^3.2.3",
"uuid": "9.0.1" "uuid": "14.0.0"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,svelte,js,css,md,json}": [ "*.{ts,svelte,js,css,md,json}": [
"prettier --plugin-search-dir=. --write", "prettier --plugin-search-dir=. --write",
"eslint --ignore-path .gitignore " "eslint "
] ]
}, },
"volta": {
"node": "18.20.3",
"yarn": "1.22.22"
},
"engines": { "engines": {
"node": ">=16.7" "node": ">=24.16.0"
},
"packageManager": "pnpm@10.34.4+sha512.8768be55200ae3f2226b6527fcca2687e14bc4e5f12d7721a0f25da3df47915177058648db4177baf348120fa0ba2752d8d8d93f6beaf1fe64ae18da8de961af",
"pnpm": {
"onlyBuiltDependencies": [
"deasync",
"esbuild",
"svelte-preprocess"
],
"ignoredBuiltDependencies": [
"vue-demi"
]
} }
} }
+28
View File
@@ -0,0 +1,28 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
forbidOnly: !!process.env.CI,
fullyParallel: true,
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
reporter: process.env.CI ? [['github'], ['list']] : 'list',
retries: process.env.CI ? 2 : 0,
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
browserName: 'chromium',
permissions: ['clipboard-read', 'clipboard-write'],
trace: 'retain-on-failure',
viewport: { width: 1920, height: 1080 }
},
webServer: {
command: `pnpm ${process.env.CI ? 'preview' : 'dev'}`,
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
},
workers: process.env.CI ? 3 : undefined
});
+7273
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
module.exports = {
plugins: [
// Some plugins, like postcss-nested, need to run before Tailwind
tailwindcss,
// But others, like autoprefixer, need to run after
autoprefixer,
!dev &&
cssnano({
preset: 'default'
})
]
};
+2 -1
View File
@@ -24,5 +24,6 @@
"major": { "major": {
"dependencyDashboardApproval": true "dependencyDashboardApproval": true
}, },
"dependencyDashboardAutoclose": true "dependencyDashboardAutoclose": true,
"rangeStrategy": "bump"
} }
+119
View File
@@ -0,0 +1,119 @@
@import '@fontsource-variable/recursive/crsv.css';
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@layer base {
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--primary: hsl(240 10% 91%);
--primary-foreground: hsl(255 20% 15%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(228 24% 96%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(340 100% 44%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 72.22% 50.59%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--border-dark: hsl(214.3 31.8% 81.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(222.2 84% 4.9%);
--radius: 0.75rem;
}
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--card: hsl(222.2 84% 4.9%);
--card-foreground: hsl(210 40% 98%);
--popover: hsl(222.2 84% 4.9%);
--popover-foreground: hsl(210 40% 98%);
--primary: hsl(210 40% 30%);
--primary-foreground: hsl(222.2 47.4% 90%);
--secondary: hsl(217.2 32.6% 17.5%);
--secondary-foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.6% 17.5%);
--muted-foreground: hsl(215 20.2% 65.1%);
--accent: hsl(340 100% 44%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(217.2 32.6% 17.5%);
--border-dark: hsl(217.2 32.6% 27.5%);
--input: hsl(217.2 32.6% 17.5%);
--ring: hsl(212.7 26.8% 83.9%);
}
}
@theme inline {
/* Radius (for rounded-*) */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Colors */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-border-dark: var(--border-dark);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring);
--color-radius: var(--radius);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border;
}
body {
@apply h-screen w-screen overflow-hidden bg-background text-foreground;
}
}
body {
font-family: 'Recursive Variable', sans-serif;
}
.d {
@apply border border-red-500;
}
+5 -7
View File
@@ -4,20 +4,18 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title> <title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title>
<meta <meta name="og:image" content="%sveltekit.assets%/favicon.svg" />
name="og:image"
content="https://github.com/mermaid-js/mermaid/raw/develop/img/header.png" />
<link rel="canonical" href="https://mermaid.live" />
<meta <meta
name="description" name="description"
content="Simplify documentation and avoid heavy tools. Open source Visio Alternative. Commonly used for explaining your code! Mermaid is a simple markdown-like script language for generating charts from text via javascript." /> content="Simplify documentation and avoid heavy tools. Open source Visio Alternative. Commonly used for explaining your code! Mermaid is a simple markdown-like script language for generating charts from text via javascript." />
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png" />
<link rel="mask-icon" href="%sveltekit.assets%/favicon.svg" color="#000000" /> <link rel="mask-icon" href="%sveltekit.assets%/favicon.svg" color="#000000" />
<meta name="theme-color" content="#6366F1" /> <meta name="theme-color" content="#ff3670" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" /> <link rel="manifest" href="%sveltekit.assets%/manifest.json" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body style="overflow: hidden"> <body>
<div id="svelte">%sveltekit.body%</div> <div id="svelte">%sveltekit.body%</div>
</body> </body>
</html> </html>
-17
View File
@@ -1,17 +0,0 @@
@import '@fortawesome/fontawesome-free/css/all.min.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
.input {
@apply flex-1 rounded border-2 border-solid border-primary;
}
.action-btn {
@apply btn btn-primary;
}
#graph-div sub,
#graph-div sup {
position: initial;
vertical-align: revert;
}
+4
View File
@@ -4,7 +4,11 @@ interface ImportMetaEnv {
readonly MERMAID_RENDERER_URL?: string; readonly MERMAID_RENDERER_URL?: string;
readonly MERMAID_KROKI_RENDERER_URL?: string; readonly MERMAID_KROKI_RENDERER_URL?: string;
readonly MERMAID_ANALYTICS_URL?: string; readonly MERMAID_ANALYTICS_URL?: string;
readonly MERMAID_DOCS_URL?: string;
readonly MERMAID_DOMAIN?: string; readonly MERMAID_DOMAIN?: string;
readonly MERMAID_IS_ENABLED_MERMAID_CHART_LINKS?: string;
readonly MERMAID_PRIVACY_POLICY_URL?: string;
readonly MERMAID_HIDE_PRIVACY_POLICY?: string;
// more env variables... // more env variables...
} }
-6
View File
@@ -1,6 +0,0 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {});
return response;
};
+174
View File
@@ -0,0 +1,174 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import { cn } from '$lib/utils.js';
import CloseIcon from '~icons/material-symbols/close-rounded';
interface Props {
show: boolean;
input: string;
onClose: () => void;
onHeightChange?: (height: number) => void;
onTryFree: () => void;
}
let { show, input = $bindable(), onClose, onHeightChange, onTryFree }: Props = $props();
let textarea = $state<HTMLTextAreaElement>();
let container = $state<HTMLDivElement>();
$effect(() => {
if (!container || !onHeightChange) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target instanceof HTMLElement) {
onHeightChange(entry.target.offsetHeight);
}
}
});
observer.observe(container);
return () => observer.disconnect();
});
function resizeTextarea() {
if (!textarea) return;
const computed = globalThis.getComputedStyle(textarea);
const lineHeight =
Number.parseFloat(computed.lineHeight) || Number.parseFloat(computed.fontSize) * 1.5 || 0;
const paddingTop = Number.parseFloat(computed.paddingTop) || 0;
const paddingBottom = Number.parseFloat(computed.paddingBottom) || 0;
const minHeight = lineHeight + paddingTop + paddingBottom;
const maxLines = 8;
const maxHeight = lineHeight * maxLines + paddingTop + paddingBottom;
textarea.style.height = 'auto';
const nextHeight = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight));
textarea.style.height = `${nextHeight}px`;
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
}
$effect(() => {
if (input !== undefined) {
resizeTextarea();
}
});
function handleKeydown(e: KeyboardEvent) {
if (show && e.key === 'Escape') {
onClose();
}
}
function handleOutsideClick(e: MouseEvent) {
if (show && container && e.target instanceof Node && !container.contains(e.target)) {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} onmousedown={handleOutsideClick} />
{#if show}
<div
bind:this={container}
class={cn(
'button-container-for-animation relative z-50 mr-6 flex w-auto flex-col gap-2 rounded-xl border-2 border-border bg-background p-2 shadow-xl dark:border-border-dark dark:bg-secondary',
!input.trim() && 'rainbow-border'
)}
role="dialog"
aria-modal="true"
tabindex="-1">
<div class="relative flex min-h-2 items-start gap-1 px-1">
<textarea
bind:this={textarea}
bind:value={input}
onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim()) {
onTryFree();
}
}
}}
placeholder="Describe what to add or change"
rows="1"
class="focus font-recursive min-h-0 flex-1 resize-none border-none bg-transparent px-1 text-sm font-normal text-foreground placeholder:text-muted-foreground focus:ring-0 focus:outline-none disabled:opacity-50 dark:text-foreground dark:placeholder:text-muted-foreground"
style="height: 20px; overflow-y: hidden;"></textarea>
<button onclick={onClose} class="text-muted-foreground hover:text-foreground">
<CloseIcon class="size-4" />
</button>
</div>
<div class="flex items-center justify-between">
<span class="font-recursive text-xs font-normal text-foreground dark:text-foreground"
>Sign Up at Mermaid.ai to try AI</span>
<Button
class="font-recursive h-6 w-16 gap-1.5 rounded-sm bg-accent p-1 text-xs font-medium text-white no-underline hover:bg-accent/90 hover:text-white hover:no-underline active:bg-accent/80 dark:bg-accent dark:text-white! dark:hover:bg-accent/90 dark:active:bg-accent/80"
onclick={onTryFree}>
Try free
</Button>
</div>
</div>
{/if}
<style>
@property --gradient-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
@keyframes gradient-angle-shift {
0% {
--gradient-angle: 0deg;
}
100% {
--gradient-angle: 360deg;
}
}
.button-container-for-animation {
border-radius: 12px;
}
.button-container-for-animation::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 14px;
padding: 2px;
background: conic-gradient(
from var(--gradient-angle, 0deg),
color-mix(in srgb, var(--color-accent) 0%, transparent) 0%,
color-mix(in srgb, var(--color-accent) 0%, transparent) 12%,
color-mix(in srgb, var(--color-accent) 12%, transparent) 18%,
color-mix(in srgb, var(--color-accent) 42%, transparent) 24%,
color-mix(in srgb, var(--color-accent) 77%, transparent) 32%,
rgba(93, 85, 212, 0.923) 41%,
rgba(93, 85, 212, 0.5) 52%,
color-mix(in srgb, var(--color-accent) 58%, transparent) 72%,
color-mix(in srgb, var(--color-accent) 56%, transparent) 82%,
color-mix(in srgb, var(--color-accent) 36%, transparent) 87%,
color-mix(in srgb, var(--color-accent) 19%, transparent) 92%,
color-mix(in srgb, var(--color-accent) 10%, transparent) 96%,
color-mix(in srgb, var(--color-accent) 3%, transparent) 98%,
color-mix(in srgb, var(--color-accent) 0%, transparent) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
z-index: 0;
opacity: 0;
transition: opacity 0.3s ease-out;
}
.button-container-for-animation.rainbow-border::before {
opacity: 1;
animation: gradient-angle-shift 2s linear infinite;
}
</style>
+198 -144
View File
@@ -1,33 +1,93 @@
<script lang="ts"> <script lang="ts">
import Card from '$/components/Card/Card.svelte';
import CopyButton from '$/components/CopyButton.svelte';
import CopyInput from '$/components/CopyInput.svelte';
import ExternalLinkWrapper from '$/components/ExternalLinkWrapper.svelte';
import { Button } from '$/components/ui/button';
import { Input } from '$/components/ui/input';
import { Separator } from '$/components/ui/separator';
import * as ToggleGroup from '$/components/ui/toggle-group';
import { TID } from '$/constants';
import { getDomain } from '$/util/util';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import Card from '$lib/components/Card/Card.svelte';
import { waitForRender } from '$lib/util/autoSync'; import { waitForRender } from '$lib/util/autoSync';
import { env } from '$lib/util/env'; import { inputState, updateCodeStore, urls, validatedState } from '$lib/util/state.svelte';
import { pakoSerde } from '$lib/util/serde';
import { stateStore } from '$lib/util/state';
import { logEvent } from '$lib/util/stats'; import { logEvent } from '$lib/util/stats';
import { version as FAVersion } from '@fortawesome/fontawesome-free/package.json';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { toBase64 } from 'js-base64'; import { toBase64 } from 'js-base64';
import { version as FAVersion } from '@fortawesome/fontawesome-free/package.json'; import DownloadIcon from '~icons/material-symbols/download';
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
import WidthIcon from '~icons/material-symbols/width-rounded';
const FONT_AWESOME_URL = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}/css/all.min.css`; const FONT_AWESOME_URL = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}/css/all.min.css`;
const { krokiRendererUrl, rendererUrl } = env;
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void; type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
const getFileName = (extension: string) => const getFileName = (extension: string) =>
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`; `mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`;
/**
* Fix text clipping in exported SVG for hand-drawn (rough) mode.
* svg2roughjs copies foreignObject elements but their height is often insufficient,
* causing text bottom edges to be cut off regardless of language.
*/
const fixForeignObjectClipping = (svg: HTMLElement) => {
const foreignObjects = svg.querySelectorAll('foreignObject');
foreignObjects.forEach((foreignObj) => {
const currentHeight = parseFloat(foreignObj.getAttribute('height') || '0');
if (currentHeight <= 0) return;
const currentY = parseFloat(foreignObj.getAttribute('y') || '0');
const newHeight = currentHeight * 1.5;
const heightDiff = newHeight - currentHeight;
foreignObj.setAttribute('height', newHeight.toString());
foreignObj.setAttribute('y', (currentY - heightDiff / 2).toString());
// Ensure inner HTML elements are vertically centered within the expanded area
const htmlElements = foreignObj.querySelectorAll('div, span, p');
htmlElements.forEach((htmlEl) => {
const el = htmlEl as HTMLElement;
el.style.display = 'flex';
el.style.alignItems = 'center';
el.style.justifyContent = 'center';
el.style.height = '100%';
});
});
};
const getSvgElement = () => {
const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement;
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
return svgElement;
};
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => { const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
if (svg) { if (svg) {
// Prevents the SVG size of the interface from being changed // Prevents the SVG size of the interface from being changed
svg = svg.cloneNode(true) as HTMLElement; svg = svg.cloneNode(true) as HTMLElement;
} }
height && svg?.setAttribute('height', `${height}px`); if (height) {
width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage svg?.setAttribute('height', `${height}px`);
}
if (width) {
svg?.setAttribute('width', `${width}px`);
}
// Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
if (!svg) { if (!svg) {
svg = getSvgElement(); svg = getSvgElement();
} }
if (validatedState.current.rough) {
fixForeignObjectClipping(svg);
}
svg.style.backgroundColor = window
.getComputedStyle(document.body)
.getPropertyValue('--background');
const svgString = svg.outerHTML const svgString = svg.outerHTML
.replaceAll('<br>', '<br/>') .replaceAll('<br>', '<br/>')
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`); .replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
@@ -37,50 +97,6 @@
${svgString}`); ${svgString}`);
}; };
const exportImage = async (event: Event, exporter: Exporter) => {
await waitForRender();
if (document.querySelector('.outOfSync')) {
throw new Error('Diagram is out of sync');
}
const canvas: HTMLCanvasElement = document.createElement('canvas');
const svg = document.querySelector<HTMLElement>('#container svg');
if (!svg) {
throw new Error('svg not found');
}
const box: DOMRect = svg.getBoundingClientRect();
canvas.width = box.width;
canvas.height = box.height;
if (imagemodeselected === 'width') {
const ratio = box.height / box.width;
canvas.width = userimagesize;
canvas.height = userimagesize * ratio;
} else if (imagemodeselected === 'height') {
const ratio = box.width / box.height;
canvas.width = userimagesize * ratio;
canvas.height = userimagesize;
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('context not found');
}
context.fillStyle = `hsl(${window.getComputedStyle(document.body).getPropertyValue('--b1')})`;
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
image.addEventListener('load', exporter(context, image));
image.src = `data:image/svg+xml;base64,${getBase64SVG(svg, canvas.width, canvas.height)}`;
event.stopPropagation();
event.preventDefault();
};
const getSvgElement = () => {
const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement;
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
return svgElement;
};
const simulateDownload = (download: string, href: string): void => { const simulateDownload = (download: string, href: string): void => {
const a = document.createElement('a'); const a = document.createElement('a');
a.download = download; a.download = download;
@@ -88,6 +104,65 @@ ${svgString}`);
a.click(); a.click();
a.remove(); a.remove();
}; };
const exportImage = async (event: Event, exporter: Exporter) => {
updateCodeStore({ panZoom: false });
await new Promise((resolve) => setTimeout(resolve, 1000));
await waitForRender();
const canvas = document.createElement('canvas');
const svg = document.querySelector<HTMLElement>('#container svg');
if (!svg) {
throw new Error('svg not found');
}
const box = svg.getBoundingClientRect();
// In rough mode, SVG has width/height="100%" so getBoundingClientRect returns
// the container size, not the actual diagram size. Use viewBox dimensions instead.
const svgEl = svg as unknown as SVGSVGElement;
const viewBox = svgEl.viewBox?.baseVal;
const contentWidth = viewBox && viewBox.width > 0 ? viewBox.width : box.width;
const contentHeight = viewBox && viewBox.height > 0 ? viewBox.height : box.height;
if (imageSizeMode === 'width') {
const ratio = contentHeight / contentWidth;
canvas.width = imageSize;
canvas.height = imageSize * ratio;
} else if (imageSizeMode === 'height') {
const ratio = contentWidth / contentHeight;
canvas.width = imageSize * ratio;
canvas.height = imageSize;
} else {
const multiplier = 2;
canvas.width = contentWidth * multiplier;
canvas.height = contentHeight * multiplier;
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('context not found');
}
context.fillStyle = window.getComputedStyle(document.body).getPropertyValue('--background');
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
image.addEventListener('load', () => {
exporter(context, image)();
updateCodeStore({ panZoom: true });
});
image.src = `data:image/svg+xml;base64,${getBase64SVG(svg, canvas.width, canvas.height)}`;
// Fallback to set panZoom to true after 2 seconds
// This is a workaround for the case when the image is not loaded
setTimeout(() => {
if (!inputState.panZoom) {
updateCodeStore({ panZoom: true });
}
}, 2000);
event.stopPropagation();
event.preventDefault();
};
const downloadImage: Exporter = (context, image) => { const downloadImage: Exporter = (context, image) => {
return () => { return () => {
const { canvas } = context; const { canvas } = context;
@@ -100,7 +175,7 @@ ${svgString}`);
}; };
const isClipboardAvailable = (): boolean => { const isClipboardAvailable = (): boolean => {
return Object.prototype.hasOwnProperty.call(window, 'ClipboardItem') as boolean; return Object.prototype.hasOwnProperty.call(window, 'ClipboardItem');
}; };
const clipboardCopy: Exporter = (context, image) => { const clipboardCopy: Exporter = (context, image) => {
@@ -124,7 +199,10 @@ ${svgString}`);
}; };
}; };
const onCopyClipboard = async (event: Event) => { const onCopyClipboard = async (event?: Event) => {
if (!event) {
return;
}
await exportImage(event, clipboardCopy); await exportImage(event, clipboardCopy);
logEvent('copyClipboard'); logEvent('copyClipboard');
}; };
@@ -143,120 +221,96 @@ ${svgString}`);
}); });
}; };
const onCopyMarkdown = () => { let gistURL = $state('');
document.querySelector<HTMLInputElement>('#markdown')?.select(); $effect(() => {
document.execCommand('Copy'); const { loader } = validatedState.current;
logEvent('copyMarkdown');
};
let gistURL = '';
stateStore.subscribe(({ loader }) => {
if (loader?.type === 'gist') { if (loader?.type === 'gist') {
// @ts-expect-error Gist will have url
gistURL = loader.config.url; gistURL = loader.config.url;
} }
}); });
const loadGist = () => { const loadGist = () => {
if (!gistURL) { if (!gistURL) {
alert('Please enter a Gist URL first'); return alert('Please enter a Gist URL first');
} }
window.location.href = `${window.location.pathname}?gist=${gistURL}`; window.location.href = `${window.location.pathname}?gist=${gistURL}`;
logEvent('loadGist'); logEvent('loadGist');
}; };
let iUrl: string; let imageSizeMode: 'auto' | 'width' | 'height' = $state('auto');
let svgUrl: string;
let krokiUrl: string;
let mdCode: string;
let imagemodeselected = 'auto';
let userimagesize = 1080;
let isNetlify = false; $effect(() => {
if (browser && ['mermaid.live', 'netlify'].some((path) => window.location.host.includes(path))) { if (!imageSizeMode) {
isNetlify = true; imageSizeMode = 'auto';
} }
stateStore.subscribe(({ code, serialized }) => {
iUrl = `${rendererUrl}/img/${serialized}?type=png`;
svgUrl = `${rendererUrl}/svg/${serialized}`;
krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize(code)}`;
mdCode = `[![](${iUrl})](${window.location.protocol}//${window.location.host}${window.location.pathname}#${serialized})`;
}); });
let imageSize = $state(1080);
const isNetlify = browser && window.location.host.includes('netlify');
</script> </script>
<Card title="Actions" isOpen={false}> {#snippet dualActionButton(text: string, download: (event: Event) => unknown, url?: string)}
<div class="m-2 flex flex-wrap gap-2"> <div class="flex flex-grow gap-0.5">
{#if isClipboardAvailable()} <Button
<button class="action-btn w-full" on:click={onCopyClipboard} class={['flex-grow', url && 'rounded-r-none']}
><i class="far fa-copy mr-2" /> Copy Image to clipboard onclick={download}
</button> data-testid="download-{text}">
{/if} <DownloadIcon />
<button id="downloadPNG" class="action-btn flex-grow" on:click={onDownloadPNG}> {text}
<i class="fas fa-download mr-2" /> PNG </Button>
</button> <ExternalLinkWrapper domain={getDomain(url)} isVisible={!!url}>
<button id="downloadSVG" class="action-btn flex-grow" on:click={onDownloadSVG}> <Button class="rounded-l-none" href={url} target="_blank" rel="noreferrer noopener">
<i class="fas fa-download mr-2" /> SVG <ExternalLinkIcon />
</button> </Button>
<a target="_blank" rel="noreferrer" class="flex-grow" href={iUrl}> </ExternalLinkWrapper>
<button class="action-btn w-full"> </div>
<i class="fas fa-external-link-alt mr-2" /> PNG {/snippet}
</button>
</a>
<a target="_blank" rel="noreferrer" class="flex-grow" href={svgUrl}>
<button class="action-btn w-full">
<i class="fas fa-external-link-alt mr-2" /> SVG
</button>
</a>
<a target="_blank" rel="noreferrer" class="flex-grow" href={krokiUrl}>
<button class="action-btn w-full">
<i class="fas fa-external-link-alt mr-2" /> Kroki
</button>
</a>
<div class="flex items-center gap-2"> <Card title="Actions" isStackable icon={{ component: DownloadIcon, class: 'rotate-180' }}>
<div class="flex min-w-fit flex-col gap-2 p-2">
<div class="flex w-full items-center gap-2 py-2 whitespace-nowrap">
PNG size PNG size
<label for="autosize"> <ToggleGroup.Root type="single" variant="outline" bind:value={imageSizeMode}>
<input type="radio" value="auto" id="autosize" bind:group={imagemodeselected} /> Auto <ToggleGroup.Item value="auto">Auto</ToggleGroup.Item>
</label> <ToggleGroup.Item value="width">Width</ToggleGroup.Item>
<ToggleGroup.Item value="height">Height</ToggleGroup.Item>
<label for="width"> </ToggleGroup.Root>
<input type="radio" value="width" id="width" bind:group={imagemodeselected} /> Width {#if imageSizeMode !== 'auto'}
</label> <WidthIcon
class={['size-6 shrink-0 transition-all', imageSizeMode === 'width' && 'rotate-90']} />
<label for="height"> {/if}
<input type="radio" value="height" id="height" bind:group={imagemodeselected} /> Height <Input
</label>
{#if imagemodeselected !== 'auto'}
<input
id="height"
class="input"
type="number" type="number"
min="3" min="3"
max="10000" max="10000"
bind:value={userimagesize} /> disabled={imageSizeMode === 'auto'}
bind:value={imageSize} />
</div>
<div class="flex gap-2">
{@render dualActionButton('PNG', onDownloadPNG, urls.current.png)}
{@render dualActionButton('SVG', onDownloadSVG, urls.current.svg)}
<ExternalLinkWrapper domain={getDomain(urls.current.kroki)} isVisible={!!urls.current.kroki}>
<a target="_blank" rel="noreferrer" class="flex-grow" href={urls.current.kroki}>
<Button class="action-btn flex w-full items-center gap-2">
<ExternalLinkIcon /> Kroki
</Button>
</a>
</ExternalLinkWrapper>
</div>
<Separator />
{#if isClipboardAvailable()}
<CopyButton onclick={onCopyClipboard} label="Copy Image" />
{/if} {/if}
</div> <ExternalLinkWrapper
labelPrefix="Thumbnail generated by"
domain={getDomain(urls.current.png)}
isVisible={!!urls.current.mdCode}>
<CopyInput value={urls.current.mdCode} label="Copy Markdown" testID={TID.copyMarkdown} />
</ExternalLinkWrapper>
<div class="flex w-full items-center gap-2"> <div class="flex w-full items-center gap-2">
<input class="input" id="markdown" type="text" value={mdCode} on:click={onCopyMarkdown} /> <Input type="url" bind:value={gistURL} placeholder="Enter Gist URL" />
<label for="markdown"> <Button onclick={loadGist}>Load Gist</Button>
<button class="btn btn-primary btn-md flex-auto" on:click={onCopyMarkdown}>
Copy Markdown
</button>
</label>
</div>
<div class="flex w-full items-center gap-2">
<input
class="input"
id="gist"
type="text"
bind:value={gistURL}
placeholder="Enter Gist URL" />
<label for="gist">
<button class="btn btn-primary btn-md flex-auto" on:click={loadGist}> Load Gist </button>
</label>
</div> </div>
{#if isNetlify} {#if isNetlify}
<div class="flex w-full items-center justify-center"> <div class="flex w-full items-center justify-center">
+72 -20
View File
@@ -1,33 +1,85 @@
<script lang="ts"> <script lang="ts">
import type { Tab } from '$lib/types'; import type { Tab } from '$/types';
import type { Component, Snippet } from 'svelte';
import { quintOut } from 'svelte/easing';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import CollapseAllIcon from '~icons/material-symbols/collapse-all-rounded';
import Tabs from './Tabs.svelte'; import Tabs from './Tabs.svelte';
export let isCloseable = true;
export let isOpen = true; interface Props {
export let tabs: Tab[] = []; isClosable?: boolean;
export let activeTabID = ''; isOpen?: boolean;
export let title: string; isStackable?: boolean;
$: isOpen = isCloseable ? isOpen : true; tabs?: Tab[];
$: isTabsShown = isOpen && tabs.length > 0; activeTabID?: string;
title?: string;
icon?: {
component: Component;
class?: string;
};
onselect?: (tab: Tab) => void;
actions?: Snippet;
children: Snippet;
}
let {
isClosable = true,
isOpen = false,
isStackable = false,
tabs = [],
activeTabID = '',
title,
icon,
onselect,
actions,
children
}: Props = $props();
const toggleCardOpen = () => {
if (isClosable) {
isOpen = !isOpen;
}
};
let isTabsShown = $derived(isOpen && tabs.length > 0);
</script> </script>
<div class="card m-2 flex flex-grow flex-col overflow-hidden rounded shadow-2xl"> <div
class={[
'card flex h-fit flex-col overflow-hidden rounded-2xl border-2 border-muted',
isOpen && 'isOpen flex-grow',
isStackable ? 'flex-1 group-has-[.isOpen]:w-full group-has-[.isOpen]:flex-none' : 'w-full'
]}>
<div <div
role="toolbar" role="toolbar"
tabindex="0" tabindex="0"
class="bg-primary p-2 {isTabsShown ? 'pb-0' : ''} flex-none cursor-pointer" class={[
on:click={() => (isOpen = !isOpen)} 'flex h-11 flex-none cursor-pointer items-center justify-between bg-muted p-2 whitespace-nowrap',
on:keypress={() => (isOpen = !isOpen)}> isTabsShown && 'pb-1'
<div class="flex justify-between"> ]}
<Tabs on:select {tabs} bind:isOpen {title} {isCloseable} {activeTabID} /> onclick={toggleCardOpen}
<div class="flex items-center gap-x-4 {isTabsShown ? '-mt-2' : ''}"> onkeypress={toggleCardOpen}>
<slot name="actions" /> {#if icon || title}
</div> <span role="menubar" tabindex="0" class="flex w-fit items-center gap-3">
</div> {#if icon}
<icon.component class={icon.class} />
{/if}
{title}
</span>
{/if}
{#if isOpen && tabs && tabs.length > 0}
<Tabs {onselect} {tabs} {activeTabID} />
{/if}
{@render actions?.()}
{#if isOpen && isClosable}
<CollapseAllIcon />
{/if}
</div> </div>
{#if isOpen} {#if isOpen}
<div class="card-body flex-grow overflow-auto p-0 text-base-content" transition:slide> <div class="flex-grow overflow-x-auto" transition:slide={{ easing: quintOut }}>
<slot /> {@render children()}
</div> </div>
{/if} {/if}
</div> </div>
+38 -43
View File
@@ -1,56 +1,51 @@
<script lang="ts"> <script lang="ts">
import type { Tab, TabEvents } from '$lib/types'; import { Button } from '$/components/ui/button';
import { createEventDispatcher } from 'svelte'; import { Separator } from '$/components/ui/separator';
import type { Tab } from '$lib/types';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
export let isCloseable = true;
export let tabs: Tab[];
export let title: string;
export let isOpen = false;
export let activeTabID: string;
if (!activeTabID && tabs.length > 0) { let {
activeTabID = tabs[0].id; tabs,
} activeTabID,
const dispatch = createEventDispatcher<TabEvents>(); onselect
}: {
tabs: Tab[];
activeTabID: string;
onselect?: (tab: Tab) => void;
} = $props();
// Derive (don't mutate the prop) so the highlight tracks a bound activeTabID.
const effectiveTabID = $derived(activeTabID || tabs[0]?.id);
const toggleTabs = (tab: Tab) => { const toggleTabs = (tab: Tab) => {
activeTabID = tab.id; return (event: Event) => {
dispatch('select', tab); event.stopPropagation();
onselect?.(tab);
};
}; };
</script> </script>
<div class="flex cursor-default"> <div class="flex w-fit cursor-default items-center gap-2">
<span <ul class="flex gap-2 align-middle" transition:fade>
role="menubar" {#each tabs as tab, index (tab.id)}
tabindex="0" <Button
class="mr-2 font-semibold"
on:click|stopPropagation={() => (isOpen = !isOpen)}
on:keypress|stopPropagation={() => (isOpen = !isOpen)}>
{#if isCloseable}
<i class="fas fa-chevron-right icon" class:isOpen />
{/if}
{title}</span>
{#if isOpen && tabs}
<ul class="tabs" transition:fade>
{#each tabs as tab}
<div
role="tab" role="tab"
tabindex="0" variant="ghost"
class="tab tab-lifted {activeTabID === tab.id ? 'tab-active' : 'text-primary-content'}" class={[
on:click|stopPropagation={() => toggleTabs(tab)} 'px-2',
on:keypress|stopPropagation={() => toggleTabs(tab)}> effectiveTabID === tab.id && 'rounded-b-none border-b-2 border-b-primary-foreground/50'
<i class="mr-1 {tab.icon}" /> ]}
onclick={toggleTabs(tab)}
onkeypress={toggleTabs(tab)}>
<tab.icon />
{tab.title} {tab.title}
</Button>
{#if index < tabs.length - 1}
<div class="my-2">
<Separator orientation="vertical" class="w-0.5 bg-slate-300" />
</div> </div>
{/if}
{/each} {/each}
</ul> </ul>
{/if}
</div> </div>
<style>
.icon {
transition-duration: 0.5s;
}
.isOpen {
transform: rotate(90deg);
}
</style>
-23
View File
@@ -1,23 +0,0 @@
import { cleanup, render } from '@testing-library/svelte';
import { describe, expect, it, afterEach } from 'vitest';
import Card from './Card.svelte';
describe('card.svelte', () => {
// TODO: @testing-library/svelte claims to add this automatically but it doesn't work without explicit afterEach
afterEach(() => cleanup());
it('mounts', () => {
const { container } = render(Card, {
title: 'TabTest',
tabs: [
{ id: 't1', title: 'title1', icon: 'fab fa-git-alt' },
{ id: 't2', title: 'title2', icon: 'far fa-bookmark' }
]
});
expect(container).toBeTruthy();
expect(container).toHaveTextContent('TabTest');
expect(container).toHaveTextContent('title1');
expect(container).toHaveTextContent('title2');
expect(container).not.toHaveTextContent('title3');
});
});
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import { notify } from '$/util/notify';
import { scale } from 'svelte/transition';
import CheckIcon from '~icons/material-symbols/check-rounded';
import CopyIcon from '~icons/material-symbols/content-copy-outline-rounded';
let {
onclick,
label = 'Copy'
}: { onclick: (event?: Event) => Promise<unknown>; label?: string } = $props();
let showCheckIcon = $state(false);
</script>
<Button
onclick={async (event) => {
try {
showCheckIcon = true;
setTimeout(() => {
showCheckIcon = false;
}, 1000);
await onclick(event);
} catch {
notify('Failed to copy');
}
}}>
<div class="grid">
{#key showCheckIcon}
<span transition:scale class="col-start-1 row-start-1">
{#if showCheckIcon}
<CheckIcon />
{:else}
<CopyIcon />
{/if}
</span>
{/key}
</div>
{label}
</Button>
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import CopyButton from '$/components/CopyButton.svelte';
import { Input } from '$/components/ui/input';
import type { InputType } from '$/types';
import { copyToClipboard } from '$/util/util';
let {
value,
label = 'Copy',
type = 'url',
testID
}: { value: string; label?: string; type?: InputType; testID?: string } = $props();
</script>
<div class="flex w-full items-center gap-2">
<Input
{type}
{value}
data-testid={testID}
onclick={(event) => {
event.currentTarget.setSelectionRange(0, event.currentTarget.value.length);
}} />
<CopyButton onclick={() => copyToClipboard(value)} {label} />
</div>
+273
View File
@@ -0,0 +1,273 @@
<script lang="ts">
import type { EditorProps } from '$/types';
import { env } from '$/util/env';
import { urls, validatedState } from '$/util/state.svelte';
import { logMermaidChartClick } from '$/util/stats';
import { AIPromptViewZoneManager } from '$lib/util/AIPromptViewZoneManager';
import { initEditor } from '$lib/util/monacoExtra';
import { errorDebug } from '$lib/util/util';
import { mode } from 'mode-watcher';
import * as monaco from 'monaco-editor';
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { onMount } from 'svelte';
import AIPromptPopup from './AIPromptPopup.svelte';
const { onUpdate }: EditorProps = $props();
let divElement: HTMLDivElement | undefined = $state();
let aiPromptPopupElement: HTMLDivElement | undefined = $state();
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let editorOptions = {
minimap: {
enabled: false
},
overviewRulerLanes: 0,
glyphMargin: true,
lineNumbersMinChars: 4
} satisfies monaco.editor.IStandaloneEditorConstructionOptions;
let currentText = '';
let isUpdatingFromState = false;
let showPopup = $state(false);
let popupPosition = $state({ top: 0, lineNumber: 0 });
let decorationsCollection: monaco.editor.IEditorDecorationsCollection | undefined;
let input = $state('');
let lastMouseLine = 0;
const aiPromptManager = new AIPromptViewZoneManager();
const applyEditorTheme = (currentMode: typeof mode.current) => {
if (!editor) return;
monaco.editor.setTheme(`mermaid${currentMode === 'dark' ? '-dark' : ''}`);
divElement?.classList.toggle('mermaid-dark', currentMode === 'dark');
};
$effect(() => {
applyEditorTheme(mode.current);
});
const jsonModel = monaco.editor.createModel(
'',
'json',
monaco.Uri.parse('internal://config.json')
);
const mermaidModel = monaco.editor.createModel(
'',
'mermaid',
monaco.Uri.parse('internal://mermaid.mmd')
);
const renderAIPromptGutterGlyphIcon = () => {
decorationsCollection?.clear();
if (!editor || showPopup) {
return;
}
const model = editor.getModel();
if (!model) {
return;
}
if (lastMouseLine > 0 && model.id === mermaidModel.id) {
decorationsCollection?.set([
{
range: new monaco.Range(lastMouseLine, 1, lastMouseLine, 1),
options: {
glyphMarginClassName: 'suggestion-icon'
}
}
]);
}
};
const closePopup = () => {
showPopup = false;
input = '';
aiPromptManager.hide();
renderAIPromptGutterGlyphIcon();
};
const toggleAIPopup = (lineNumber: number) => {
if (!divElement || !aiPromptPopupElement) return;
popupPosition = {
top: 0,
lineNumber
};
showPopup = !showPopup;
if (showPopup) {
aiPromptManager.show(popupPosition.lineNumber, aiPromptPopupElement, 100);
editor?.setSelection(new monaco.Range(0, 0, 0, 0));
} else {
aiPromptManager.hide();
}
renderAIPromptGutterGlyphIcon();
};
onMount(() => {
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new monacoJsonWorker();
}
return new monacoEditorWorker();
}
};
if (!divElement) {
throw new Error('divEl is undefined');
}
monaco.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
schemas: [
{
fileMatch: ['config.json'],
uri: `${env.docsUrl}/schemas/config.schema.json`
}
]
});
initEditor(monaco);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
aiPromptManager.setEditor(editor);
decorationsCollection = editor.createDecorationsCollection([]);
editor.onMouseDown((e) => {
const isGutter = e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN;
if (isGutter && e.target.position?.lineNumber === lastMouseLine && lastMouseLine > 0) {
e.event.preventDefault();
e.event.stopPropagation();
toggleAIPopup(e.target.position.lineNumber);
}
});
editor.onDidChangeModelContent(({ isFlush }) => {
const newText = editor?.getValue();
if (!newText || currentText === newText || isFlush || isUpdatingFromState) {
return;
}
currentText = newText;
onUpdate(currentText);
});
editor.onMouseMove((e) => {
if (!editor) return;
if (showPopup) return;
if (editor.getModel()?.id !== mermaidModel.id) return;
lastMouseLine = e.target.position?.lineNumber ?? 0;
renderAIPromptGutterGlyphIcon();
});
editor.onMouseLeave(() => {
lastMouseLine = 0;
renderAIPromptGutterGlyphIcon();
});
applyEditorTheme(mode.current);
const resizeObserver = new ResizeObserver((entries) => {
editor?.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
if (divElement.parentElement) {
resizeObserver.observe(divElement);
}
renderAIPromptGutterGlyphIcon();
return () => {
resizeObserver.disconnect();
jsonModel.dispose();
mermaidModel.dispose();
aiPromptManager.destroy();
editor?.dispose();
};
});
$effect(() => {
const { errorMarkers, editorMode, code, mermaid } = validatedState.current;
if (!editor) {
return;
}
const model = editorMode === 'code' ? mermaidModel : jsonModel;
if (editor.getModel()?.id !== model.id) {
editor.setModel(model);
renderAIPromptGutterGlyphIcon();
}
// Clear decorations if not in 'code' mode, or if the model changes
if (editorMode !== 'code' || editor.getModel()?.id !== mermaidModel.id) {
decorationsCollection?.clear();
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
if (newText !== currentText) {
isUpdatingFromState = true;
try {
editor.setScrollTop(0);
editor.pushUndoStop();
editor.executeEdits('updateCode', [
{
range: model.getFullModelRange(),
text: newText
}
]);
editor.pushUndoStop();
currentText = newText;
} finally {
isUpdatingFromState = false;
}
renderAIPromptGutterGlyphIcon();
}
// Display/clear errors
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
</script>
<div class="relative h-full grow overflow-hidden">
<div bind:this={divElement} id="editor" class="h-full w-full"></div>
<div bind:this={aiPromptPopupElement}>
<AIPromptPopup
show={showPopup}
bind:input
onHeightChange={(height) => aiPromptManager.updateHeight(height)}
onClose={closePopup}
onTryFree={() => {
logMermaidChartClick('vibeDiagramming');
window.open(
urls.current.mermaidChart({ medium: 'vibe_diagramming' }).save,
'_blank',
'noopener'
);
closePopup();
}} />
</div>
</div>
<style>
:global(.suggestion-icon) {
background-color: #e8eaf9;
width: 20px !important;
height: 20px !important;
margin-left: 4px;
background-image: url('/icons/use-chat.svg');
background-size: 16px 16px;
background-repeat: no-repeat;
background-position: center;
border-radius: 4px;
cursor: pointer;
}
:global(#editor.mermaid-dark .suggestion-icon) {
background-color: #2e4d6b;
background-image: url('/icons/use-chat-dark.svg');
}
</style>
@@ -0,0 +1,116 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import { TID } from '$/constants';
import type { DocumentationConfig } from '$/types';
import { env } from '$/util/env';
import { standardizeDiagramType } from '$/util/mermaid';
import { validatedState } from '$/util/state.svelte';
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
const docURLBase = env.docsUrl;
const docMap = {
architecture: {
code: '/syntax/architecture.html'
},
block: {
code: '/syntax/block.html'
},
c4: {
code: '/syntax/c4.html'
},
class: {
code: '/syntax/classDiagram.html',
config: '/syntax/classDiagram.html#configuration'
},
er: {
code: '/syntax/entityRelationshipDiagram.html',
config: '/syntax/entityRelationshipDiagram.html#styling'
},
flowchart: {
code: '/syntax/flowchart.html',
config: '/syntax/flowchart.html#configuration'
},
gantt: {
code: '/syntax/gantt.html',
config: '/syntax/gantt.html#configuration'
},
gitGraph: {
code: '/syntax/gitgraph.html',
config: '/syntax/gitgraph.html#gitgraph-specific-configuration-options'
},
journey: {
code: '/syntax/userJourney.html'
},
kanban: {
code: '/syntax/kanban.html',
config: '/syntax/kanban.html#configuration-options'
},
mindmap: {
code: '/syntax/mindmap.html'
},
packet: {
code: '/syntax/packet.html',
config: '/config/schema-docs/config-defs-packet-diagram-config.html'
},
pie: {
code: '/syntax/pie.html',
config: '/syntax/pie.html#configuration'
},
quadrantChart: {
code: '/syntax/quadrantChart.html',
config: '/syntax/quadrantChart.html#chart-configurations'
},
requirement: {
code: '/syntax/requirementDiagram.html'
},
sankey: {
code: '/syntax/sankey.html',
config: '/syntax/sankey.html#configuration'
},
sequence: {
code: '/syntax/sequenceDiagram.html',
config: '/syntax/sequenceDiagram.html#configuration'
},
stateDiagram: {
code: '/syntax/stateDiagram.html'
},
timeline: {
code: '/syntax/timeline.html',
config: '/syntax/timeline.html#themes'
},
treemap: {
code: '/syntax/treemap.html',
config: '/syntax/treemap.html#configuration-options'
},
xychart: {
code: '/syntax/xyChart.html',
config: '/syntax/xyChart.html#chart-configurations'
},
zenuml: {
code: '/syntax/zenuml.html'
}
} as const satisfies DocumentationConfig;
const doc = $derived.by(() => {
const { editorMode, diagramType } = validatedState.current;
if (!diagramType) {
return { key: '', url: docURLBase };
}
const key = standardizeDiagramType(diagramType);
const docConfig: { code: string; config?: string } = docMap[key as keyof typeof docMap] ?? {
code: ''
};
const url = docURLBase + (docConfig[editorMode] ?? docConfig.code ?? '');
return { key, url };
});
</script>
<Button
variant="ghost"
data-testid={TID.diagramDocumentationButton}
href={doc.url}
target="_blank"
title="View documentation for {doc.key.replace('Diagram', '')} diagram">
<BookIcon />
Docs
</Button>
+38
View File
@@ -0,0 +1,38 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import * as Popover from '$/components/ui/popover';
import type { Component } from 'svelte';
interface Props {
links: { title: string; href: string }[];
icon?: Component;
class?: string;
}
let props: Props = $props();
</script>
<Popover.Root>
<Popover.Trigger class="flex items-center gap-0">
<Button variant="ghost" size="sm">
<props.icon class={props.class} />
</Button>
</Popover.Trigger>
<Popover.Content>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul tabindex="0" class="flex flex-col">
{#each props.links as { href, title } (title)}
<li class="rounded-md p-2 hover:bg-muted">
<a
role="menuitem"
tabindex="0"
class="whitespace-nowrap underline"
target="_blank"
{href}>
{title}
</a>
</li>
{/each}
</ul>
</Popover.Content>
</Popover.Root>
+66 -116
View File
@@ -1,133 +1,83 @@
<script lang="ts" context="module">
declare global {
interface Window {
Cypress: boolean;
editorLoaded: boolean;
}
}
</script>
<script lang="ts"> <script lang="ts">
import type { EditorMode } from '$lib/types'; import DesktopEditor from '$/components/DesktopEditor.svelte';
import { stateStore, updateCode, updateConfig } from '$lib/util/state'; import McWrapper from '$/components/McWrapper.svelte';
import { themeStore } from '$lib/util/theme'; import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
import { errorDebug, syncDiagram } from '$lib/util/util'; import MobileEditor from '$/components/MobileEditor.svelte';
import * as monaco from 'monaco-editor'; import { Button } from '$/components/ui/button';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import { TID } from '$/constants';
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import { env } from '$/util/env';
import { onMount } from 'svelte'; import { updateCode, updateConfig, urls, validatedState } from '$lib/util/state.svelte';
import { initEditor } from '$lib/util/monacoExtra'; import { logMermaidChartClick } from '$lib/util/stats';
import { logEvent } from '$lib/util/stats'; import { debounce } from 'lodash-es';
import ExclamationCircleIcon from '~icons/material-symbols/error-outline-rounded';
let divElement: HTMLDivElement | undefined; const { isMobile } = $props<{ isMobile: boolean }>();
let editor: monaco.editor.IStandaloneCodeEditor | undefined; const onUpdate = (text: string) => {
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { if (validatedState.current.editorMode === 'code') {
minimap: {
enabled: false
},
theme: 'mermaid',
overviewRulerLanes: 0
};
let text = '';
stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
// console.log('editor store subscription', { code, mermaid });
if (!editor) {
return;
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
if (newText !== text) {
// console.log('updating editor text', newText);
editor.setScrollTop(0);
editor.setValue(newText);
text = newText;
}
// Update editor mode if it's different
const language = editorMode === 'code' ? 'mermaid' : 'json';
const model = editor.getModel();
if (!model) {
console.error("editor model doesn't exist");
return;
}
if (model.getLanguageId() !== language) {
monaco.editor.setModelLanguage(model, language);
}
// Display/clear errors
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
themeStore.subscribe(({ isDark }) => {
editor && monaco.editor.setTheme(isDark ? 'mermaid-dark' : 'mermaid');
});
const handleUpdate = (text: string, mode: EditorMode) => {
// console.log('editor HandleUpdate', { text, mode });
if (mode === 'code') {
updateCode(text); updateCode(text);
} else { } else {
updateConfig(text); updateConfig(text);
} }
}; };
onMount(() => { let showError = $state(false);
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new monacoJsonWorker();
}
return new monacoEditorWorker();
}
};
if (!divElement) { const showErrorDebounced = debounce(() => {
throw new Error('divEl is undefined'); showError = true;
} }, 3000);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
initEditor(monaco);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
editor.onDidChangeModelContent(({ isFlush }) => {
const newText = editor?.getValue();
if (!newText || text === newText || isFlush) {
return;
}
text = newText;
handleUpdate(text, $stateStore.editorMode);
});
editor.addAction({
id: 'mermaid-render-diagram',
label: 'Render Diagram',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
run: function () {
syncDiagram();
logEvent('renderDiagram', {
method: 'keyboardShortcut'
});
}
});
monaco.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid');
const resizeObserver = new ResizeObserver((entries) => {
editor?.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
if (divElement.parentElement) { $effect(() => {
resizeObserver.observe(divElement.parentElement); if (validatedState.current.error) {
showErrorDebounced();
} else {
showErrorDebounced.cancel();
showError = false;
} }
if (window.Cypress) {
window.editorLoaded = true;
}
return () => { return () => {
editor?.dispose(); showErrorDebounced.cancel();
}; };
}); });
</script> </script>
<div bind:this={divElement} id="editor" class="overflow-hidden" /> <div class="flex h-full flex-col">
{#if isMobile}
<MobileEditor {onUpdate} />
{:else}
<DesktopEditor {onUpdate} />
{/if}
{#if showError && validatedState.current.error instanceof Error}
<div class="flex flex-col text-sm" data-testid={TID.errorContainer}>
<div class="flex items-center justify-between gap-2 bg-slate-900 p-2 text-white">
<div class="flex w-fit items-center gap-2">
<ExclamationCircleIcon class="size-6 text-destructive" aria-hidden="true" />
<div class="flex flex-col">
<p>Syntax error</p>
{#if env.isEnabledMermaidChartLinks && validatedState.current.editorMode === 'code'}
<p class="text-xs text-white/60" data-testid={TID.aiHelpText}>
Create a free account to repair with AI
</p>
{/if}
</div>
</div>
{#if validatedState.current.editorMode === 'code'}
<McWrapper>
<Button
variant="accent"
size="sm"
data-testid={TID.aiRepairButton}
href={urls.current.mermaidChart({ medium: 'ai_repair' }).save}
target="_blank"
onclick={() => logMermaidChartClick('aiRepair')}>
<MermaidChartIcon />
AI Repair
</Button>
</McWrapper>
{/if}
</div>
<output class="max-h-32 overflow-auto bg-muted p-2" name="mermaid-error" for="editor">
<pre>{validatedState.current.error?.toString()}</pre>
</output>
</div>
{/if}
</div>
@@ -0,0 +1,119 @@
<script lang="ts">
import McWrapper from '$/components/McWrapper.svelte';
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
import { Button } from '$/components/ui/button';
import { standardizeDiagramType } from '$/util/mermaid';
import { validatedState, urls } from '$/util/state.svelte';
import { logMermaidChartClick } from '$/util/stats';
import { quintInOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
const visualEditDiagramTypes = new Set([
'flowchart',
'stateDiagram',
'classDiagram',
'sequenceDiagram',
'er',
'requirement',
'mindmap'
]);
const diagramType = $derived.by(() => {
const dt = validatedState.current.diagramType;
return dt ? standardizeDiagramType(dt) : undefined;
});
const showVisualEdit = $derived.by(() => {
return diagramType ? visualEditDiagramTypes.has(diagramType) : false;
});
interface EnhancedEditAction {
campaign: string;
label: string;
medium: 'ai_edit' | 'visual_edit' | 'voice_edit';
source: string;
}
const cycleIntervalMs = 30_000;
let currentActionIndex = $state(0);
const availableActions = $derived.by<EnhancedEditAction[]>(() => {
if (!validatedState.current.diagramType) {
return [];
}
const actions: EnhancedEditAction[] = [
{ campaign: 'voice_1', label: 'with voice', medium: 'voice_edit', source: 'voiceEdit' },
{ campaign: 'ai_1', label: 'with AI', medium: 'ai_edit', source: 'aiEdit' }
];
if (showVisualEdit) {
actions.unshift({
campaign: 'visual_1',
label: 'visually',
medium: 'visual_edit',
source: 'visualEdit'
});
}
return actions;
});
const currentAction = $derived.by(() => {
const actions = availableActions;
if (actions.length === 0) {
return undefined;
}
return actions[currentActionIndex % actions.length];
});
$effect(() => {
const actionCount = availableActions.length;
if (actionCount === 0) {
currentActionIndex = 0;
return;
}
if (currentActionIndex >= actionCount) {
currentActionIndex = 0;
}
if (actionCount <= 1) {
return;
}
const intervalID = setInterval(() => {
currentActionIndex = (currentActionIndex + 1) % actionCount;
}, cycleIntervalMs);
return () => clearInterval(intervalID);
});
</script>
{#if currentAction}
<McWrapper>
<Button
variant="secondary"
size="sm"
href={urls.current.mermaidChart({
medium: currentAction.medium,
campaign: currentAction.campaign
}).save}
target="_blank"
onclick={() => logMermaidChartClick(currentAction.source)}>
<MermaidChartIcon />
Edit
{#key currentAction.label}
<span
class="-ml-1"
in:slide={{ axis: 'x', easing: quintInOut, delay: 400 }}
out:slide={{ axis: 'x', easing: quintInOut }}>
{currentAction.label}
</span>
{/key}
</Button>
</McWrapper>
{/if}
@@ -0,0 +1,63 @@
<script lang="ts">
import { validatedState } from '$/util/state.svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
import type { ComponentProps, Snippet } from 'svelte';
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
let {
children,
domain,
shouldCheckDiagramType = true,
side = 'bottom',
labelPrefix = 'Opens your diagram in',
isVisible = true,
sharesData = true,
showPopup = true
}: {
children: Snippet;
domain: string;
shouldCheckDiagramType?: boolean;
side?: ComponentProps<typeof Tooltip.Content>['side'];
labelPrefix?: string;
isVisible?: boolean;
sharesData?: boolean;
showPopup?: boolean;
} = $props();
let shouldDisableComponent = $derived(
shouldCheckDiagramType && validatedState.current.diagramType === 'zenuml'
);
</script>
{#if isVisible}
<Tooltip.Provider>
<Tooltip.Root delayDuration={100}>
<Tooltip.Trigger>
<div class={[shouldDisableComponent && 'pointer-events-none cursor-not-allowed grayscale']}>
{@render children()}
</div>
</Tooltip.Trigger>
{#if showPopup}
<Tooltip.Content {side} class="bg-secondary shadow-xl">
<div
class="flex cursor-help items-center gap-2"
title={sharesData
? 'Your diagram will be sent to the external service'
: 'Your diagram is not shared'}>
{#if shouldDisableComponent}
<div class="text-muted-foreground">
This diagram type is not supported in {domain}
</div>
{:else}
<ExternalLinkIcon />
<span class="flex items-center gap-1">
{labelPrefix}
<div class="text-accent">{domain}</div>
</span>
{/if}
</div>
</Tooltip.Content>
{/if}
</Tooltip.Root>
</Tooltip.Provider>
{/if}
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="flex h-12 items-center justify-between gap-2 rounded-2xl bg-muted p-3">
{@render children()}
</div>
+53
View File
@@ -0,0 +1,53 @@
<!--
@component
Exports a `waitForFontAwesomeToLoad` function that waits for FontAwesome to load.
Usage:
```svelte
<script lang="ts">
import FontAwesome from '$lib/client/components/FontAwesome';
let waitForFontAwesomeToLoad: FontAwesome["waitForFontAwesomeToLoad"];
</script>
<FontAwesome bind:waitForFontAwesomeToLoad />
```
-->
<script context="module" lang="ts">
/**
* Returns `true` if the code may contain FontAwesome icons, and we should
* wait for the fonts to load before rendering.
*/
export function mayContainFontAwesome(code: string) {
// taken from https://github.com/mermaid-js/mermaid/blob/7043892e871d0c413ec63dc1570a8ef738d15568/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js#L63
// Not ideal, since we're looking at unparsed code.
const regex = /fa[blrs]?:fa-[\w-]+/g;
return regex.test(code);
}
</script>
<script lang="ts">
// Vite will automatically take care of adding this to our `<head>`
let lazyLoadFontAwesomeCSS = import('./FontAwesomeCSS.svelte');
let fontsLoaded = (async () => {
await lazyLoadFontAwesomeCSS;
// Once the stylesheet has been parsed, the fonts will be in document.fonts
// TODO: Maybe we should scan the CSS style sheet only, and only load FontAwesome fonts
await Promise.allSettled(Array.from(document.fonts, (font) => font.load()));
})();
/**
* Wait for FontAwesome to load.
*
* @returns A promise that resolves when FontAwesome is
* loaded, or there was an error that we ignore.
*/
export async function waitForFontAwesomeToLoad() {
return await fontsLoaded;
}
</script>
{#await lazyLoadFontAwesomeCSS then FontAwesomeCSS}
<FontAwesomeCSS.default />
{/await}
+14
View File
@@ -0,0 +1,14 @@
<!--
@component
Imports the FontAwesome CSS file.
Needed, since the `@sveltejs/adapter-node` plugin doesn't seem to support
lazy-importing CSS, even though `@sveltejs/adapter-vercel` is fine with it!
However, making a `.svelte` file that imports the CSS file, and then
lazy-loading the `.svelte` file works.
-->
<script lang="ts">
import '@fortawesome/fontawesome-free/css/all.css';
</script>
+136 -117
View File
@@ -1,45 +1,65 @@
<script lang="ts"> <script lang="ts">
import Card from '$lib/components/Card/Card.svelte'; import Card from '$lib/components/Card/Card.svelte';
import { inputStateStore, getStateString } from '$lib/util/state'; import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types';
import {
addHistoryEntry,
historyModeStore,
clearHistoryData,
getPreviousState,
historyStore,
loaderHistoryStore,
restoreHistory
} from './history';
import { notify, prompt } from '$lib/util/notify'; import { notify, prompt } from '$lib/util/notify';
import { onMount } from 'svelte'; import { serializeState } from '$lib/util/serde';
import { get } from 'svelte/store'; import { inputState, replaceInputState } from '$lib/util/state.svelte';
import { logEvent } from '$lib/util/stats';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dayjsRelativeTime from 'dayjs/plugin/relativeTime'; import dayjsRelativeTime from 'dayjs/plugin/relativeTime';
import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types'; import BookmarkIcon from '~icons/material-symbols/bookmark-outline-rounded';
import { logEvent } from '$lib/util/stats'; import TrashAltIcon from '~icons/material-symbols/delete-outline-rounded';
import DownloadIcon from '~icons/material-symbols/download-rounded';
import SaveIcon from '~icons/material-symbols/save-outline-rounded';
import UndoIcon from '~icons/material-symbols/settings-backup-restore-rounded';
import UploadIcon from '~icons/material-symbols/upload-rounded';
import HistoryIcon from '~icons/mdi/clock-outline';
import GitAltIcon from '~icons/mdi/git';
import OpenInNewIcon from '~icons/material-symbols/open-in-new-rounded';
import { Button } from '../ui/button';
import { Separator } from '../ui/separator';
import {
addManualEntry,
clearActive,
historyState,
removeEntry,
restoreEntries,
setMode
} from './historyState.svelte';
dayjs.extend(dayjsRelativeTime); dayjs.extend(dayjsRelativeTime);
const HISTORY_SAVE_INTERVAL = 60_000; const baseTabs: Tab[] = [
{ id: 'manual', title: 'Saved', icon: BookmarkIcon },
const tabSelectHandler = (message: CustomEvent<Tab>) => { { id: 'auto', title: 'Timeline', icon: HistoryIcon }
historyModeStore.set(message.detail.id as HistoryType);
};
let tabs: Tab[] = [
{
id: 'manual',
title: 'Saved',
icon: 'far fa-bookmark'
},
{
id: 'auto',
title: 'Timeline',
icon: 'fas fa-history'
}
]; ];
const loaderTab: Tab = { id: 'loader', title: 'Revisions', icon: GitAltIcon };
const tabs = $derived(
historyState.loaderEntries.length > 0 ? [loaderTab, ...baseTabs] : baseTabs
);
// Surface revisions once when they first appear; the user can switch away after.
let revisionsShown = false;
$effect(() => {
if (historyState.loaderEntries.length > 0 && !revisionsShown) {
revisionsShown = true;
setMode('loader');
}
});
const emptyMessage = $derived(
historyState.mode === 'auto'
? 'No timeline snapshots yet.\nThe Timeline is saved automatically every minute.'
: 'No saved states yet.\nClick the Save button to bookmark the current diagram and restore it later.'
);
const tabSelectHandler = (tab: Tab) => {
setMode(tab.id as HistoryType);
};
const downloadHistory = () => { const downloadHistory = () => {
const data = get(historyStore); const data = historyState.entries;
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
@@ -47,9 +67,7 @@
a.download = `mermaid-history-${dayjs().format('YYYY-MM-DD-HHmmss')}.json`; a.download = `mermaid-history-${dayjs().format('YYYY-MM-DD-HHmmss')}.json`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
logEvent('history', { logEvent('history', { action: 'download' });
action: 'download'
});
}; };
const uploadHistory = () => { const uploadHistory = () => {
@@ -62,100 +80,79 @@
return; return;
} }
const data: HistoryEntry[] = JSON.parse(await file.text()); const data: HistoryEntry[] = JSON.parse(await file.text());
restoreHistory(data); const { restored, invalid, duplicates } = restoreEntries(data);
notify(`${restored} restored, ${duplicates} duplicate, ${invalid} invalid.`);
}); });
input.click(); input.click();
}; };
const saveHistory = (auto = false) => { const saveHistory = () => {
const currentState: string = getStateString(); if (!addManualEntry($state.snapshot(inputState))) {
const previousState: string = getPreviousState(auto);
if (previousState !== currentState) {
addHistoryEntry({
state: $inputStateStore,
time: Date.now(),
type: auto ? 'auto' : 'manual'
});
} else if (!auto) {
notify('State already saved.'); notify('State already saved.');
} }
}; };
const clearHistory = (id?: string): void => { const clearAll = () => {
if (!id && !prompt('Clear all saved items?')) { if (prompt('Clear all saved items?')) {
return; clearActive();
} }
clearHistoryData(id);
}; };
const restoreHistoryItem = (state: State): void => { const restoreHistoryItem = (state: State): void => {
inputStateStore.set({ ...state, updateDiagram: true }); replaceInputState({ ...state, updateDiagram: true });
}; };
const relativeTime = (time: number) => { // Absolute editor URL for an entry, so the link can be opened in a new tab or copied.
const t = new Date(time); const entryUrl = (state: State): string =>
return `${new Date(t).toLocaleString()} (${dayjs(t).fromNow()})`; `${window.location.origin}${window.location.pathname}#${serializeState(state)}`;
};
onMount(() => { // Serialize each entry's URL once per change rather than per row on every render.
historyModeStore.set('manual'); const entriesWithUrl = $derived(
setInterval(() => { historyState.entries.map((entry) => ({ ...entry, openUrl: entryUrl(entry.state) }))
saveHistory(true); );
}, HISTORY_SAVE_INTERVAL);
});
loaderHistoryStore.subscribe((entries) => {
if (entries.length > 0 && tabs.length === 2) {
tabs = [
{
id: 'loader',
title: 'Revisions',
icon: 'fab fa-git-alt'
},
...tabs
];
historyModeStore.set('loader');
}
});
let isOpen = false;
</script> </script>
<Card on:select={tabSelectHandler} bind:isOpen {tabs} title="History"> <Card onselect={tabSelectHandler} isOpen isClosable={false} {tabs} activeTabID={historyState.mode}>
<div slot="actions"> {#snippet actions()}
<button <div class="flex items-center gap-2">
<Button
size="icon"
variant="ghost"
id="uploadHistory" id="uploadHistory"
class="btn btn-secondary btn-xs w-12" onclick={uploadHistory}
on:click|stopPropagation={() => uploadHistory()} title="Upload history"><UploadIcon /></Button>
title="Upload history"><i class="fa fa-upload" /></button> {#if historyState.entries.length > 0}
{#if $historyStore.length > 0} <Button
<button
id="downloadHistory" id="downloadHistory"
class="btn btn-secondary btn-xs w-12" size="icon"
on:click|stopPropagation={() => downloadHistory()} variant="ghost"
title="Download history"><i class="fa fa-download" /></button> onclick={downloadHistory}
title="Download history"><DownloadIcon /></Button>
{/if} {/if}
| <Separator orientation="vertical" />
<button <Button
id="saveHistory" id="saveHistory"
class="btn btn-success btn-xs w-12" size="icon"
on:click|stopPropagation={() => saveHistory()} variant="ghost"
title="Save current state"><i class="far fa-save" /></button> onclick={saveHistory}
{#if $historyModeStore !== 'loader'} title="Save current state"><SaveIcon /></Button>
<button {#if historyState.mode !== 'loader'}
<Button
id="clearHistory" id="clearHistory"
class="btn btn-error btn-xs w-12" size="icon"
on:click|stopPropagation={() => clearHistory()} variant="ghost"
title="Delete all saved states"><i class="fas fa-trash-alt" /></button> class="hover:text-destructive"
onclick={clearAll}
title="Delete all saved states"><TrashAltIcon /></Button>
{/if} {/if}
</div> </div>
<ul class="h-56 space-y-2 overflow-auto p-2" id="historyList"> {/snippet}
{#if $historyStore.length > 0} <ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList">
{#each $historyStore as { id, state, time, name, url, type }} {#if entriesWithUrl.length > 0}
<li class="flex-col rounded p-2 shadow"> {#each entriesWithUrl as { id, state, time, name, url, type, openUrl } (id)}
<div class="flex"> <li class="flex flex-col gap-2">
<div class="flex-1"> <div class="flex items-center justify-between">
<div class="flex flex-col text-base-content"> <div class="flex flex-col">
{#if url} {#if url}
<a <a
href={url} href={url}
@@ -163,28 +160,50 @@
title="Open revision in new tab" title="Open revision in new tab"
class="text-blue-500 hover:underline">{name}</a> class="text-blue-500 hover:underline">{name}</a>
{:else} {:else}
<span>{name}</span> <span class="whitespace-nowrap">{name}</span>
{/if} {/if}
<span class="text-sm text-gray-400">{relativeTime(time)}</span> <span class="text-xs whitespace-nowrap text-primary-foreground/30">
{new Date(time).toLocaleString()}
</span>
</div> </div>
</div>
<div class="flex content-center gap-2"> <div class="flex items-center gap-2">
<button class="btn btn-success" on:click={() => restoreHistoryItem(state)} <span class="text-sm whitespace-nowrap text-primary-foreground/50">
><i class="fas fa-undo mr-1" />Restore</button> {dayjs(time).fromNow()}
</span>
<Button
href={openUrl}
target="_blank"
rel="noopener"
size="icon"
variant="ghost"
title="Open in new tab">
<OpenInNewIcon />
</Button>
<Button
size="icon"
variant="ghost"
title="Restore this version"
onclick={() => restoreHistoryItem(state)}>
<UndoIcon />
</Button>
{#if type !== 'loader'} {#if type !== 'loader'}
<button class="btn btn-error" on:click={() => clearHistory(id)} <Button
><i class="fas fa-trash-alt mr-1" />Delete</button> size="icon"
variant="ghost"
class="hover:text-destructive"
title="Delete this version"
onclick={() => removeEntry(id)}>
<TrashAltIcon />
</Button>
{/if} {/if}
</div> </div>
</div> </div>
<Separator />
</li> </li>
{/each} {/each}
{:else} {:else}
<div class="m-2"> <div class="m-2 text-center whitespace-pre-line">{emptyMessage}</div>
No items in History<br />
Click the Save button to save current state and restore it later.<br />
Timeline will automatically be saved every minute.
</div>
{/if} {/if}
</ul> </ul>
</Card> </Card>
-127
View File
@@ -1,127 +0,0 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import type { HistoryEntry } from '$lib/types';
import { describe, it, expect } from 'vitest';
import {
addHistoryEntry,
injectHistoryIDs,
clearHistoryData,
historyModeStore,
historyStore
} from './history';
import { defaultState } from '$lib/util/state';
import { get } from 'svelte/store';
describe('history', () => {
it('should handle saving individual history entry', () => {
expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]');
expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]');
addHistoryEntry({
state: defaultState,
time: 12_345,
type: 'manual'
});
const [manualEntry] = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualEntry.time).toBe(12_345);
expect(manualEntry.type).toBe('manual');
expect(manualEntry.name).not.toBeNull();
expect(manualEntry.state).not.toBeNull();
addHistoryEntry({
state: defaultState,
time: 54_321,
type: 'auto'
});
const [autoEntry] = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(autoEntry.time).toBe(54_321);
expect(autoEntry.type).toBe('auto');
expect(autoEntry.name).not.toBeNull();
expect(autoEntry.state).not.toBeNull();
historyModeStore.set('manual');
clearHistoryData();
historyModeStore.set('auto');
clearHistoryData();
expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]');
expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]');
});
it('should clear history entries', () => {
addHistoryEntry({
state: defaultState,
time: 12_345,
type: 'manual'
});
addHistoryEntry({
state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' },
time: 123_456,
type: 'manual'
});
historyModeStore.set('manual');
const store: HistoryEntry[] = get(historyStore);
expect(store.length).toBe(2);
clearHistoryData(store[1].id);
expect(get(historyStore).length).toBe(1);
clearHistoryData();
expect(get(historyStore).length).toBe(0);
historyModeStore.set('auto');
addHistoryEntry({
state: defaultState,
time: 54_321,
type: 'auto'
});
addHistoryEntry({
state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' },
time: 654_321,
type: 'auto'
});
expect(get(historyStore).length).toBe(2);
clearHistoryData();
expect(get(historyStore).length).toBe(0);
// Test calling when history is empty
clearHistoryData();
expect(get(historyStore).length).toBe(0);
});
});
describe('history migration', () => {
it('should inject history IDs as migration', () => {
window.localStorage.setItem(
'manualHistoryStore',
'[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"manual","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"helpful-ocean"}]'
);
window.localStorage.setItem(
'autoHistoryStore',
'[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"auto","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"needy-mosquito"}]'
);
let manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
let autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
injectHistoryIDs();
manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
});
});
-152
View File
@@ -1,152 +0,0 @@
import { derived, writable, get } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import { persist, localStorage } from '$lib/util/persist';
import { generateSlug } from 'random-word-slugs';
import type { HistoryEntry, HistoryType, Optional } from '$lib/types';
import { v4 as uuidV4 } from 'uuid';
import { logEvent } from '$lib/util/stats';
const MAX_AUTO_HISTORY_LENGTH = 30;
export const historyModeStore: Writable<HistoryType> = persist(
writable('manual'),
localStorage(),
'autoHistoryMode'
);
const autoHistoryStore: Writable<HistoryEntry[]> = persist(
writable([]),
localStorage(),
'autoHistoryStore'
);
const manualHistoryStore: Writable<HistoryEntry[]> = persist(
writable([]),
localStorage(),
'manualHistoryStore'
);
export const loaderHistoryStore: Writable<HistoryEntry[]> = writable([]);
export const historyStore: Readable<HistoryEntry[]> = derived(
[historyModeStore, autoHistoryStore, manualHistoryStore, loaderHistoryStore],
([historyMode, autoHistories, manualHistories, loadedHistories], set) => {
switch (historyMode) {
case 'auto': {
set(autoHistories);
break;
}
case 'manual': {
set(manualHistories);
break;
}
case 'loader': {
set(loadedHistories);
break;
}
default: {
set(autoHistories);
}
}
}
);
export const addHistoryEntry = (entryToAdd: Optional<HistoryEntry, 'id'>): void => {
const entry: HistoryEntry = {
...entryToAdd,
id: uuidV4()
};
if (entry.type === 'loader') {
loaderHistoryStore.update((entries) => [entry, ...entries]);
return;
}
if (!entry.name) {
entry.name = generateSlug(2);
}
if (entry.type === 'auto') {
autoHistoryStore.update((entries) => {
if (entries.length >= MAX_AUTO_HISTORY_LENGTH) {
entries = entries.slice(0, MAX_AUTO_HISTORY_LENGTH - 1);
}
return [entry, ...entries];
});
}
manualHistoryStore.update((entries) => [entry, ...entries]);
logEvent('history', { action: 'save' });
};
export const clearHistoryData = (idToClear?: string): void => {
(get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
if (get(historyModeStore) !== 'loader') {
entries = entries.filter(({ id }) => idToClear && id != idToClear);
logEvent('history', { action: 'clear', type: idToClear ? 'single' : 'all' });
}
return entries;
});
};
export const getPreviousState = (auto: boolean): string => {
const entries = get(auto ? autoHistoryStore : manualHistoryStore);
if (entries.length > 0) {
return JSON.stringify(entries[0].state);
}
return '';
};
export const restoreHistory = (data: HistoryEntry[]) => {
const entries = data.filter((element) => validateEntry(element));
const invalidEntryCount = data.length - entries.length;
if (invalidEntryCount > 0) {
console.error(`${invalidEntryCount} invalid history entries were removed.`);
console.error(data);
}
if (entries.length > 0) {
let entryCount = 0;
(entries[0].type === 'auto' ? autoHistoryStore : manualHistoryStore).update((existing) => {
const existingIDs = new Set(existing.map(({ id }) => id));
const newEntries = entries.filter(({ id }) => !existingIDs.has(id));
entryCount = newEntries.length;
const combined = [...existing, ...newEntries];
combined.sort((a, b) => b.time - a.time);
return combined;
});
alert(
`${entryCount} entries restored. ${invalidEntryCount} invalid, ${
entries.length - entryCount
} duplicates.`
);
logEvent('history', {
action: 'restore',
success: entryCount,
invalid: invalidEntryCount,
duplicates: entries.length - entryCount
});
} else {
alert('No valid entries found.');
}
};
const setIDs = (entries: HistoryEntry[]) => {
for (const entry of entries) {
if (!entry.id) {
entry.id = uuidV4();
}
}
return entries;
};
export const injectHistoryIDs = (): void => {
autoHistoryStore.update(setIDs);
manualHistoryStore.update(setIDs);
};
const validateEntry = (entry: HistoryEntry): boolean => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return entry.type && entry.state && entry.time && true;
};
@@ -0,0 +1,177 @@
import type { HistoryEntry, HistoryType, Optional, State } from '$lib/types';
import { persisted, readJSON, type Persisted } from '$lib/util/persist.svelte';
import { inputState } from '$lib/util/state.svelte';
import { logEvent } from '$lib/util/stats';
import { generateSlug } from 'random-word-slugs';
import { v4 as uuidV4 } from 'uuid';
const MAX_AUTO_HISTORY_LENGTH = 30;
const AUTO_SAVE_INTERVAL = 60_000;
const auto = persisted<HistoryEntry[]>('autoHistoryStore', []);
const manual = persisted<HistoryEntry[]>('manualHistoryStore', []);
const mode = persisted<HistoryType>('autoHistoryMode', 'manual');
let loader = $state<HistoryEntry[]>([]);
// Loader entries are in-memory, so a persisted 'loader' mode is empty after reload.
if (mode.value === 'loader') {
mode.value = 'manual';
}
// The persisted slot backing a mode; loader is in-memory and has no slot.
const slotFor = (m: HistoryType): Persisted<HistoryEntry[]> | null => {
switch (m) {
case 'auto': {
return auto;
}
case 'manual': {
return manual;
}
default: {
return null;
}
}
};
export const historyState = {
get entries(): HistoryEntry[] {
return slotFor(mode.value)?.value ?? loader;
},
get loaderEntries(): HistoryEntry[] {
return loader;
},
get mode(): HistoryType {
return mode.value;
}
};
export const setMode = (next: HistoryType): void => {
mode.value = next;
};
// Dedup key: only the fields that define the diagram, so volatile/view-only
// fields (renderCount, pan/zoom, …) don't count as a change.
export const stateKey = (state: State): string =>
JSON.stringify({ code: state.code, mermaid: state.mermaid });
const createEntry = (state: State, type: 'auto' | 'manual'): HistoryEntry => ({
id: uuidV4(),
name: generateSlug(2),
state,
time: Date.now(),
type
});
// Returns true if added, false if it duplicated the most recent entry.
const addEntry = (
slot: Persisted<HistoryEntry[]>,
state: State,
type: 'auto' | 'manual',
maxLength?: number
): boolean => {
const entries = slot.value;
if (entries.length > 0 && stateKey(entries[0].state) === stateKey(state)) {
return false;
}
const trimmed =
maxLength && entries.length >= maxLength ? entries.slice(0, maxLength - 1) : entries;
slot.value = [createEntry(state, type), ...trimmed];
logEvent('history', { action: 'save', type });
return true;
};
export const addManualEntry = (state: State): boolean => addEntry(manual, state, 'manual');
export const addAutoEntry = (state: State): boolean =>
addEntry(auto, state, 'auto', MAX_AUTO_HISTORY_LENGTH);
// Replaces the in-memory revisions (e.g. when a gist is loaded), assigning ids.
export const setLoaderEntries = (entries: Optional<HistoryEntry, 'id'>[]): void => {
loader = entries.map((entry) =>
entry.id ? (entry as HistoryEntry) : { ...entry, id: uuidV4() }
);
};
export const removeEntry = (id: string): void => {
const slot = slotFor(mode.value);
if (!slot) {
return;
}
slot.value = slot.value.filter((entry) => entry.id !== id);
logEvent('history', { action: 'clear', type: 'single' });
};
export const clearActive = (): void => {
const slot = slotFor(mode.value);
if (!slot) {
return;
}
slot.value = [];
logEvent('history', { action: 'clear', type: 'all' });
};
const validateEntry = (entry: HistoryEntry): boolean =>
Boolean(entry && entry.type && entry.state) && typeof entry.time === 'number';
export interface RestoreResult {
restored: number;
invalid: number;
duplicates: number;
}
// Routes each uploaded entry to the store matching its own type, skipping ids
// that already exist.
export const restoreEntries = (data: HistoryEntry[]): RestoreResult => {
const valid = data.filter((entry) => validateEntry(entry));
const invalid = data.length - valid.length;
let restored = 0;
const slots: [HistoryType, Persisted<HistoryEntry[]>][] = [
['auto', auto],
['manual', manual]
];
for (const [type, slot] of slots) {
const incoming = valid.filter((entry) => entry.type === type);
if (incoming.length === 0) {
continue;
}
const existingIDs = slot.value.map(({ id }) => id);
const fresh = incoming.filter(({ id }) => !existingIDs.includes(id));
restored += fresh.length;
slot.value = [...slot.value, ...fresh].sort((a, b) => b.time - a.time);
}
const duplicates = valid.length - restored;
logEvent('history', { action: 'restore', duplicates, invalid, success: restored });
return { restored, invalid, duplicates };
};
const setIDs = (entries: HistoryEntry[]): HistoryEntry[] =>
entries.map((entry) => (entry.id ? entry : { ...entry, id: uuidV4() }));
// One-time migration: re-reads localStorage so entries written by an older
// version get ids, then persists and updates the reactive state.
export const injectHistoryIDs = (): void => {
auto.value = setIDs(readJSON<HistoryEntry[]>('autoHistoryStore', []));
manual.value = setIDs(readJSON<HistoryEntry[]>('manualHistoryStore', []));
};
let autoSaveTimer: ReturnType<typeof setInterval> | undefined;
// Idempotent; returns the stop function for use as a lifecycle cleanup.
export const startAutoSave = (): (() => void) => {
if (autoSaveTimer === undefined) {
autoSaveTimer = setInterval(
() => addAutoEntry($state.snapshot(inputState)),
AUTO_SAVE_INTERVAL
);
}
return stopAutoSave;
};
export const stopAutoSave = (): void => {
if (autoSaveTimer !== undefined) {
clearInterval(autoSaveTimer);
autoSaveTimer = undefined;
}
};
@@ -0,0 +1,314 @@
import type { HistoryEntry } from '$lib/types';
import { defaultState, replaceInputState } from '$lib/util/state.svelte';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
addAutoEntry,
addManualEntry,
clearActive,
historyState,
injectHistoryIDs,
removeEntry,
restoreEntries,
setLoaderEntries,
setMode,
startAutoSave,
stateKey,
stopAutoSave
} from './historyState.svelte';
const codeState = (code: string) => ({ ...defaultState, code });
/** Read the entries currently shown for a given mode. */
const entriesFor = (mode: 'auto' | 'manual' | 'loader'): HistoryEntry[] => {
setMode(mode);
return historyState.entries;
};
beforeEach(() => {
// Reset every store through the public API so tests don't leak into each other.
setMode('manual');
clearActive();
setMode('auto');
clearActive();
setLoaderEntries([]);
setMode('manual');
});
describe('stateKey', () => {
it('ignores volatile and view-only fields, keying only on code + config', () => {
const a = {
...defaultState,
code: 'graph TD\n A-->B',
panZoom: true,
renderCount: 1,
updateDiagram: true
};
const b = {
...defaultState,
code: 'graph TD\n A-->B',
pan: { x: 5, y: 5 },
panZoom: false,
renderCount: 99,
updateDiagram: false
};
expect(stateKey(a)).toBe(stateKey(b));
});
it('differs when code differs', () => {
expect(stateKey(codeState('graph TD\n A-->B'))).not.toBe(
stateKey(codeState('graph TD\n A-->C'))
);
});
it('differs when config differs', () => {
const a = { ...defaultState, mermaid: '{"theme":"dark"}' };
const b = { ...defaultState, mermaid: '{"theme":"forest"}' };
expect(stateKey(a)).not.toBe(stateKey(b));
});
});
describe('addManualEntry', () => {
it('adds to the manual store only, never the auto store', () => {
expect(addManualEntry(codeState('graph TD\n A-->B'))).toBe(true);
expect(entriesFor('manual')).toHaveLength(1);
expect(entriesFor('auto')).toHaveLength(0);
});
it('returns false and does not add a duplicate of the latest entry', () => {
const state = codeState('graph TD\n A-->B');
expect(addManualEntry(state)).toBe(true);
expect(addManualEntry(state)).toBe(false);
expect(entriesFor('manual')).toHaveLength(1);
});
it('treats states differing only in volatile/view fields as duplicates', () => {
expect(addManualEntry({ ...defaultState, code: 'graph TD\n A-->B', renderCount: 1 })).toBe(
true
);
expect(
addManualEntry({
...defaultState,
code: 'graph TD\n A-->B',
panZoom: false,
renderCount: 2,
updateDiagram: true
})
).toBe(false);
expect(entriesFor('manual')).toHaveLength(1);
});
it('adds a new entry when the code changes', () => {
expect(addManualEntry(codeState('graph TD\n A-->B'))).toBe(true);
expect(addManualEntry(codeState('graph TD\n A-->C'))).toBe(true);
expect(entriesFor('manual')).toHaveLength(2);
});
it('generates an id and a name for each entry', () => {
addManualEntry(codeState('graph TD\n A-->B'));
const [entry] = entriesFor('manual');
expect(entry.id).toBeTruthy();
expect(entry.name).toBeTruthy();
expect(entry.type).toBe('manual');
});
});
describe('addAutoEntry', () => {
it('adds to the auto store only, never the manual store', () => {
expect(addAutoEntry(codeState('graph TD\n A-->B'))).toBe(true);
expect(entriesFor('auto')).toHaveLength(1);
expect(entriesFor('manual')).toHaveLength(0);
});
it('returns false and does not add a duplicate of the latest entry', () => {
const state = codeState('graph TD\n A-->B');
expect(addAutoEntry(state)).toBe(true);
expect(addAutoEntry(state)).toBe(false);
expect(entriesFor('auto')).toHaveLength(1);
});
it('caps the auto store at 30 entries, dropping the oldest', () => {
for (let i = 0; i < 35; i++) {
addAutoEntry(codeState(`graph TD\n A-->B${i}`));
}
const entries = entriesFor('auto');
expect(entries).toHaveLength(30);
expect(entries[0].state.code).toBe('graph TD\n A-->B34');
});
});
describe('historyState.entries', () => {
it('reflects the active mode', () => {
addManualEntry(codeState('manual-code'));
addAutoEntry(codeState('auto-code'));
setMode('manual');
expect(historyState.entries).toHaveLength(1);
expect(historyState.entries[0].state.code).toBe('manual-code');
setMode('auto');
expect(historyState.entries).toHaveLength(1);
expect(historyState.entries[0].state.code).toBe('auto-code');
setMode('loader');
expect(historyState.entries).toHaveLength(0);
});
});
describe('removeEntry / clearActive', () => {
it('removes a single entry from the active store by id', () => {
addManualEntry(codeState('graph TD\n A-->B'));
addManualEntry(codeState('graph TD\n A-->C'));
setMode('manual');
const target = historyState.entries[1].id;
removeEntry(target);
expect(historyState.entries).toHaveLength(1);
expect(historyState.entries.some((e) => e.id === target)).toBe(false);
});
it('clears all entries in the active store only', () => {
addManualEntry(codeState('graph TD\n A-->B'));
addAutoEntry(codeState('graph TD\n A-->C'));
setMode('manual');
clearActive();
expect(entriesFor('manual')).toHaveLength(0);
expect(entriesFor('auto')).toHaveLength(1);
});
it('does nothing in loader mode', () => {
setLoaderEntries([
{ name: 'rev', state: defaultState, time: 1, type: 'loader', url: 'http://x' }
]);
setMode('loader');
clearActive();
expect(historyState.entries).toHaveLength(1);
});
});
describe('setLoaderEntries', () => {
it('replaces the in-memory revisions and assigns ids', () => {
setLoaderEntries([
{ name: 'v1', state: defaultState, time: 1, type: 'loader', url: 'http://x/1' },
{ name: 'v2', state: defaultState, time: 2, type: 'loader', url: 'http://x/2' }
]);
setMode('loader');
expect(historyState.entries).toHaveLength(2);
expect(historyState.entries.every((e) => e.id)).toBe(true);
setLoaderEntries([
{ name: 'only', state: defaultState, time: 3, type: 'loader', url: 'http://x/3' }
]);
expect(historyState.entries).toHaveLength(1);
expect(historyState.entries[0].name).toBe('only');
});
});
describe('restoreEntries', () => {
it('routes each entry to the store matching its own type', () => {
const result = restoreEntries([
{ id: 'a1', name: 'a', state: defaultState, time: 10, type: 'auto' },
{ id: 'm1', name: 'm', state: defaultState, time: 20, type: 'manual' }
]);
expect(result.restored).toBe(2);
expect(entriesFor('auto').map((e) => e.id)).toEqual(['a1']);
expect(entriesFor('manual').map((e) => e.id)).toEqual(['m1']);
});
it('skips duplicates by id and reports them', () => {
restoreEntries([{ id: 'm1', name: 'm', state: defaultState, time: 20, type: 'manual' }]);
const result = restoreEntries([
{ id: 'm1', name: 'm', state: defaultState, time: 20, type: 'manual' },
{ id: 'm2', name: 'm2', state: defaultState, time: 30, type: 'manual' }
]);
expect(result.restored).toBe(1);
expect(result.duplicates).toBe(1);
expect(entriesFor('manual')).toHaveLength(2);
});
it('reports invalid entries and does not restore them', () => {
const result = restoreEntries([
{ id: 'm1', name: 'm', state: defaultState, time: 20, type: 'manual' },
{ foo: 'bar' } as unknown as HistoryEntry
]);
expect(result.restored).toBe(1);
expect(result.invalid).toBe(1);
});
it('sorts restored entries newest first', () => {
restoreEntries([
{ id: 'm1', name: 'old', state: defaultState, time: 10, type: 'manual' },
{ id: 'm2', name: 'new', state: defaultState, time: 30, type: 'manual' },
{ id: 'm3', name: 'mid', state: defaultState, time: 20, type: 'manual' }
]);
expect(entriesFor('manual').map((e) => e.time)).toEqual([30, 20, 10]);
});
it('restores entries whose time is 0 (epoch) instead of treating them as invalid', () => {
const result = restoreEntries([
{ id: 'm0', name: 'epoch', state: defaultState, time: 0, type: 'manual' }
]);
expect(result.restored).toBe(1);
expect(result.invalid).toBe(0);
expect(entriesFor('manual')).toHaveLength(1);
});
});
describe('injectHistoryIDs migration', () => {
it('adds ids to persisted entries that lack them', () => {
window.localStorage.setItem(
'manualHistoryStore',
'[{"state":{"code":"a"},"time":1,"type":"manual","name":"x"}]'
);
window.localStorage.setItem(
'autoHistoryStore',
'[{"state":{"code":"b"},"time":2,"type":"auto","name":"y"}]'
);
injectHistoryIDs();
const manual = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
const auto = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manual).toHaveLength(1);
expect(auto).toHaveLength(1);
expect(manual.every(({ id }) => id !== undefined)).toBe(true);
expect(auto.every(({ id }) => id !== undefined)).toBe(true);
});
});
describe('auto-save lifecycle', () => {
afterEach(() => {
stopAutoSave();
vi.useRealTimers();
});
it('records an auto entry on each interval from the current editor state', () => {
vi.useFakeTimers();
replaceInputState(codeState('graph TD\n auto-saved'));
startAutoSave();
vi.advanceTimersByTime(60_000);
const entries = entriesFor('auto');
expect(entries).toHaveLength(1);
expect(entries[0].state.code).toBe('graph TD\n auto-saved');
});
it('is idempotent: calling startAutoSave twice does not double-record', () => {
vi.useFakeTimers();
replaceInputState(codeState('graph TD\n once'));
startAutoSave();
startAutoSave();
vi.advanceTimersByTime(60_000);
expect(entriesFor('auto')).toHaveLength(1);
});
it('stops recording after stopAutoSave', () => {
vi.useFakeTimers();
replaceInputState(codeState('graph TD\n stoppable'));
startAutoSave();
vi.advanceTimersByTime(60_000);
stopAutoSave();
replaceInputState(codeState('graph TD\n after-stop'));
vi.advanceTimersByTime(60_000);
expect(entriesFor('auto')).toHaveLength(1);
});
});
+143
View File
@@ -0,0 +1,143 @@
<script lang="ts">
import McWrapper from '$/components/McWrapper.svelte';
import * as Popover from '$/components/ui/popover';
import { Switch } from '$/components/ui/switch';
import { env } from '$/util/env';
import { urls } from '$/util/state.svelte';
import { logMermaidChartClick } from '$/util/stats';
import { cn } from '$/utils';
import { mode, setMode } from 'mode-watcher';
import type { Component, Snippet } from 'svelte';
import MermaidTailIcon from '~icons/custom/mermaid-tail';
import AddIcon from '~icons/material-symbols/add-2-rounded';
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
import DuplicateIcon from '~icons/material-symbols/content-copy-outline-rounded';
import ContrastIcon from '~icons/material-symbols/contrast';
import PluginIcon from '~icons/material-symbols/electrical-services-rounded';
import MenuIcon from '~icons/material-symbols/menu-rounded';
import CommunityIcon from '~icons/material-symbols/person-play-outline-rounded';
import PlaygroundIcon from '~icons/material-symbols/shape-line-outline';
import MermaidChartIcon from './MermaidChartIcon.svelte';
interface MenuItem {
label: string;
icon: Component;
href: string;
class?: string;
onclick?: () => void;
sharesData?: boolean;
checkDiagramType?: boolean;
isSectionEnd?: boolean;
renderer: Snippet<[Omit<MenuItem, 'renderer'>]>;
}
const menuItems: MenuItem[] = $derived([
{ label: 'New', icon: AddIcon, href: urls.current.new, renderer: menuItem },
{ label: 'Duplicate', icon: DuplicateIcon, href: window.location.href, renderer: menuItem },
{
href: urls.current.mermaidChart({ medium: 'main_menu' }).playground,
icon: PlaygroundIcon,
isSectionEnd: true,
label: 'Edit in Playground',
onclick: () => logMermaidChartClick('editInPlayground'),
renderer: mcMenuItem
},
{
label: 'Mermaid.js',
icon: MermaidTailIcon,
href: env.docsUrl,
renderer: menuItem
},
{
label: 'Documentation',
icon: BookIcon,
href: `${env.docsUrl}/intro/`,
renderer: menuItem
},
{
label: 'Community',
icon: CommunityIcon,
href: 'https://discord.gg/sKeNQX4Wtj',
renderer: menuItem
},
{
checkDiagramType: false,
href: urls.current.mermaidChart({ medium: 'main_menu' }).plugins,
icon: PluginIcon,
label: 'Plugins',
onclick: () => logMermaidChartClick('plugins'),
renderer: mcMenuItem,
sharesData: false
},
{
href: '#',
icon: ContrastIcon,
isSectionEnd: true,
label: 'Dark Mode',
renderer: darkModeMenuItem
},
{
checkDiagramType: false,
class: 'text-accent border-b-0',
href: urls.current.mermaidChart({ medium: 'main_menu' }).home,
icon: MermaidChartIcon,
label: 'Mermaid',
onclick: () => logMermaidChartClick('mermaidHome'),
renderer: mcMenuItem,
sharesData: false
}
]);
</script>
{#snippet menuItem(options: Omit<MenuItem, 'renderer'>)}
<a
href={options.href}
target="_blank"
onclick={options.onclick}
class={cn(
'flex items-center justify-start gap-2 border-b-2 p-2 px-3 hover:bg-muted',
options.isSectionEnd && 'border-border-dark',
options.class
)}>
<options.icon class="size-5" />
{options.label}
</a>
{/snippet}
{#snippet mcMenuItem(item: Omit<MenuItem, 'renderer'>)}
<McWrapper
side="right"
labelPrefix={item.sharesData === false ? 'Opens a new tab in' : undefined}
sharesData={item.sharesData}
shouldCheckDiagramType={item.checkDiagramType}>
{@render menuItem(item)}
</McWrapper>
{/snippet}
{#snippet darkModeMenuItem(options: Omit<MenuItem, 'renderer'>)}
<div
class={cn(
'flex cursor-pointer items-center justify-between border-b-2 px-3 py-2 hover:bg-muted',
options.isSectionEnd && 'border-border-dark',
options.class
)}>
<span class="flex items-center gap-2">
<ContrastIcon />
Dark Mode
</span>
<Switch
checked={mode.current === 'dark'}
onCheckedChange={(dark) => setMode(dark ? 'dark' : 'light')} />
</div>
{/snippet}
<Popover.Root>
<Popover.Trigger class="shrink-0">
<MenuIcon class="size-6" />
</Popover.Trigger>
<Popover.Content align="start" class="flex flex-col overflow-hidden border-2 p-0" sideOffset={16}>
{#each menuItems as { renderer, ...item } (item.label)}
{@render renderer(item)}
{/each}
</Popover.Content>
</Popover.Root>
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
import { env } from '$/util/env';
import { isOnMermaidAI } from '$/util/migration/domainMigration';
import type { ComponentProps } from 'svelte';
import ExternalLinkWrapper from './ExternalLinkWrapper.svelte';
let {
children,
...props
}: Omit<
ComponentProps<typeof ExternalLinkWrapper>,
'isVisible' | 'domain' | 'showPopup'
> = $props();
const mermaidChartDomain = 'mermaid.ai';
</script>
<ExternalLinkWrapper
{...props}
domain={mermaidChartDomain}
isVisible={env.isEnabledMermaidChartLinks}
showPopup={!isOnMermaidAI()}>
{@render children()}
</ExternalLinkWrapper>
@@ -0,0 +1,8 @@
<script lang="ts">
import { asset } from '$app/paths';
import type { ClassValue } from 'svelte/elements';
let { class: className }: { class?: ClassValue } = $props();
</script>
<img class={['size-4', className]} src={asset('/mermaidchart-logo.svg')} alt="Mermaid Chart" />
+100
View File
@@ -0,0 +1,100 @@
<script lang="ts">
import type { EditorProps } from '$/types';
import { validatedState } from '$/util/state.svelte';
import { json, jsonLanguage } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { yamlFrontmatter } from '@codemirror/lang-yaml';
import { language } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { vsCodeDark } from '@fsegurai/codemirror-theme-vscode-dark';
import { vsCodeLight } from '@fsegurai/codemirror-theme-vscode-light';
import { basicSetup } from 'codemirror';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte';
let editorView: EditorView | undefined;
let editorContainer: HTMLDivElement;
// Deliberately not $state: the sync effect below both reads and writes it,
// so a reactive currentText would make every keystroke re-run the effect
// against the not-yet-revalidated state and revert the user's input.
let currentText = '';
const themeCompartment = new Compartment();
const languageCompartment = new Compartment();
const { onUpdate }: EditorProps = $props();
$effect(() => {
editorView?.dispatch({
effects: themeCompartment.reconfigure(mode.current === 'dark' ? vsCodeDark : vsCodeLight)
});
});
onMount(() => {
editorView = new EditorView({
state: EditorState.create({
doc: currentText,
extensions: [
basicSetup,
languageCompartment.of([]),
themeCompartment.of([]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newText = update.state.doc.toString();
if (currentText === newText) {
return;
}
currentText = newText;
onUpdate(newText);
}
}),
EditorView.theme({
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
height: '100%'
},
'&.cm-scroller': {
overflow: 'auto'
}
})
]
}),
parent: editorContainer
});
return () => {
editorView?.destroy();
};
});
$effect(() => {
const { editorMode, code, mermaid } = validatedState.current;
const text = editorMode === 'code' ? code : mermaid;
if (currentText === text || !editorView) {
return;
}
currentText = text;
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: text
}
});
const stateLanguage = editorView.state.facet(language);
const isStateJson = stateLanguage === jsonLanguage;
const isCodeJson = editorMode === 'config';
if (stateLanguage && isStateJson === isCodeJson) {
return;
}
editorView.dispatch({
effects: languageCompartment.reconfigure(
isCodeJson ? json() : yamlFrontmatter({ content: markdown() })
)
});
});
</script>
<div bind:this={editorContainer} class="size-full"></div>
+59 -130
View File
@@ -1,173 +1,102 @@
<script context="module" lang="ts"> <script lang="ts" module>
import { logEvent, logMermaidChartClick } from '$lib/util/stats';
import { version } from 'mermaid/package.json'; import { version } from 'mermaid/package.json';
import { logEvent, plausible } from '$lib/util/stats';
void logEvent('version', { void logEvent('version', {
mermaidVersion: version mermaidVersion: version
}); });
</script> </script>
<script lang="ts"> <script lang="ts">
import Theme from './Theme.svelte'; import { resolve } from '$app/paths';
import { dismissPromotion, getActivePromotion } from '$lib/util/promos/promo'; import MainMenu from '$/components/MainMenu.svelte';
import Privacy from './Privacy.svelte'; import { Button } from '$/components/ui/button';
let isMenuOpen = false; import { Separator } from '$/components/ui/separator';
import { dismissPromotion, getActivePromotion } from '$lib/util/promos/promo.svelte';
import { untrack, type ComponentProps, type Snippet } from 'svelte';
import MermaidIcon from '~icons/custom/mermaid';
import CloseIcon from '~icons/material-symbols/close-rounded';
import GithubIcon from '~icons/mdi/github';
import DropdownNavMenu from './DropdownNavMenu.svelte';
function toggleMenu() { interface Props {
isMenuOpen = !isMenuOpen; mobileToggle?: Snippet;
children: Snippet;
hidePromotion?: boolean;
} }
interface Link { let { children, mobileToggle, hidePromotion = false }: Props = $props();
href: string;
title?: string; type Links = ComponentProps<typeof DropdownNavMenu>['links'];
icon?: string;
img?: string; const githubLinks: Links = [
} { title: 'Mermaid JS', href: 'https://github.com/mermaid-js/mermaid' },
const links: Link[] = [
{ {
title: 'Documentation', title: 'Mermaid Live Editor',
href: 'https://mermaid.js.org/intro/getting-started.html' href: 'https://github.com/mermaid-js/mermaid-live-editor'
}, },
{ {
title: 'Tutorial', title: 'Mermaid CLI',
href: 'https://mermaid.js.org/ecosystem/tutorials.html'
},
{
title: 'Mermaid',
href: 'https://github.com/mermaid-js/mermaid'
},
{
title: 'CLI',
href: 'https://github.com/mermaid-js/mermaid-cli' href: 'https://github.com/mermaid-js/mermaid-cli'
},
{
href: 'https://github.com/mermaid-js/mermaid-live-editor',
icon: 'fab fa-github fa-lg'
},
{
href: 'https://mermaidchart.com',
img: './mermaidchart-logo.svg'
} }
]; ];
let activePromotion = getActivePromotion(); let activePromotion = $state(untrack(() => (hidePromotion ? undefined : getActivePromotion())));
const trackBannerClick = () => { const trackBannerClick = () => {
if (!plausible || !activePromotion) { if (!activePromotion) {
return; return;
} }
logEvent('bannerClick', { logEvent('bannerClick', {
promotion: activePromotion.id promotion: activePromotion.id
}); });
logMermaidChartClick('banner');
}; };
</script> </script>
{#if activePromotion} {#if activePromotion}
<div class="top-bar z-10 flex h-fit w-full bg-primary">
<div <div
class="top-bar z-10 flex h-fit w-full items-center justify-center bg-gradient-to-r from-[#bd34fe] to-[#ff3670] p-1 text-center text-white"> class="flex grow"
<div
class="flex flex-grow"
role="button" role="button"
tabindex="0" tabindex="0"
on:click={trackBannerClick} onclick={trackBannerClick}
on:keypress={trackBannerClick}> onkeypress={trackBannerClick}>
<svelte:component this={activePromotion.component} /> <activePromotion.component {closeBanner} />
</div> </div>
<button {#snippet closeBanner()}
<Button
title="Dismiss banner" title="Dismiss banner"
on:click={() => { variant="ghost"
class="hover:bg-transparent hover:text-[#261A56]"
size="sm"
onclick={() => {
dismissPromotion(activePromotion?.id); dismissPromotion(activePromotion?.id);
activePromotion = undefined; activePromotion = undefined;
}}> }}>
<i class="fa fa-close px-2" /> <CloseIcon />
</button> </Button>
{/snippet}
</div> </div>
{/if} {/if}
<div class="navbar bg-primary p-0 shadow-lg"> <nav class="z-50 flex p-4 sm:p-6">
<div class="mx-2 flex-1 px-2"> <div class="flex flex-1 items-center gap-2">
<span class="text-lg font-bold"> <MainMenu />
<a href="/">Mermaid<span class="text-xs font-thin">v{version}</span> Live Editor</a> <MermaidIcon class="size-6" />
</span> <a href={resolve('/', {})} class="whitespace-nowrap text-accent">
</div> {#if !mobileToggle}
Mermaid
<label
for="menu-toggle"
class={isMenuOpen ? 'hidden' : 'pointer-cursor fixed right-4 z-[1000] lg:hidden'}>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="fill-current"
width="20"
height="20"
viewBox="0 0 20 20">
<title>Menu</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
</svg>
</label>
<!-- Cross SVG -->
<label
for="menu-toggle"
class={isMenuOpen ? 'pointer-cursor fixed right-4 z-[1000] lg:hidden' : 'hidden'}>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="fill-current"
width="20"
height="20"
viewBox="0 0 20 20">
<title>Cross</title>
<line x1="5" y1="5" x2="15" y2="15" stroke="white" stroke-width="2" />
<line x1="5" y1="15" x2="15" y2="5" stroke="white" stroke-width="2" />
</svg>
</label>
<input
class="hidden"
type="checkbox"
id="menu-toggle"
bind:checked={isMenuOpen}
on:click={toggleMenu} />
<div class="hidden w-full lg:flex lg:w-auto lg:items-center" id="menu">
<Theme />
<ul class="items-center justify-between pt-4 text-base lg:flex lg:pt-0">
<li>
<Privacy />
</li>
{#each links as { title, href, icon, img }}
<li>
<a class="btn btn-ghost" target="_blank" {href}>
{#if icon}
<i class={icon} />
{:else if img}
<img src={img} alt={title} />
{/if}
{#if title}
{title}
{/if} {/if}
Live Editor
</a> </a>
</li>
{/each}
</ul>
</div> </div>
<div
id="menu"
class="hidden flex-nowrap items-center justify-between gap-3 overflow-hidden md:flex">
<DropdownNavMenu icon={GithubIcon} links={githubLinks} />
<Separator orientation="vertical" />
{@render children()}
</div> </div>
{@render mobileToggle?.()}
<style> </nav>
#menu-toggle:checked + #menu {
position: absolute;
top: 2.5rem;
padding: 1rem 0;
background: #661ae6;
display: flex;
}
.navbar {
z-index: 10000;
}
img {
width: 1.5rem;
height: 1.5rem;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
<script lang="ts">
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import type { PanZoomState } from '$/util/panZoom';
import { urls } from '$/util/state.svelte';
import ExpandIcon from '~icons/material-symbols/open-in-full-rounded';
import ArrowsToCircleIcon from '~icons/material-symbols/screenshot-frame-2';
import MagnifyingGlassPlusIcon from '~icons/material-symbols/zoom-in';
import MagnifyingGlassMinusIcon from '~icons/material-symbols/zoom-out';
let { panZoomState }: { panZoomState: PanZoomState } = $props();
</script>
<FloatingToolbar>
<Button variant="ghost" size="icon" title="Reset view" onclick={() => panZoomState.reset()}>
<ArrowsToCircleIcon />
</Button>
<Separator orientation="vertical" />
<Button
variant="ghost"
size="icon"
class="hidden sm:block"
onclick={() => panZoomState.zoomOut()}>
<MagnifyingGlassMinusIcon />
</Button>
<Button variant="ghost" size="icon" class="hidden sm:block" onclick={() => panZoomState.zoomIn()}>
<MagnifyingGlassPlusIcon />
</Button>
<Separator orientation="vertical" class="hidden sm:block" />
<Button variant="ghost" size="icon" title="Full Screen" href={urls.current.view} target="_blank">
<ExpandIcon />
</Button>
</FloatingToolbar>
+69 -172
View File
@@ -1,148 +1,20 @@
<script lang="ts"> <script lang="ts">
import { updateCode } from '$lib/util/state'; import Card from '$/components/Card/Card.svelte';
import Card from '$lib/components/Card/Card.svelte'; import { Button, buttonVariants } from '$/components/ui/button';
import * as Popover from '$/components/ui/popover';
import { getSampleDiagrams, type SampleExample } from '$/util/mermaid';
import { updateCode } from '$lib/util/state.svelte';
import { logEvent } from '$lib/util/stats'; import { logEvent } from '$lib/util/stats';
import { cn } from '$lib/utils';
import ShapesIcon from '~icons/material-symbols/account-tree-outline-rounded';
import ChevronDownIcon from '~icons/material-symbols/keyboard-arrow-down-rounded';
const samples = { const extras: Record<string, SampleExample[]> = {
Flow: `flowchart TD ZenUML: [
A[Christmas] -->|Get money| B(Go shopping) {
B --> C{Let me think} title: 'Order Service',
C -->|One| D[Laptop] isDefault: true,
C -->|Two| E[iPhone] code: `zenuml
C -->|Three| F[fa:fa-car Car]`,
Sequence: `sequenceDiagram
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!`,
Class: `classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}`,
State: `stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]`,
Gantt: `gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d`,
Pie: `pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15`,
ER: `erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"`,
'User Journey': `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 3: Me`,
Git: `gitGraph
commit
commit
branch develop
checkout develop
commit
commit
checkout main
merge develop
commit
commit`,
Mindmap: `mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectivness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid`,
QuadrantChart: `quadrantChart
title Reach and engagement of campaigns
x-axis Low Reach --> High Reach
y-axis Low Engagement --> High Engagement
quadrant-1 We should expand
quadrant-2 Need to promote
quadrant-3 Re-evaluate
quadrant-4 May be improved
Campaign A: [0.3, 0.6]
Campaign B: [0.45, 0.23]
Campaign C: [0.57, 0.69]
Campaign D: [0.78, 0.34]
Campaign E: [0.40, 0.34]
Campaign F: [0.35, 0.78]`,
XYChart: `
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]`,
Block: `block-beta
columns 3
doc>"Document"]:3
space down1<[" "]>(down) space
block:e:3
l["left"]
m("A wide one in the middle")
r["right"]
end
space down2<[" "]>(down) space
db[("DB")]:3
space:3
D space C
db --> D
C --> db
D --> C
style m fill:#d6d,stroke:#333,stroke-width:4px
`,
ZenUML: `zenuml
title Order Service title Order Service
@Actor Client #FFEBE6 @Actor Client #FFEBE6
@Boundary OrderController #0747A6 @Boundary OrderController #0747A6
@@ -166,48 +38,73 @@
} }
} }
` `
}
]
}; };
type SampleTypes = keyof typeof samples; const samples = { ...getSampleDiagrams(), ...extras };
const loadSampleDiagram = (diagramType: SampleTypes): void => {
updateCode(samples[diagramType], { const loadSampleDiagram = (diagramType: string, example: SampleExample): void => {
updateDiagram: true, updateCode(example.code, {
resetPanZoom: true resetPanZoom: true,
updateDiagram: true
}); });
logEvent('loadSampleDiagram', { diagramType }); logEvent('loadSampleDiagram', { diagramType, exampleTitle: example.title });
}; };
// Adding in this array will add an icon to the preset menu const mainDiagrams = [
const newDiagrams: SampleTypes[] = ['QuadrantChart', 'XYChart', 'Block', 'ZenUML']; 'Flowchart',
const diagramOrder: SampleTypes[] = [
'Flow',
'Sequence',
'Class', 'Class',
'Sequence',
'Entity Relationship',
'State', 'State',
'ER', 'Mindmap'
'Gantt', ];
'User Journey',
'Git', const diagramOrder = [
'Pie', ...mainDiagrams,
'Mindmap', ...Object.keys(samples)
'QuadrantChart', .filter((key) => !mainDiagrams.includes(key))
'XYChart', .sort()
'Block',
'ZenUML'
]; ];
</script> </script>
<Card title="Sample Diagrams" isOpen={false}> <Card title="Sample Diagrams" isOpen isStackable icon={{ component: ShapesIcon }}>
<div class="flex flex-wrap gap-2 p-2"> <div class="flex h-fit max-h-52 flex-wrap gap-2 overflow-y-auto p-2">
{#each diagramOrder as sample} {#each diagramOrder as sample (sample)}
<button {@const examples = samples[sample]}
class="btn btn-primary btn-sm w-fit min-w-20 flex-grow normal-case" <div class="flex min-w-20 flex-grow">
on:click={() => loadSampleDiagram(sample)}> <Button
size="sm"
class={cn('flex-grow normal-case', examples.length > 1 && 'rounded-r-none')}
onclick={() => loadSampleDiagram(sample, examples[0])}>
{sample} {sample}
{#if newDiagrams.includes(sample)} </Button>
<span class="fa fa-heart ml-2" /> {#if examples.length > 1}
<Popover.Root>
<Popover.Trigger
aria-label="Choose a {sample} example"
class={cn(
buttonVariants({ size: 'sm' }),
'rounded-l-none border-l border-primary-foreground/30 px-0.5 [&_svg]:size-5'
)}>
<ChevronDownIcon />
</Popover.Trigger>
<Popover.Content align="start" class="flex w-fit flex-col gap-1 p-1">
{#each examples as example (example.title)}
<Popover.Close
class={cn(
buttonVariants({ variant: 'ghost', size: 'sm' }),
'justify-start normal-case'
)}
onclick={() => loadSampleDiagram(sample, example)}>
{example.title}
</Popover.Close>
{/each}
</Popover.Content>
</Popover.Root>
{/if} {/if}
</button> </div>
{/each} {/each}
</div> </div>
</Card> </Card>
+59 -35
View File
@@ -1,42 +1,66 @@
<label for="privacyModal" class="btn btn-ghost flex gap-2"> <script lang="ts">
<i class="fa-solid fa-shield-halved text-xl"></i> Security import ExternalLinkWrapper from '$/components/ExternalLinkWrapper.svelte';
</label> import * as Dialog from '$/components/ui/dialog';
import { env } from '$/util/env';
import { isOnMermaidLive } from '$/util/migration/domainMigration';
import ShieldIcon from '~icons/material-symbols/shield-lock-outline-rounded';
</script>
<input type="checkbox" id="privacyModal" class="modal-toggle" /> {#if env.privacyPolicyUrl}
<div class="modal" role="dialog"> <a href={env.privacyPolicyUrl} target="_blank">
<div class="modal-box flex flex-col gap-4 text-base-content"> <ShieldIcon />
<h1 class="text-3xl font-bold">Data security</h1>
<p>
<span class="text-xl font-extrabold">
The content of the diagrams you create never leaves your browser.
</span>
It's stored in the URL, and the browser's local storage only.
</p>
<p>
Mermaid live editor is a fully client side application, that will also work as an offline <a
href="https://web.dev/explore/progressive-web-apps"
class="link"
target="_blank">
PWA.
</a> </a>
{:else}
<Dialog.Root>
<Dialog.Trigger>
<ShieldIcon />
</Dialog.Trigger>
<Dialog.Content class="max-h-full overflow-hidden overflow-y-auto p-12">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl">
<ShieldIcon class="size-8 text-green-700" />
Data security
</Dialog.Title>
</Dialog.Header>
{#if isOnMermaidLive()}
<p class="text-xl font-semibold">Your diagrams never leave your browser.</p>
<p>They're only stored in the URL and your browser's local storage.</p>
<p>
This is a fully open source, client-side app deployed on <a
href="https://github.com/mermaid-js/mermaid-live-editor/deployments"
class="underline"
target="_blank">GitHub Pages</a>
that works offline as a
<a href="https://web.dev/explore/progressive-web-apps" target="_blank"
>Progressive Web App</a
>.
</p> </p>
<p> <p>
The only server we have is a self hosted version of the open source and privacy friendly We use self hosted, privacy-friendly Plausible Analytics to collect anonymous usage
Plausible Analytics. We only collect data related to actions performed, like the type of metadata (diagram types, feature usage, etc.). All data is <a
diagram rendered, number of times a feature was used, etc. <br /> All the data we collect is href="https://p.mermaid.live/mermaid.live"
anonymized and class="underline"
<a href="https://p.mermaid.live/mermaid.live" class="link" target="_blank"> target="_blank">publicly available</a
available publicly. >.
</a>
</p> </p>
<ExternalLinkWrapper domain="example.com" isVisible>
<p class="text-left">
External services (PNG/SVG/Kroki exports, "Save to Mermaid Chart", "Repair with AI",
etc) will share your diagram with those 3rd parties, and are highlighted in the UI on
hover.
</p>
</ExternalLinkWrapper>
{:else}
<p>No privacy policy has been configured for this deployment.</p>
<p> <p>
Additional services like the external PNG/SVG/Kroki links and "Save to Mermaid Chart" feature If you are self-hosting the Mermaid Live Editor, set the
will share your diagram with the respective 3rd party service. <code class="rounded bg-muted px-1.5 py-0.5 text-sm">MERMAID_PRIVACY_POLICY_URL</code>
environment variable at build time to link to your privacy policy, or set
<code class="rounded bg-muted px-1.5 py-0.5 text-sm">MERMAID_HIDE_PRIVACY_POLICY</code>
to <code class="rounded bg-muted px-1.5 py-0.5 text-sm">true</code> to hide this button.
</p> </p>
{/if}
<label class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" for="privacyModal"> </Dialog.Content>
X </Dialog.Root>
</label> {/if}
</div>
</div>
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import { buttonVariants } from '$/components/ui/button';
import * as Dialog from '$/components/ui/dialog';
import { Separator } from '$/components/ui/separator';
import { env } from '$/util/env';
import { urls } from '$/util/state.svelte';
import { asset } from '$app/paths';
import ShareIcon from '~icons/material-symbols/share';
import CopyInput from './CopyInput.svelte';
import MermaidChartIcon from './MermaidChartIcon.svelte';
</script>
<Dialog.Root>
<Dialog.Trigger class={buttonVariants({ size: 'sm' })}>Share</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl">
<ShareIcon class="size-5" /> Shareable links
</Dialog.Title>
<Dialog.Description>Share your diagrams with others.</Dialog.Description>
</Dialog.Header>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<h2 class="flex items-center gap-2">
<img class="size-5" src={asset('/favicon.svg')} alt="Mermaid Live Editor" />
Mermaid Live Editor
</h2>
<CopyInput value={window.location.href} />
<Dialog.Description>
The content of the diagrams you create never leaves your browser.
</Dialog.Description>
</div>
{#if env.isEnabledMermaidChartLinks}
<Separator />
<div class="flex flex-col gap-2">
<h2 class="flex items-center gap-2">
<MermaidChartIcon class="size-5" />
Mermaid Chart Playground
</h2>
<CopyInput value={urls.current.mermaidChart({ medium: 'share' }).playground} />
<Dialog.Description>
Opens the Mermaid Chart Playground with Mermaid AI, Visual Editor, and more.
</Dialog.Description>
</div>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,27 @@
<script lang="ts">
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
import { Toggle } from '$/components/ui/toggle';
import { defaultState, inputState, updateCodeStore } from '$/util/state.svelte';
import RoughIcon from '~icons/material-symbols/draw-outline-rounded';
import BackgroundIcon from '~icons/material-symbols/grid-4x4-rounded';
if (inputState.grid === undefined) {
// Handle cases where old states were saved without grid option
updateCodeStore({ grid: defaultState.grid });
}
</script>
<FloatingToolbar>
<Toggle
bind:pressed={() => inputState.rough, (rough) => updateCodeStore({ rough })}
size="sm"
title="Hand-Drawn">
<RoughIcon />
</Toggle>
<Toggle
bind:pressed={() => inputState.grid ?? defaultState.grid, (grid) => updateCodeStore({ grid })}
size="sm"
title="Background Grid">
<BackgroundIcon />
</Toggle>
</FloatingToolbar>
-66
View File
@@ -1,66 +0,0 @@
<script lang="ts">
import { setTheme, themeStore } from '$lib/util/theme';
const themes = [
'🌝 light',
'🌚 dark',
'🧁 cupcake',
'🐝 bumblebee',
'✳️ emerald',
'🏢 corporate',
'🌃 synthwave',
'👴 retro',
'🤖 cyberpunk',
'🌸 valentine',
'🎃 halloween',
'🌷 garden',
'🌲 forest',
'🐟 aqua',
'👓 lofi',
'🖍 pastel',
'🧚‍♀️ fantasy',
'📝 wireframe',
'🏴 black',
'💎 luxury',
'🧛‍♂️ dracula'
];
</script>
<div class="dropdown hidden lg:block">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div tabindex="0" class="btn btn-ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-6 w-6 stroke-current md:mr-2"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /></svg>
<span class="hidden md:inline">Theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1792 1792"
class="ml-1 inline-block h-4 w-4 fill-current"
><path
d="M1395 736q0 13-10 23l-466 466q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l393 393 393-393q10-10 23-10t23 10l50 50q10 10 10 23z" /></svg>
</div>
<div
class="dropdown-content top-px mt-14 h-96 w-56 overflow-y-auto bg-base-200 text-base-content shadow-2xl">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul tabindex="0" class="menu compact p-4">
{#each themes as theme}
<li class:bordered={$themeStore.theme !== undefined && theme.includes($themeStore.theme)}>
<span
role="menuitem"
tabindex="0"
class="btn btn-ghost justify-start"
on:click={() => setTheme(theme)}
on:keypress={() => setTheme(theme)}>{theme}</span>
</li>
{/each}
</ul>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
<script lang="ts">
import { mode } from 'mode-watcher';
import { cubicInOut } from 'svelte/easing';
import { type TransitionConfig } from 'svelte/transition';
import MoonIcon from '~icons/material-symbols/dark-mode-outline-rounded';
import SunIcon from '~icons/material-symbols/light-mode-outline-rounded';
const spin = (
node: Element,
{ duration = 400, easing = cubicInOut, clockWise = true } = {}
): TransitionConfig => {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration,
easing,
css: (t, u) => `
transform: ${transform} rotate(${u * 90 * (clockWise ? 1 : -1)}deg);
opacity: ${opacity * t}
`
};
};
</script>
<div class="inline-grid">
{#key mode.current}
<div
in:spin={{ clockWise: true }}
out:spin={{ clockWise: false }}
class="col-start-1 row-start-1">
{#if mode.current === 'dark'}
<MoonIcon />
{:else}
<SunIcon />
{/if}
</div>
{/key}
</div>
@@ -0,0 +1,31 @@
<script lang="ts">
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
import Privacy from '$/components/Privacy.svelte';
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import { TID } from '$/constants';
import { env } from '$/util/env';
import { version } from 'mermaid/package.json';
import { mode, setMode } from 'mode-watcher';
import ThemeIcon from './ThemeIcon.svelte';
</script>
<FloatingToolbar>
<span class="text-sm font-semibold opacity-60">v{version}</span>
{#if !env.hidePrivacyPolicy}
<Button variant="ghost" size="icon" title="Privacy & Security">
<Privacy />
</Button>
<Separator orientation="vertical" />
{/if}
<Button
variant="ghost"
size="icon"
data-testid={TID.themeToggleButton}
title="Switch to {mode.current === 'dark' ? 'light' : 'dark'} theme"
class="[&_svg]:size-5"
onclick={() => setMode(mode.current === 'dark' ? 'light' : 'dark')}>
<ThemeIcon />
</Button>
</FloatingToolbar>
+76 -114
View File
@@ -1,112 +1,93 @@
<script lang="ts"> <script lang="ts">
import type { State, ValidatedState } from '$lib/types'; import type { State, ValidatedState } from '$/types';
import { recordRenderTime, shouldRefreshView } from '$lib/util/autoSync'; import { recordRenderTime, shouldRefreshView } from '$/util/autoSync';
import { render as renderDiagram } from '$lib/util/mermaid'; import { render as renderDiagram } from '$/util/mermaid';
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state'; import { PanZoomState } from '$/util/panZoom';
import { logEvent, saveStatistics } from '$lib/util/stats'; import { updateCodeStore, validatedState } from '$/util/state.svelte';
import { cmdKey } from '$lib/util/util'; import { saveStatistics } from '$/util/stats';
import FontAwesome, { mayContainFontAwesome } from '$lib/components/FontAwesome.svelte';
import uniqueID from 'lodash-es/uniqueId';
import type { MermaidConfig } from 'mermaid'; import type { MermaidConfig } from 'mermaid';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import panzoom from 'svg-pan-zoom';
import { Svg2Roughjs } from 'svg2roughjs'; import { Svg2Roughjs } from 'svg2roughjs';
let {
panZoomState = new PanZoomState(),
shouldShowGrid = true
}: { panZoomState?: PanZoomState; shouldShowGrid?: boolean } = $props();
let code = ''; let code = '';
let config = ''; let config = '';
let container: HTMLDivElement; let container: HTMLDivElement | undefined = $state();
let rough: boolean; let rough: boolean;
let view: HTMLDivElement; let view: HTMLDivElement | undefined = $state();
let error = false; let error = $state(false);
let errorLines: string[] = []; let panZoom = true;
let outOfSync = false;
let hide = false;
let manualUpdate = true; let manualUpdate = true;
let panZoomEnabled = $stateStore.panZoom; let waitForFontAwesomeToLoad: FontAwesome['waitForFontAwesomeToLoad'] | undefined = $state();
let pzoom: typeof panzoom | undefined;
const handlePanZoomChange = () => { // Set up panZoom state observer to update the store when pan/zoom changes
if (!pzoom) { const setupPanZoomObserver = () => {
return; panZoomState.onPanZoomChange = (pan, zoom) => {
}
const pan = pzoom.getPan();
const zoom = pzoom.getZoom();
updateCodeStore({ pan, zoom }); updateCodeStore({ pan, zoom });
logEvent('panZoom'); };
}; };
const handlePanZoom = (state: State) => { const handlePanZoom = (state: State, graphDiv: SVGSVGElement) => {
if (!state.panZoom) { try {
return; panZoomState.updateElement(graphDiv, state);
} catch (error) {
console.error('PanZoom error:', error);
} }
hide = true;
pzoom?.destroy();
pzoom = undefined;
void Promise.resolve().then(() => {
const graphDiv = document.querySelector<HTMLElement>('#graph-div');
if (!graphDiv) {
return;
}
pzoom = panzoom(graphDiv, {
onPan: handlePanZoomChange,
onZoom: handlePanZoomChange,
controlIconsEnabled: true,
fit: true,
center: true
});
const { pan, zoom } = state;
if (pan !== undefined && zoom !== undefined && Number.isFinite(zoom)) {
pzoom.zoom(zoom);
pzoom.pan(pan);
}
hide = false;
});
}; };
const handleStateChange = async (state: ValidatedState) => { const handleStateChange = async (state: ValidatedState) => {
const startTime = Date.now(); const startTime = Date.now();
if (state.error !== undefined) { if (state.error !== undefined) {
error = true; error = true;
errorLines = state.error.toString().split('\n');
return; return;
} }
error = false; error = false;
let diagramType: string | undefined;
try { try {
if (container && state && (state.updateDiagram || state.autoSync)) { if (container) {
if (!state.autoSync) {
$inputStateStore.updateDiagram = false;
}
outOfSync = false;
manualUpdate = true; manualUpdate = true;
// Do not render if there is no change in Code/Config/PanZoom // Do not render if there is no change in Code/Config/PanZoom
if ( if (
code === state.code && code === state.code &&
config === state.mermaid && config === state.mermaid &&
panZoomEnabled === state.panZoom && rough === state.rough &&
rough === state.rough panZoom === state.panZoom
) { ) {
return; return;
} }
if (!shouldRefreshView()) { if (!shouldRefreshView()) {
outOfSync = true;
return; return;
} }
code = state.code; code = state.code;
config = state.mermaid; config = state.mermaid;
panZoomEnabled = state.panZoom;
rough = state.rough; rough = state.rough;
const scroll = view.parentElement?.scrollTop; panZoom = state.panZoom ?? true;
delete container.dataset.processed;
const { svg, bindFunctions } = await renderDiagram(
Object.assign({}, JSON.parse(state.mermaid)) as MermaidConfig,
code,
'graph-div'
);
if (mayContainFontAwesome(code)) {
await waitForFontAwesomeToLoad?.();
}
const scroll = view?.parentElement?.scrollTop;
delete container.dataset.processed;
const viewID = uniqueID('graph-');
const {
svg,
bindFunctions,
diagramType: detectedDiagramType
} = await renderDiagram(JSON.parse(state.mermaid) as MermaidConfig, code, viewID);
diagramType = detectedDiagramType;
if (svg.length > 0) { if (svg.length > 0) {
handlePanZoom(state); // eslint-disable-next-line svelte/no-dom-manipulating
container.innerHTML = svg; container.innerHTML = svg;
const graphDiv = document.querySelector<SVGSVGElement>('#graph-div'); let graphDiv = document.querySelector<SVGSVGElement>(`#${viewID}`);
if (!graphDiv) { if (!graphDiv) {
throw new Error('graph-div not found'); throw new Error('graph-div not found');
} }
@@ -115,16 +96,18 @@
svg2roughjs.svg = graphDiv; svg2roughjs.svg = graphDiv;
await svg2roughjs.sketch(); await svg2roughjs.sketch();
graphDiv.remove(); graphDiv.remove();
const sketch = document.querySelector<HTMLElement>('#container > svg'); const sketch = document.querySelector<SVGSVGElement>('#container > svg');
if (!sketch) { if (!sketch) {
throw new Error('sketch not found'); throw new Error('sketch not found');
} }
const height = sketch.getAttribute('height'); const height = sketch.getAttribute('height');
const width = sketch.getAttribute('width'); const width = sketch.getAttribute('width');
sketch.setAttribute('id', 'graph-div');
sketch.setAttribute('height', '100%'); sketch.setAttribute('height', '100%');
sketch.setAttribute('width', '100%'); sketch.setAttribute('width', '100%');
sketch.setAttribute('viewBox', `0 0 ${width} ${height}`); sketch.setAttribute('viewBox', `0 0 ${width} ${height}`);
sketch.style.maxWidth = '100%'; sketch.style.maxWidth = '100%';
graphDiv = sketch;
} else { } else {
graphDiv.setAttribute('height', '100%'); graphDiv.setAttribute('height', '100%');
graphDiv.style.maxWidth = '100%'; graphDiv.style.maxWidth = '100%';
@@ -132,79 +115,58 @@
bindFunctions(graphDiv); bindFunctions(graphDiv);
} }
} }
if (state.panZoom) {
handlePanZoom(state, graphDiv);
} }
if (view.parentElement && scroll) { }
if (view?.parentElement && scroll) {
view.parentElement.scrollTop = scroll; view.parentElement.scrollTop = scroll;
} }
error = false; error = false;
} else if (manualUpdate) { } else if (manualUpdate) {
manualUpdate = false; manualUpdate = false;
} else if (code !== state.code || config !== state.mermaid) {
outOfSync = true;
} }
} catch (error_) { } catch (error_) {
console.error('view fail', error_); console.error('view fail', error_);
error = true; error = true;
} }
const renderTime = Date.now() - startTime; const renderTime = Date.now() - startTime;
saveStatistics({ code, renderTime, isRough: state.rough }); saveStatistics({ code, diagramType, isRough: state.rough, renderTime });
recordRenderTime(renderTime, () => { recordRenderTime(renderTime, () => {
$inputStateStore.updateDiagram = true; updateCodeStore({ updateDiagram: true });
}); });
}; };
onMount(() => { onMount(() => {
stateStore.subscribe((state) => { setupPanZoomObserver();
void handleStateChange(state);
});
window.addEventListener('resize', () => {
if ($stateStore.panZoom && pzoom) {
pzoom.resize();
}
}); });
// Queue state changes to avoid race condition
let pendingStateChange = Promise.resolve();
$effect(() => {
const state = validatedState.current;
// eslint-disable-next-line @typescript-eslint/no-empty-function
pendingStateChange = pendingStateChange.then(() => handleStateChange(state).catch(() => {}));
}); });
</script> </script>
{#if (error && $stateStore.error instanceof Error) || outOfSync} <FontAwesome bind:waitForFontAwesomeToLoad />
<div
class="absolute z-10 w-full p-2 font-mono {error
? 'text-red-600'
: 'text-yellow-600'} bg-base-100 bg-opacity-80 text-left"
id="errorContainer">
{#if error}
{#each errorLines as line}
{line}<br />
{/each}
{:else}
Diagram out of sync. <br />
{#if $stateStore.autoSync}
It will be updated automatically.
{:else}
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
{/if}
{/if}
</div>
{/if}
<div id="view" bind:this={view} class="h-full p-2" class:error class:outOfSync> <div
<div id="container" bind:this={container} class="h-full overflow-auto" class:hide /> id="view"
bind:this={view}
class={['h-full w-full', shouldShowGrid && `grid-bg-${mode.current}`, error && 'opacity-50']}>
<div id="container" bind:this={container} class="h-full overflow-auto"></div>
</div> </div>
<style> <style>
#view { .grid-bg-light {
flex: 1; background-size: 30px 30px;
background-image: radial-gradient(circle, #e4e4e48c 2px, #0000 2px);
} }
#container { .grid-bg-dark {
transition: visibility 0.3s; background-size: 30px 30px;
} background-image: radial-gradient(circle, #46464646 2px, #0000 2px);
.error,
.outOfSync {
opacity: 0.5;
}
.hide {
visibility: hidden;
} }
</style> </style>
@@ -0,0 +1,101 @@
<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 { dismissEditorChooser } from '$/util/migration/domainMigration';
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 { createEditorChooserActions } from './editorChooserActions';
interface Props {
open: boolean;
}
let { open = $bindable() }: Props = $props();
// 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(() => {
handled = true;
open = false;
});
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.Root
bind:open
onOpenChange={(v) => {
if (v) return;
if (!handled) actions.log('dismissed');
handled = false;
dismissEditorChooser();
}}>
<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>
</Dialog.Root>
@@ -0,0 +1,10 @@
<script lang="ts"></script>
<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,38 @@
import { logEvent, logMermaidChartClick } from '$/util/stats';
import { getCheckoutUrl, getMermaidAiLiveUrl } from '$/util/util';
const utmMedium = 'editorSelection';
const utmCampaign = 'live_2026';
export interface EditorChooserActions {
log: (buttonClick: string) => void;
startTrial: (buttonClick?: string) => void;
dismiss: (buttonClick: string) => void;
openMermaidAiLive: (buttonClick: string) => void;
}
export const createEditorChooserActions = (close: () => void): EditorChooserActions => {
const log = (buttonClick: string) => {
logEvent('chooseEditor', { buttonClick });
};
const startTrial = (buttonClick = 'startTrial') => {
log(buttonClick);
logMermaidChartClick('editorPicker');
close();
window.open(getCheckoutUrl({ utmCampaign, utmMedium }), '_blank', 'noopener');
};
const dismiss = (buttonClick: string) => {
log(buttonClick);
close();
};
const openMermaidAiLive = (buttonClick: string) => {
log(buttonClick);
close();
window.open(getMermaidAiLiveUrl({ utmCampaign, utmMedium }), '_blank', 'noopener');
};
return { log, startTrial, dismiss, openMermaidAiLive };
};
@@ -0,0 +1,67 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { tv, type VariantProps } from 'tailwind-variants';
export const buttonVariants = tv({
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-6 [&_svg]:shrink-0',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
outline:
'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
accent: 'bg-accent text-accent-foreground hover:bg-accent/80 shadow-sm',
ghost: 'hover:bg-primary hover:text-primary-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'size-8'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href,
type = 'button',
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}>
{@render children?.()}
</button>
{/if}
+9
View File
@@ -0,0 +1,9 @@
export {
default as Button,
buttonVariants,
default as Root,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
type ButtonProps as Props
} from './button.svelte';
@@ -0,0 +1,36 @@
<script lang="ts">
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import { Dialog as DialogPrimitive } from 'bits-ui';
import X from 'lucide-svelte/icons/x';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
class={cn(
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...restProps}>
{@render children?.()}
<DialogPrimitive.Close
class="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none disabled:pointer-events-none data-[state=open]:text-muted-foreground">
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>
@@ -0,0 +1,15 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
class={cn('text-sm text-muted-foreground', className)}
{...restProps} />
@@ -0,0 +1,18 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...restProps}>
{@render children?.()}
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
{...restProps}>
{@render children?.()}
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
class={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
className
)}
{...restProps} />
@@ -0,0 +1,15 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
class={cn('text-lg leading-none font-semibold tracking-tight', className)}
{...restProps} />
+36
View File
@@ -0,0 +1,36 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Title from './dialog-title.svelte';
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
export {
Close,
Content,
Description,
//
Root as Dialog,
Close as DialogClose,
Content as DialogContent,
Description as DialogDescription,
Footer as DialogFooter,
Header as DialogHeader,
Overlay as DialogOverlay,
Portal as DialogPortal,
Title as DialogTitle,
Trigger as DialogTrigger,
Footer,
Header,
Overlay,
Portal,
Root,
Title,
Trigger
};
+7
View File
@@ -0,0 +1,7 @@
import Root from './input.svelte';
export {
Root,
//
Root as Input
};
+42
View File
@@ -0,0 +1,42 @@
<script lang="ts">
import type { InputType } from '$/types';
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLInputAttributes } from 'svelte/elements';
type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> &
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
...restProps
}: Props = $props();
</script>
{#if type === 'file'}
<input
bind:this={ref}
class={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
type="file"
bind:files
bind:value
{...restProps} />
{:else}
<input
bind:this={ref}
class={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
{type}
bind:value
{...restProps} />
{/if}
+17
View File
@@ -0,0 +1,17 @@
import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose
};
@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
align = 'center',
sideOffset = 4,
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
portalProps?: PopoverPrimitive.PortalProps;
} = $props();
</script>
<PopoverPrimitive.Portal {...portalProps}>
<PopoverPrimitive.Content
bind:ref
{align}
{sideOffset}
class={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...restProps} />
</PopoverPrimitive.Portal>
+13
View File
@@ -0,0 +1,13 @@
import { Pane } from 'paneforge';
import Handle from './resizable-handle.svelte';
import PaneGroup from './resizable-pane-group.svelte';
export {
PaneGroup,
Pane,
Handle,
//
PaneGroup as ResizablePaneGroup,
Pane as ResizablePane,
Handle as ResizableHandle
};

Some files were not shown because too many files have changed in this diff Show More