feat: Add history section
This commit is contained in:
+15
-15
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-variants": "^0.3.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
<Button
|
||||
role="tab"
|
||||
variant="ghost"
|
||||
class="px-2 {activeTabID === tab.id ? 'rounded-b-none border-b-2 border-b-primary/50' : ''}"
|
||||
class="px-2 {activeTabID === tab.id
|
||||
? 'rounded-b-none border-b-2 border-b-primary-foreground/50'
|
||||
: ''}"
|
||||
onclick={toggleTabs(tab)}
|
||||
onkeypress={toggleTabs(tab)}>
|
||||
<tab.icon />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { stopPropagation } from 'svelte/legacy';
|
||||
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types';
|
||||
import { notify, prompt } from '$lib/util/notify';
|
||||
@@ -11,13 +9,15 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import GitAltIcon from '~icons/fa-brands/git-alt';
|
||||
import BookmarkIcon from '~icons/fa-regular/bookmark';
|
||||
import SaveIcon from '~icons/fa-regular/save';
|
||||
import DownloadIcon from '~icons/fa/download';
|
||||
import UploadIcon from '~icons/fa/upload';
|
||||
import HistoryIcon from '~icons/fa6-solid/history';
|
||||
import TrashAltIcon from '~icons/fa6-solid/trash-alt';
|
||||
import UndoIcon from '~icons/fa6-solid/undo';
|
||||
import BookmarkIcon from '~icons/material-symbols/bookmark-outline-rounded';
|
||||
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 { Button } from '../ui/button';
|
||||
import { Separator } from '../ui/separator';
|
||||
import {
|
||||
addHistoryEntry,
|
||||
clearHistoryData,
|
||||
@@ -35,6 +35,7 @@
|
||||
const tabSelectHandler = (tab: Tab) => {
|
||||
historyModeStore.set(tab.id as HistoryType);
|
||||
};
|
||||
|
||||
let tabs: Tab[] = $state([
|
||||
{
|
||||
id: 'manual',
|
||||
@@ -102,11 +103,6 @@
|
||||
inputStateStore.set({ ...state, updateDiagram: true });
|
||||
};
|
||||
|
||||
const relativeTime = (time: number) => {
|
||||
const t = new Date(time);
|
||||
return `${new Date(t).toLocaleString()} (${dayjs(t).fromNow()})`;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
historyModeStore.set('manual');
|
||||
setInterval(() => {
|
||||
@@ -129,68 +125,80 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card onselect={tabSelectHandler} isOpen={false} {tabs} title="History">
|
||||
<Card onselect={tabSelectHandler} isOpen isClosable={false} {tabs}>
|
||||
{#snippet actions()}
|
||||
<div>
|
||||
<button
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
id="uploadHistory"
|
||||
class="btn btn-secondary btn-xs w-12"
|
||||
onclick={stopPropagation(() => uploadHistory())}
|
||||
title="Upload history"
|
||||
aria-label="Upload history"><UploadIcon /></button>
|
||||
onclick={uploadHistory}
|
||||
title="Upload history"><UploadIcon /></Button>
|
||||
{#if $historyStore.length > 0}
|
||||
<button
|
||||
<Button
|
||||
id="downloadHistory"
|
||||
class="btn btn-secondary btn-xs w-12"
|
||||
onclick={stopPropagation(() => downloadHistory())}
|
||||
title="Download history"
|
||||
aria-label="Download history"><DownloadIcon /></button>
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onclick={downloadHistory}
|
||||
title="Download history"><DownloadIcon /></Button>
|
||||
{/if}
|
||||
|
|
||||
<button
|
||||
<Separator orientation="vertical" />
|
||||
<Button
|
||||
id="saveHistory"
|
||||
class="btn btn-success btn-xs w-12"
|
||||
onclick={stopPropagation(() => saveHistory())}
|
||||
title="Save current state"
|
||||
aria-label="Save current state"><SaveIcon /></button>
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onclick={() => saveHistory()}
|
||||
title="Save current state"><SaveIcon /></Button>
|
||||
{#if $historyModeStore !== 'loader'}
|
||||
<button
|
||||
<Button
|
||||
id="clearHistory"
|
||||
class="btn btn-error btn-xs w-12"
|
||||
onclick={stopPropagation(() => clearHistory())}
|
||||
title="Delete all saved states"
|
||||
aria-label="Delete all saved states"><TrashAltIcon /></button>
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:text-destructive"
|
||||
onclick={() => clearHistory()}
|
||||
title="Delete all saved states"><TrashAltIcon /></Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
<ul class="h-56 space-y-2 overflow-auto p-2" id="historyList">
|
||||
<ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList">
|
||||
{#if $historyStore.length > 0}
|
||||
{#each $historyStore as { id, state, time, name, url, type }}
|
||||
<li class="flex-col rounded p-2 shadow">
|
||||
<div class="flex">
|
||||
<div class="flex-1">
|
||||
<div class="text-base-content flex flex-col">
|
||||
{#if url}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
title="Open revision in new tab"
|
||||
class="text-blue-500 hover:underline">{name}</a>
|
||||
{:else}
|
||||
<span>{name}</span>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-400">{relativeTime(time)}</span>
|
||||
</div>
|
||||
{#each $historyStore as { id, state, time, name, url, type } (id)}
|
||||
<li class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
{#if url}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
title="Open revision in new tab"
|
||||
class="text-blue-500 hover:underline">{name}</a>
|
||||
{:else}
|
||||
<span>{name}</span>
|
||||
{/if}
|
||||
<span class="whitespace-nowrap text-xs text-primary-foreground/30">
|
||||
{new Date(time).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex content-center gap-2">
|
||||
<button class="btn btn-success" onclick={() => restoreHistoryItem(state)}
|
||||
><UndoIcon class="mr-1" />Restore</button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="whitespace-nowrap text-sm text-primary-foreground/50">
|
||||
{dayjs(time).fromNow()}
|
||||
</span>
|
||||
<Button size="icon" variant="ghost" onclick={() => restoreHistoryItem(state)}>
|
||||
<UndoIcon />
|
||||
</Button>
|
||||
{#if type !== 'loader'}
|
||||
<button class="btn btn-error" onclick={() => clearHistory(id)}
|
||||
><TrashAltIcon class="mr-1" />Delete</button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:text-destructive"
|
||||
onclick={() => clearHistory(id)}>
|
||||
<TrashAltIcon />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 { localStorage, persist } from '$lib/util/persist';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { generateSlug } from 'random-word-slugs';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
const MAX_AUTO_HISTORY_LENGTH = 30;
|
||||
|
||||
|
||||
@@ -15,13 +15,19 @@
|
||||
import { stateStore } from '$lib/util/state';
|
||||
import { MCBaseURL } from '$lib/util/util';
|
||||
import { toggleMode } from 'mode-watcher';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import type { ComponentProps, Snippet } from 'svelte';
|
||||
import GithubIcon from '~icons/fa-brands/github';
|
||||
import CloseIcon from '~icons/fa/close';
|
||||
import MoonIcon from '~icons/fa6-solid/moon';
|
||||
import SunIcon from '~icons/fa6-solid/sun';
|
||||
import DropdownNavMenu from './DropdownNavMenu.svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
const { isEnabledMermaidChartLinks } = env;
|
||||
|
||||
let isMenuOpen = $state(false);
|
||||
@@ -185,11 +191,7 @@
|
||||
class="absolute rotate-90 scale-0 transition-all duration-200 dark:rotate-0 dark:scale-100" />
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm">Share</Button>
|
||||
{#if isEnabledMermaidChartLinks}
|
||||
<Button size="sm" target="_blank" href="https://mermaidchart.com"
|
||||
><img class="size-6" src="./mermaidchart-logo.svg" alt="Mermaid Chart" />Save diagram</Button>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let restProps: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={$mode}
|
||||
class="toaster group"
|
||||
toastOptions={{
|
||||
classes: {
|
||||
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -1,5 +1,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export const notify = (message: string): void => {
|
||||
alert(message);
|
||||
toast(message);
|
||||
};
|
||||
|
||||
export const prompt = (message: string): boolean => {
|
||||
|
||||
@@ -86,6 +86,9 @@ export class PanZoomState {
|
||||
|
||||
public resize() {
|
||||
this.pzoom?.resize();
|
||||
if (!this.isDirty) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public zoomIn() {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||
import { loadingStateStore } from '$lib/util/loading';
|
||||
import { toggleDarkTheme } from '$lib/util/state';
|
||||
import { initHandler } from '$lib/util/util';
|
||||
import { mode, ModeWatcher } from 'mode-watcher';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
|
||||
import '../app.postcss';
|
||||
|
||||
interface Props {
|
||||
@@ -39,6 +41,8 @@
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<Toaster />
|
||||
|
||||
<main class="text-primary-content h-screen">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import History from '$/components/History/History.svelte';
|
||||
import PanZoomToolbar from '$/components/PanZoomToolbar.svelte';
|
||||
import SyncRoughToolbar from '$/components/SyncRoughToolbar.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Resizable from '$/components/ui/resizable';
|
||||
import { Toggle } from '$/components/ui/toggle';
|
||||
import VersionSecurityToolbar from '$/components/VersionSecurityToolbar.svelte';
|
||||
import { env } from '$/util/env';
|
||||
import { PanZoomState } from '$/util/panZoom';
|
||||
import Actions from '$lib/components/Actions.svelte';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
@@ -13,10 +16,11 @@
|
||||
import View from '$lib/components/View.svelte';
|
||||
import type { DocumentationConfig, EditorMode, Tab, ValidatedState } from '$lib/types';
|
||||
import { stateStore, updateCodeStore } from '$lib/util/state';
|
||||
import { initHandler } from '$lib/util/util';
|
||||
import { initHandler, MCBaseURL } from '$lib/util/util';
|
||||
import { onMount } from 'svelte';
|
||||
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
import CodeIcon from '~icons/material-symbols/code-blocks-outline';
|
||||
import HistoryIcon from '~icons/material-symbols/history';
|
||||
import GearIcon from '~icons/material-symbols/settings-outline-rounded';
|
||||
|
||||
const panZoomState = new PanZoomState();
|
||||
@@ -107,7 +111,7 @@
|
||||
updateCodeStore({ editorMode });
|
||||
};
|
||||
|
||||
const tabs: Tab[] = [
|
||||
const editorTabs: Tab[] = [
|
||||
{
|
||||
id: 'code',
|
||||
title: 'Code',
|
||||
@@ -123,15 +127,36 @@
|
||||
onMount(async () => {
|
||||
await initHandler();
|
||||
});
|
||||
|
||||
let isHistoryOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<Navbar />
|
||||
<Navbar>
|
||||
<Button size="sm">Share</Button>
|
||||
<Toggle bind:pressed={isHistoryOpen}>
|
||||
<HistoryIcon />
|
||||
</Toggle>
|
||||
{#if env.isEnabledMermaidChartLinks}
|
||||
<Button
|
||||
size="sm"
|
||||
href={`${MCBaseURL}/app/plugin/save?state=${$stateStore.serialized}`}
|
||||
target="_blank">
|
||||
<img class="size-4" src="./mermaidchart-logo.svg" alt="Mermaid Chart" />
|
||||
Save diagram
|
||||
</Button>
|
||||
{/if}
|
||||
</Navbar>
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<Resizable.PaneGroup direction="horizontal" autoSaveId="liveEditor" class="p-6 pt-0">
|
||||
<Resizable.Pane defaultSize={40} minSize={15} class="hidden md:block">
|
||||
<div class="flex h-full flex-col gap-6" id="editorPane">
|
||||
<Card onselect={tabSelectHandler} isOpen {tabs} {activeTabID} isClosable={false}>
|
||||
<Card
|
||||
onselect={tabSelectHandler}
|
||||
isOpen
|
||||
tabs={editorTabs}
|
||||
{activeTabID}
|
||||
isClosable={false}>
|
||||
{#snippet actions()}
|
||||
<div class="flex flex-row items-center">
|
||||
<Button
|
||||
@@ -203,13 +228,24 @@
|
||||
</div>
|
||||
{/snippet} -->
|
||||
|
||||
<!-- <div class="flex-1 overflow-auto"> -->
|
||||
<View {panZoomState} />
|
||||
<div class="absolute right-0 top-0"><PanZoomToolbar {panZoomState} /></div>
|
||||
<div class="absolute bottom-0 left-5"><SyncRoughToolbar /></div>
|
||||
<div class="absolute bottom-0 right-0"><VersionSecurityToolbar /></div>
|
||||
<!-- </div> -->
|
||||
<!-- </Card> -->
|
||||
<Resizable.PaneGroup direction="horizontal" autoSaveId="viewAndHistory" class="">
|
||||
<Resizable.Pane minSize={15} class="relative">
|
||||
<!-- <div class="flex-1 overflow-auto"> -->
|
||||
<View {panZoomState} />
|
||||
<div class="absolute right-0 top-0"><PanZoomToolbar {panZoomState} /></div>
|
||||
<div class="absolute bottom-0 right-0"><VersionSecurityToolbar /></div>
|
||||
<div class="absolute bottom-0 left-5"><SyncRoughToolbar /></div>
|
||||
</Resizable.Pane>
|
||||
{#if isHistoryOpen}
|
||||
<Resizable.Handle class="ml-2 opacity-0" />
|
||||
<Resizable.Pane minSize={25} defaultSize={40}>
|
||||
<div class="flex h-full flex-grow flex-col">
|
||||
<History />
|
||||
</div>
|
||||
</Resizable.Pane>
|
||||
{/if}
|
||||
</Resizable.PaneGroup>
|
||||
|
||||
<div class="mx-2 rounded p-2 shadow md:hidden">
|
||||
Code editing not supported on mobile. Please use a desktop browser.
|
||||
</div>
|
||||
|
||||
@@ -6337,6 +6337,11 @@ svelte-preprocess@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz#fdc1f9dc41b6f22bf8b1f059e9f21eaaae181eeb"
|
||||
integrity sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==
|
||||
|
||||
svelte-sonner@^0.3.28:
|
||||
version "0.3.28"
|
||||
resolved "https://registry.yarnpkg.com/svelte-sonner/-/svelte-sonner-0.3.28.tgz#b24b3e7af924d5c50ca2e33cede1382283074abf"
|
||||
integrity sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==
|
||||
|
||||
svelte-toolbelt@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz#63e9c6629456a1a0dd57812f85426e4631fd754e"
|
||||
|
||||
Reference in New Issue
Block a user