<script lang="ts">
  import Icon from "@iconify/svelte";
  import { onMount } from "svelte";
  import { save } from "@tauri-apps/plugin-dialog";
  import { getCurrentWebview } from "@tauri-apps/api/webview";

  import FileViewer from "$lib/components/FileViewer.svelte";
  import FileTree from "$lib/components/FileTree.svelte";
  import Editor from "$lib/components/Editor.svelte";
  import Preview from "$lib/components/Preview.svelte";
  import PromptModal from "$lib/components/PromptModal.svelte";
  import ConfirmModal from "$lib/components/ConfirmModal.svelte";
  import ConflictModal from "$lib/components/ConflictModal.svelte";
  import LoginModal from "$lib/components/LoginModal.svelte";
  import SettingsModal from "$lib/components/SettingsModal.svelte";
  import AssetsModal from "$lib/components/AssetsModal.svelte";
  import WindowControls from "$lib/components/WindowControls.svelte";
  import ImageViewer from "$lib/components/ImageViewer.svelte";
  import InfoModal from "$lib/components/InfoModal.svelte";
  import EditorToolbar from "$lib/components/EditorToolbar.svelte";
  import PageSettingsModal from "$lib/components/PageSettingsModal.svelte";

  import type { EditorView } from "@codemirror/view";
  import { insertText } from "$lib/ts/editor-actions";

  import * as api from "$lib/ts/api";
  import type { BrowseEntry } from "$lib/ts/api";
  import { pickFiles } from "$lib/ts/import";
  import {
    app,
    browseTo,
    clearMessages,
    closeTarget,
    compile,
    downloadDocument,
    openFile,
    openTarget,
    refreshAccount,
    refreshEntries,
    refreshSpaces,
    refreshTarget,
    runSync,
    saveAndCompile,
    scheduleAutosave,
    scheduleCompile,
    setError,
    setStatus,
  } from "$lib/ts/state.svelte";

  type Dialog =
    | { kind: "none" }
    | { kind: "new-project" }
    | { kind: "new-folder" }
    | { kind: "new-document" }
    | { kind: "rename-entry"; entry: BrowseEntry }
    | { kind: "delete-entry"; entry: BrowseEntry }
    | { kind: "link-entry"; entry: BrowseEntry }
    | { kind: "new-space" }
    | { kind: "delete-space"; id: string }
    | { kind: "clone-space"; id: string; name: string }
    | { kind: "new-file" }
    | { kind: "new-subfolder" }
    | { kind: "rename-file"; path: string }
    | { kind: "delete-file"; path: string }
    | { kind: "login" }
    | { kind: "settings" }
    | { kind: "info" }
    | { kind: "assets" }
    | { kind: "page-settings" }
    | { kind: "conflicts" };

  let dialog = $state<Dialog>({ kind: "none" });
  let editorView = $state<EditorView | null>(null);
  let imageViewer = $state<{ paths: string[]; index: number } | null>(null);

  const activeFile = $derived(
    app.target?.files.find((file) => file.path === app.activePath) ?? null,
  );

  function close() {
    dialog = { kind: "none" };
  }

  async function guard(action: () => Promise<void>) {
    try {
      await action();
      close();
    } catch (error) {
      setError(error);
    }
  }

  const createProject = (name: string) =>
    guard(async () => {
      const path = await api.createProjectEntry(app.currentDir, name);
      await refreshEntries();
      await openTarget(path);
    });

  const createFolder = (name: string) =>
    guard(async () => {
      await api.createFolderEntry(app.currentDir, name);
      await refreshEntries();
    });

  const createDocument = (name: string) =>
    guard(async () => {
      const path = await api.createDocumentEntry(app.currentDir, name);
      await refreshEntries();
      await openTarget(path);
    });

  const renameEntry = (entry: BrowseEntry, name: string) =>
    guard(async () => {
      await api.renameEntry(entry.path, name);
      await refreshEntries();
    });

  const deleteEntry = (entry: BrowseEntry) =>
    guard(async () => {
      await api.deleteEntry(entry.path);
      await refreshEntries();
      setStatus(`Deleted '${entry.name}'`);
    });

  const linkEntry = (entry: BrowseEntry) =>
    guard(async () => {
      const report = await api.cloudLinkProject(entry.path);
      await refreshEntries();
      await refreshSpaces();
      setStatus(`Uploaded ${report.pushed.length} files to a new cloud space`);
    });

  const createSpace = (name: string) =>
    guard(async () => {
      await api.cloudCreateSpace(name);
      await refreshSpaces();
    });

  const deleteSpace = (id: string) =>
    guard(async () => {
      await api.cloudDeleteSpace(id);
      await refreshSpaces();
    });

  const cloneSpace = (id: string, name: string) =>
    guard(async () => {
      await api.cloudCloneSpace(id, name);
      app.scope = "local";
      await browseTo("");
      setStatus(`Downloaded '${name}' to this device`);
    });

  async function importFiles() {
    const sources = await pickFiles("all");
    if (sources.length === 0) return;

    try {
      if (app.view === "editor" && app.target) {
        const imported = await api.importIntoTarget(app.target.path, sources);
        await refreshTarget();
        await compile();
        setStatus(`Imported ${imported.length} file(s)`);
      } else {
        const imported = await api.importIntoFolder(app.currentDir, sources);
        await refreshEntries();
        setStatus(`Imported ${imported.length} file(s)`);
      }
    } catch (error) {
      setError(error);
    }
  }

  const createFileInTarget = (name: string) =>
    guard(async () => {
      const path = name.includes(".") ? name : `${name}.typ`;
      await api.writeTargetFile(app.target!.path, path, "");
      await refreshTarget();
      await openFile(path);
    });

  const createFolderInTarget = (path: string) =>
    guard(async () => {
      await api.createFolderEntry(app.target!.path, path);
      await refreshTarget();
    });

  const renameFile = (path: string, next: string) =>
    guard(async () => {
      const payload = await api.readTargetFile(app.target!.path, path);
      await api.writeTargetFile(app.target!.path, next, payload.content);
      await api.deleteEntry(`${app.target!.path}/${path}`);
      await refreshTarget();
      if (app.activePath === path) await openFile(next);
    });

  const deleteFile = (path: string) =>
    guard(async () => {
      await api.deleteEntry(`${app.target!.path}/${path}`);
      if (app.activePath === path) {
        app.activePath = null;
        app.editorContent = "";
      }
      await refreshTarget();
      await compile();
    });

  async function setEntrypoint(path: string) {
    if (!app.target) return;
    try {
      await api.setTargetEntrypoint(app.target.path, path);
      await refreshTarget();
      await compile();
      setStatus(`'${path}' is now the entrypoint`);
    } catch (error) {
      setError(error);
    }
  }

  async function exportAs(format: string) {
    if (!app.target) return;

    const name = app.target.path.split("/").pop() ?? "document";
    const destination = await save({
      defaultPath: `${name.replace(/\.typ$/i, "")}.${format}`,
      filters: [{ name: format.toUpperCase(), extensions: [format] }],
    });
    if (!destination) return;

    try {
      await api.exportTarget(app.target.path, format, destination);
      setStatus(`Exported to ${destination}`);
    } catch (error) {
      setError(error);
    }
  }

  const resolveDocumentConflicts = (resolutions: api.Resolution[]) =>
    guard(async () => {
      for (const resolution of resolutions) {
        await api.cloudResolveDocument(
          app.target!.path,
          resolution.content,
          resolution.server_hash,
        );
      }
      app.conflicts = [];
      if (app.activePath) await openFile(app.activePath);
      setStatus("Conflicts resolved and uploaded");
    });

  const resolveConflicts = (resolutions: api.Resolution[]) =>
    guard(async () => {
      const report = await api.cloudResolveConflicts(
        app.target!.path,
        resolutions,
      );
      app.conflicts = report.conflicts;
      await refreshTarget();
      if (app.activePath) await openFile(app.activePath);
      setStatus("Conflicts resolved and uploaded");
    });

  function handleKeydown(event: KeyboardEvent) {
    if ((event.metaKey || event.ctrlKey) && event.key === "s") {
      event.preventDefault();
      if (app.view === "editor") saveAndCompile();
    }
  }

  let dropActive = $state(false);

  function dropDestination():
    | { kind: "folder"; path: string }
    | { kind: "target"; path: string }
    | null {
    if (app.view === "editor" && app.target) {
      return { kind: "target", path: app.target.path };
    }
    if (app.view === "files" && app.scope === "local") {
      return { kind: "folder", path: app.currentDir };
    }
    return null;
  }

  async function dropPaths(paths: string[]) {
    const destination = dropDestination();
    if (!destination || paths.length === 0) return;

    try {
      const imported =
        destination.kind === "target"
          ? await api.importIntoTarget(destination.path, paths)
          : await api.importIntoFolder(destination.path, paths);

      if (destination.kind === "target") {
        await refreshTarget();
        await compile();
      } else {
        await refreshEntries();
      }
      setStatus(`Imported ${imported.length} item(s)`);
    } catch (error) {
      setError(error);
    }
  }

  onMount(() => {
    const pending = getCurrentWebview().onDragDropEvent((event) => {
      if (event.payload.type === "over") {
        dropActive = dropDestination() !== null;
      } else if (event.payload.type === "drop") {
        dropActive = false;
        dropPaths(event.payload.paths);
      } else {
        dropActive = false;
      }
    });

    return () => {
      pending.then((unlisten) => unlisten());
    };
  });

  const lspLabel: Record<string, string> = {
    off: "LSP off",
    starting: "LSP starting",
    on: "LSP ready",
    unavailable: "LSP unavailable",
  };
</script>

<svelte:window on:keydown={handleKeydown} />

<div class="flex h-screen flex-col">
  <header
    data-tauri-drag-region
    class="flex h-11 shrink-0 select-none items-center gap-2 border-b border-[var(--color-line)] bg-[var(--color-surface)] pl-3"
  >
    {#if app.view === "editor"}
      <button
        class="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
        onclick={closeTarget}
      >
        <Icon icon="ph:arrow-left" />
        Files
      </button>
      <span data-tauri-drag-region class="text-sm font-medium">
        {app.target?.path.split("/").pop()}
      </span>
      {#if app.target?.standalone}
        <span
          class="rounded bg-[var(--color-surface-sunken)] px-1.5 py-0.5 text-[10px] text-[var(--color-ink-muted)]"
        >
          single file
        </span>
      {/if}
      {#if app.dirty}
        <span class="h-1.5 w-1.5 rounded-full bg-[var(--color-accent)]"></span>
      {/if}
    {:else}
      <Icon icon="ph:file-code" class="text-lg text-[var(--color-accent)]" />
      <span data-tauri-drag-region class="text-sm font-semibold">
        Typst Desktop
      </span>
    {/if}

    <div data-tauri-drag-region class="h-full flex-1"></div>

    {#if app.view === "editor"}
      <span
        class="flex items-center gap-1 text-[10px] text-[var(--color-ink-muted)]"
        title="Typst language server (tinymist)"
      >
        <span
          class="h-1.5 w-1.5 rounded-full
            {app.lspStatus === 'on'
            ? 'bg-[var(--color-success)]'
            : app.lspStatus === 'starting'
              ? 'bg-[var(--color-accent)]'
              : 'bg-[var(--color-ink-muted)]'}"
        ></span>
        {lspLabel[app.lspStatus]}
      </span>

      <button
        class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
        onclick={saveAndCompile}
      >
        <Icon icon="ph:floppy-disk" />
        Save
      </button>

      <div class="group relative">
        <button
          class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
        >
          <Icon icon="ph:export" />
          Export
        </button>
        <div
          class="invisible absolute right-0 top-full z-20 flex w-32 flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg group-hover:visible"
        >
          {#each ["pdf", "png", "html"] as format}
            <button
              class="px-3 py-1.5 text-left uppercase hover:bg-[var(--color-surface-sunken)]"
              onclick={() => exportAs(format)}
            >
              {format}
            </button>
          {/each}
        </div>
      </div>

      {#if app.target?.space_id || app.documentLink}
        <button
          class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
          disabled={app.syncing}
          onclick={() => runSync("sync")}
        >
          <Icon
            icon={app.syncing ? "ph:circle-notch" : "ph:arrows-clockwise"}
            class={app.syncing ? "animate-spin" : ""}
          />
          Sync
        </button>
      {/if}
    {/if}

    <button
      class="rounded-md p-1.5 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
      onclick={() => (dialog = { kind: "settings" })}
      aria-label="Settings"
    >
      <Icon icon="ph:gear-six" />
    </button>

    <button
      class="rounded-md p-1.5 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
      onclick={() => (dialog = { kind: "info" })}
      aria-label="About"
    >
      <Icon icon="ph:info" />
    </button>

    <div class="ml-1 h-5 w-px bg-[var(--color-line)]"></div>

    <WindowControls />
  </header>

  {#if app.status || app.error}
    <div
      class="flex shrink-0 items-center gap-2 border-b px-3 py-1.5 text-xs
        {app.error
        ? 'border-[var(--color-danger)]/30 bg-[var(--color-danger)]/10 text-[var(--color-danger)]'
        : 'border-[var(--color-success)]/30 bg-[var(--color-success)]/10 text-[var(--color-success)]'}"
    >
      <Icon icon={app.error ? "ph:warning-circle" : "ph:check-circle"} />
      <span class="flex-1">{app.error || app.status}</span>
      {#if app.conflicts.length > 0}
        <button
          class="rounded border border-current px-2 py-0.5 font-medium"
          onclick={() => (dialog = { kind: "conflicts" })}
        >
          Resolve
        </button>
      {/if}
      <button onclick={clearMessages} aria-label="Dismiss">
        <Icon icon="ph:x" />
      </button>
    </div>
  {/if}

  <div class="flex min-h-0 flex-1">
    {#if app.view === "files"}
      <div class="flex-1">
        <FileViewer
          onnewproject={() => (dialog = { kind: "new-project" })}
          onnewfolder={() => (dialog = { kind: "new-folder" })}
          onnewdocument={() => (dialog = { kind: "new-document" })}
          onupload={importFiles}
          onrename={(entry) => (dialog = { kind: "rename-entry", entry })}
          ondelete={(entry) => (dialog = { kind: "delete-entry", entry })}
          onlink={(entry) => (dialog = { kind: "link-entry", entry })}
          onviewimage={(paths, index) => (imageViewer = { paths, index })}
          ondownloaddocument={(documentId, title) =>
            downloadDocument(documentId, title)}
          onnewspace={() => (dialog = { kind: "new-space" })}
          onclonespace={(id, name) => (dialog = { kind: "clone-space", id, name })}
          ondeletespace={(id) => (dialog = { kind: "delete-space", id })}
          onsignin={() => (dialog = { kind: "login" })}
        />
      </div>
    {:else}
      {#if !app.target?.standalone}
        <div
          class="flex w-56 shrink-0 flex-col border-r border-[var(--color-line)] bg-[var(--color-surface)]"
        >
          <div
            class="flex items-center justify-between border-b border-[var(--color-line)] px-3 py-1.5"
          >
            <span
              class="text-[10px] font-semibold uppercase tracking-wider text-[var(--color-ink-muted)]"
            >
              Files
            </span>
            <div class="flex gap-0.5">
              <button
                class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
                onclick={() => (dialog = { kind: "new-file" })}
                aria-label="New file"
              >
                <Icon icon="ph:file-plus" />
              </button>
              <button
                class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
                onclick={() => (dialog = { kind: "new-subfolder" })}
                aria-label="New folder"
              >
                <Icon icon="ph:folder-plus" />
              </button>
              <button
                class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
                onclick={importFiles}
                aria-label="Import files from this computer"
              >
                <Icon icon="ph:upload-simple" />
              </button>
            </div>
          </div>

          <FileTree
            files={app.target?.files ?? []}
            activePath={app.activePath}
            entrypoint={app.target?.entrypoint ?? "main.typ"}
            onopen={openFile}
            onrename={(path) => (dialog = { kind: "rename-file", path })}
            ondelete={(path) => (dialog = { kind: "delete-file", path })}
            onsetentry={setEntrypoint}
          />
        </div>
      {/if}

      <div class="flex min-w-0 flex-1">
        <div class="flex min-w-0 flex-1 flex-col border-r border-[var(--color-line)]">
          {#if app.activePath && activeFile?.is_text}
            <EditorToolbar
              view={editorView}
              disabled={app.activePath.toLowerCase().endsWith(".toml")}
              onassets={() => (dialog = { kind: "assets" })}
              onpagesettings={() => (dialog = { kind: "page-settings" })}
            />

            <div
              class="flex shrink-0 items-center gap-2 border-b border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-xs"
            >
              <Icon icon="ph:file-text" class="text-[var(--color-ink-muted)]" />
              <span>{app.activePath}</span>
              {#if app.dirty}
                <span class="text-[var(--color-ink-muted)]">edited</span>
              {/if}
            </div>

            {#key app.target?.path + ":" + app.activePath}
              <div class="min-h-0 flex-1">
                <Editor
                  content={app.editorContent}
                  filePath={app.activePath}
                  targetPath={app.target?.path ?? ""}
                  diagnostics={app.diagnostics}
                  onchange={(value) => {
                    app.editorContent = value;
                    app.dirty = true;
                    scheduleCompile();
                    scheduleAutosave();
                  }}
                  onsave={saveAndCompile}
                  onlspstatus={(status) => (app.lspStatus = status)}
                  onready={(view) => (editorView = view)}
                />
              </div>
            {/key}
          {:else}
            <div
              class="flex flex-1 flex-col items-center justify-center gap-2 bg-[var(--color-surface)] text-[var(--color-ink-muted)]"
            >
              <Icon icon="ph:file-dashed" class="text-4xl" />
              <p class="text-sm">
                {app.activePath
                  ? "This file cannot be edited as text"
                  : "Select a file to edit"}
              </p>
            </div>
          {/if}
        </div>

        <div class="min-w-0 flex-1">
          <Preview
            compiled={app.compiled}
            diagnostics={app.diagnostics}
            compiling={app.compiling}
          />
        </div>
      </div>
    {/if}
  </div>
</div>

{#if imageViewer}
  <ImageViewer
    paths={imageViewer.paths}
    index={imageViewer.index}
    onclose={() => (imageViewer = null)}
  />
{/if}

{#if dropActive}
  <div
    class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-[var(--color-accent)]/10 p-8"
  >
    <div
      class="flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-[var(--color-accent)] bg-[var(--color-surface)]/95 px-10 py-8 shadow-xl"
    >
      <Icon icon="ph:tray-arrow-down" class="text-4xl text-[var(--color-accent)]" />
      <p class="text-sm font-medium">
        {app.view === "editor"
          ? `Drop to add files to ${app.target?.path.split("/").pop()}`
          : app.currentDir
            ? `Drop to add files to ${app.currentDir.split("/").pop()}`
            : "Drop to add files to your workspace"}
      </p>
      <p class="text-xs text-[var(--color-ink-muted)]">
        Images, fonts, and Typst files are copied in.
      </p>
    </div>
  </div>
{/if}

{#if dialog.kind === "new-project"}
  <PromptModal
    title="New project"
    label="Project name"
    icon="ph:folder-plus"
    placeholder="My thesis"
    onsubmit={createProject}
    onclose={close}
  />
{:else if dialog.kind === "new-folder"}
  <PromptModal
    title="New folder"
    label="Folder name"
    icon="ph:folder-plus"
    onsubmit={createFolder}
    onclose={close}
  />
{:else if dialog.kind === "new-document"}
  <PromptModal
    title="New document"
    label="Document name"
    icon="ph:file-plus"
    placeholder="notes"
    suffix=".typ"
    onsubmit={createDocument}
    onclose={close}
  />
{:else if dialog.kind === "rename-entry"}
  {@const target = dialog}
  <PromptModal
    title="Rename"
    label="New name"
    value={target.entry.name}
    confirmLabel="Rename"
    onsubmit={(name) => renameEntry(target.entry, name)}
    onclose={close}
  />
{:else if dialog.kind === "delete-entry"}
  {@const target = dialog}
  <ConfirmModal
    title="Delete"
    message="'{target.entry.name}' will be permanently removed from this device."
    onconfirm={() => deleteEntry(target.entry)}
    onclose={close}
  />
{:else if dialog.kind === "link-entry"}
  {@const target = dialog}
  <ConfirmModal
    title="Upload to cloud"
    message="A new cloud space will be created for '{target.entry.name}' and its files uploaded."
    confirmLabel="Upload"
    onconfirm={() => linkEntry(target.entry)}
    onclose={close}
  />
{:else if dialog.kind === "new-space"}
  <PromptModal
    title="New cloud space"
    label="Space name"
    icon="ph:cloud-plus"
    onsubmit={createSpace}
    onclose={close}
  />
{:else if dialog.kind === "delete-space"}
  {@const target = dialog}
  <ConfirmModal
    title="Delete cloud space"
    message="This permanently deletes the space and its files from TypstDrive. Local copies are kept."
    onconfirm={() => deleteSpace(target.id)}
    onclose={close}
  />
{:else if dialog.kind === "clone-space"}
  {@const target = dialog}
  <PromptModal
    title="Download space"
    label="Save as project"
    icon="ph:download-simple"
    value={target.name}
    confirmLabel="Download"
    onsubmit={(name) => cloneSpace(target.id, name)}
    onclose={close}
  />
{:else if dialog.kind === "new-file"}
  <PromptModal
    title="New file"
    label="File name"
    icon="ph:file-plus"
    placeholder="chapter-1"
    onsubmit={createFileInTarget}
    onclose={close}
  />
{:else if dialog.kind === "new-subfolder"}
  <PromptModal
    title="New folder"
    label="Folder name"
    icon="ph:folder-plus"
    placeholder="figures"
    onsubmit={createFolderInTarget}
    onclose={close}
  />
{:else if dialog.kind === "rename-file"}
  {@const target = dialog}
  <PromptModal
    title="Rename file"
    label="New path"
    value={target.path}
    confirmLabel="Rename"
    onsubmit={(next) => renameFile(target.path, next)}
    onclose={close}
  />
{:else if dialog.kind === "delete-file"}
  {@const target = dialog}
  <ConfirmModal
    title="Delete file"
    message="'{target.path}' will be permanently deleted."
    onconfirm={() => deleteFile(target.path)}
    onclose={close}
  />
{:else if dialog.kind === "login"}
  <LoginModal
    serverUrl={app.settings?.server_url ?? ""}
    onsuccess={async () => {
      close();
      await refreshAccount();
      app.scope = "cloud";
      setStatus("Connected to TypstDrive");
    }}
    onclose={close}
  />
{:else if dialog.kind === "settings"}
  <SettingsModal onclose={close} onsignin={() => (dialog = { kind: "login" })} />
{:else if dialog.kind === "info"}
  <InfoModal onclose={close} />
{:else if dialog.kind === "assets"}
  <AssetsModal
    oninsert={app.view === "editor" && editorView
      ? (snippet) => {
          insertText(editorView, snippet);
          close();
        }
      : undefined}
    onchanged={compile}
    onclose={close}
  />
{:else if dialog.kind === "page-settings"}
  <PageSettingsModal view={editorView} onclose={close} />
{:else if dialog.kind === "conflicts"}
  <ConflictModal
    conflicts={app.conflicts}
    onresolve={app.documentLink ? resolveDocumentConflicts : resolveConflicts}
    onclose={close}
  />
{/if}
