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>
144 lines
4.5 KiB
Svelte
144 lines
4.5 KiB
Svelte
<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>
|