# Open a dialog

Open the workbench from a browser host, pass its theme and release the request on close.

Use `openTextDiagramDialog` for a button → edit diagram → return to page workflow. It creates an independent Vue root, lazily loads the workbench and provides its own providers. The host still installs the [workbench styles](/en/guide/vue).

### dialog/launcher.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, shallowRef } from "vue";
import { QuireButton } from "@quirekit/ui/vue";
import { createTextDiagram, textDiagramTemplates } from "@quirekit/text-diagram";
import { openTextDiagramDialog, type TextDiagramDialogHandle } from "@quirekit/text-diagram-editor";
import "@quirekit/text-diagram-editor/styles.css";
import { createDiagramSession } from "../recipes/session";
import { exampleText } from "../shared/locale";
import { dialogTheme } from "../shared/theme";

const props = defineProps<{ /** Dialog control language. */ locale?: string }>();
const t = computed(() => exampleText(props.locale));
const sample = textDiagramTemplates(props.locale).find(item => item.language === "plantuml")!;
const session = createDiagramSession(createTextDiagram("dialog-example", sample.language, sample.source));
const accepted = shallowRef(session.read()), status = ref<"unsaved" | "close">("unsaved");
const lifetime = new AbortController();
let dialog: TextDiagramDialogHandle | undefined;

/** Opens a request owned by this example, releasing it exactly once on close. */
function open(): void {
  dialog?.close();
  const request = session.capture();
  dialog = openTextDiagramDialog({
    ownerDocument: document, locale: props.locale, theme: dialogTheme(document), signal: lifetime.signal,
    request: { ...request,
      /** @param next - Exact source and PNG. @returns Host acceptance before the dialog closes. */
      commit(next) { const result = request.commit(next); if (result === "saved") accepted.value = session.read(); return result; },
      /** Cancels the target when the dialog is saved, cancelled or removed. */
      release() { request.release(); status.value = "close"; },
    },
  });
}
onBeforeUnmount(() => lifetime.abort());
</script>
<template>
  <section class="dialog-example" data-dialog-example data-qk-not-prose>
    <p>{{ t.memory }}</p>
    <div><QuireButton color="primary" @click="open">{{ t.open }}</QuireButton></div>
    <output role="status">{{ t[status] }} · {{ t.revision }} {{ accepted.document.revision }}</output>
    <img v-if="accepted.preview" :src="accepted.preview.dataUrl" :alt="accepted.title" />
  </section>
</template>
<style scoped>
.dialog-example { display:grid;gap:16px;padding:20px;font-size:13px; }
p { margin:0;color:var(--qk-color-text-secondary); }
img { max-width:100%;max-height:360px;object-fit:contain;background:white; }
</style>

```

### recipes/session.ts

```ts
import { readTextDiagram, serializeTextDiagram, type TextDiagramEmbed } from "@quirekit/text-diagram";
import type { DiagramSession } from "../shared/types";

/**
 * Creates a single-object host with compare-and-save semantics.
 * @param initial - Validated persisted object or new draft.
 * @param persist - Optional synchronous storage write; throwing leaves the baseline unchanged.
 * @returns A host whose requests cannot overwrite a newer accepted revision.
 */
export function createDiagramSession(initial: TextDiagramEmbed, persist?: (next: TextDiagramEmbed) => void): DiagramSession {
  let current = readTextDiagram(initial);
  return {
    /** @returns The immutable accepted revision. */
    read() { return current; },
    /** @returns A detached baseline with explicit commit and release callbacks. */
    capture() {
      const baseline = current, expected = serializeTextDiagram(baseline);
      let released = false;
      return {
        document: baseline,
        /** @param proposal - Source and preview proposed by the workbench. @returns Atomic host acceptance. */
        commit(proposal) {
          if (released) return "missing";
          if (serializeTextDiagram(current) !== expected) return "conflict";
          const next = readTextDiagram(proposal);
          if (next.id !== baseline.id || next.document.revision !== baseline.document.revision + 1 || !next.preview) {
            throw new TypeError("Invalid diagram save");
          }
          // Storage must succeed before publishing the new revision to the UI.
          persist?.(next);
          current = next;
          return "saved";
        },
        /** Closes this target without changing the accepted object. */
        release() { released = true; },
      };
    },
  };
}

```

### shared/types.ts

```ts
import type { TextDiagramEditRequest, TextDiagramEmbed, TextDiagramSnapshot, TextDiagramRaster } from "@quirekit/text-diagram";

/** Minimal synchronous host; durable storage belongs to its caller. */
export interface DiagramSession {
  /** @returns The last accepted immutable object. */
  read(): TextDiagramEmbed;
  /** @returns A new edit request capturing the current baseline. */
  capture(): TextDiagramEditRequest;
}

/** Two output formats generated from exactly the same source. */
export interface DiagramFiles {
  /** Sanitized vector snapshot. */
  readonly vector: TextDiagramSnapshot;
  /** Bounded PNG projection. */
  readonly raster: TextDiagramRaster;
}

```

### shared/locale.ts

```ts
/** @param locale - Host interface language. @returns Demonstration labels; source is never translated. */
export function exampleText(locale = "zh-CN") {
  return locale.startsWith("en") ? {
    saved: "Saved in this browser", restored: "Restored the last saved diagram", cancelled: "Draft cancelled; showing the last saved version",
    unsaved: "No saved diagram yet", storage: "The saved diagram could not be read. You can edit a new draft; saving will replace that local entry.",
    persistence: "Save diagram stores the source and PNG in this browser. Unsaved edits are discarded when you leave this page.",
    result: "Last saved Markdown", json: "Download saved JSON", revision: "Revision", open: "Open diagram editor",
    close: "Editor closed", memory: "This dialog example keeps accepted changes until you leave the page.",
    templates: "Template catalog", language: "Language", template: "Template", try: "Try this template",
    original: "Template source", selected: "The dialog uses a detached copy; the template stays unchanged.",
  } : {
    saved: "已保存到当前浏览器", restored: "已恢复上次保存的图形", cancelled: "草稿已取消，已回到上次保存的版本",
    unsaved: "尚未保存图形", storage: "无法读取已保存的图形。可以编辑新草稿；保存将替换这一条本地记录。",
    persistence: "“保存图形”将源码与 PNG 保存在当前浏览器。离开本页会丢弃尚未保存的修改。",
    result: "上次保存的 Markdown", json: "下载已保存 JSON", revision: "修订号", open: "打开绘图编辑器",
    close: "编辑器已关闭", memory: "此弹窗示例在离开页面前保留已保存的修改。",
    templates: "模板目录", language: "绘图语言", template: "模板", try: "试用这个模板",
    original: "模板源码", selected: "弹窗使用独立副本，不会修改原模板。",
  };
}

```

### shared/theme.ts

```ts
import { createTheme, type QuireKitTheme } from "@quirekit/ui";

/** @param owner - Document with a host-controlled CSS color scheme. @returns Theme snapshot for an independent dialog root. */
export function dialogTheme(owner: Document): QuireKitTheme {
  const scheme = owner.defaultView!.getComputedStyle(owner.documentElement).colorScheme;
  return createTheme({ scheme: scheme === "dark" ? "dark" : "light" });
}

```

The example starts with a PlantUML sequence diagram. Saving displays a PNG; reopening reads the newly accepted revision. Cancelling leaves the object unchanged. This example's in-memory session ends when you leave the page.

## Theme and lifetime

Pass the actual `ownerDocument`, `locale` and host `theme`. Theme is not persisted in the document: previews retain the engine's or source-defined appearance. The example derives a theme from the page's CSS `color-scheme`; an application can pass its own `QuireKitTheme` directly.

The returned `TextDiagramDialogHandle` is usable immediately. Call `close()` even before lazy loading completes; repeated calls are safe. Pass `signal` to close everything when the host unmounts.

The dialog calls `request.release()` after a successful save, cancellation, external close or loading failure. Release is not a save notification: it also runs on cancellation. A save is accepted only when `commit` returns `saved`.

## Focus and commit

In the source pane, Esc first handles search or the editor's focus escape. Press Esc then Tab to leave the source pane. Closing restores focus to the trigger. If commit returns `conflict`, `readonly` or `missing`, the workbench explains the outcome and retains the draft.

`commit` is synchronous local acceptance, not a Promise. Applications with server persistence must define a local transaction and synchronization strategy first. Do not return `saved` before accepting the object. See [persistence](/en/guide/persistence) and the [dialog API](/en/api/editor).
