# Embed in Vue

Embed the shared Vue workbench with styles, a host-owned request and an explicit save lifecycle.

`TextDiagramEditor` owns draft editing, preview, downloads and save proposals. The application owns the current document, persistence and the decision to accept changes. The workbench never writes directly into your editor or server.

## Styles and providers

Import the component from `@quirekit/text-diagram-editor/vue` and import `@quirekit/text-diagram-editor/styles.css`. Controls also need generated styles from `@quirekit/unocss-preset` plus the theme, locale and icon providers from `@quirekit/ui/vue`.

Follow the [design system setup](http://127.0.0.1:3200/components/getting-started) for `defineQuireKitUnoConfig`, the UnoCSS Vite plugin and `virtual:uno.css`. The local design preview uses fixed port 3200. At the application root, provide `QuireThemeProvider`, `QuireLocaleProvider` and `QuireIconProvider`, using `quireIconRegistry` from `@quirekit/ui`. This documentation site's shell already installs these providers.

## Complete working integration

Start with `workspace/editor.vue`: after mounting, it supplies `ownerDocument`, obtains a host `request` and passes them to the workbench. The host decides whether to accept a save.

| File | Read when changing |
| --- | --- |
| `workspace/editor.vue` | Mounting and page behavior after save or cancel |
| `recipes/session.ts` | Current object, revision comparison and storage |
| `shared/locale.ts` | Example labels |
| `shared/types.ts` | Shared host types |

The example uses browser storage. Replace `persist` with your synchronous local transaction. Inspect one file at a time in Source.

### workspace/editor.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
import { QuireButton } from "@quirekit/ui/vue";
import { createTextDiagram, readTextDiagram, serializeTextDiagram, textDiagramMarkdown, textDiagramTemplates,
  type TextDiagramEditRequest, type TextDiagramEmbed } from "@quirekit/text-diagram";
import { TextDiagramEditor } from "@quirekit/text-diagram-editor/vue";
import "@quirekit/text-diagram-editor/styles.css";
import { createDiagramSession } from "../recipes/session";
import { exampleText } from "../shared/locale";
import type { DiagramSession } from "../shared/types";

const props = defineProps<{ /** Control language; existing diagram text remains unchanged. */ locale?: string }>();
const t = computed(() => exampleText(props.locale));
const owner = shallowRef<Document>(), request = shallowRef<TextDiagramEditRequest>(), saved = shallowRef<TextDiagramEmbed>();
const status = ref<"unsaved" | "saved" | "restored" | "cancelled">("unsaved"), storageError = ref(false), generation = ref(0);
const storageKey = "quirekit-text-diagram-playground-v1";
let session: DiagramSession;

/** Releases the old target before mounting an editor with a fresh baseline. */
function renew(): void {
  request.value?.release(); request.value = session.capture(); generation.value++;
}
onMounted(() => {
  owner.value = document;
  const template = textDiagramTemplates(props.locale)[0]!;
  let initial = createTextDiagram("playground", template.language, template.source);
  try {
    const stored = window.localStorage.getItem(storageKey);
    if (stored) { initial = readTextDiagram(JSON.parse(stored)); status.value = "restored"; }
  } catch { storageError.value = true; }
  session = createDiagramSession(initial, next => window.localStorage.setItem(storageKey, serializeTextDiagram(next)));
  saved.value = initial; renew();
});
/** Reflects only an accepted save; failed storage leaves the source editor mounted. */
function accept(): void { saved.value = session.read(); status.value = "saved"; storageError.value = false; renew(); }
/** Discards the current working copy, keeping the accepted host revision. */
function cancel(): void { status.value = "cancelled"; renew(); }
/** Downloads the entire validated saved object, including its source and PNG. */
function download(): void {
  if (!saved.value?.preview || !owner.value) return;
  const view = owner.value.defaultView!, link = owner.value.createElement("a");
  link.download = "diagram.json";
  link.href = view.URL.createObjectURL(new Blob([serializeTextDiagram(saved.value)], { type: "application/json" }));
  link.click(); setTimeout(() => view.URL.revokeObjectURL(link.href), 1000);
}
onBeforeUnmount(() => request.value?.release());
</script>
<template>
  <div class="diagram-demo" data-diagram-playground data-qk-not-prose>
    <p class="diagram-demo__hint">{{ t.persistence }}</p>
    <p v-if="storageError" role="alert">{{ t.storage }}</p>
    <TextDiagramEditor v-if="owner && request" :key="generation" :request="request" :owner-document="owner" :locale="locale" :title-editable="true" @saved="accept" @cancel="cancel" />
    <div class="diagram-demo__result">
      <output role="status" data-save-status>{{ t[status] }}<template v-if="saved?.preview"> · {{ t.revision }} {{ saved.document.revision }}</template></output>
      <QuireButton :disabled="!saved?.preview" variant="outline" @click="download">{{ t.json }}</QuireButton>
    </div>
    <details v-if="saved?.preview"><summary>{{ t.result }}</summary><pre data-saved-source>{{ textDiagramMarkdown(saved.document.language, saved.document.source) }}</pre></details>
  </div>
</template>
<style scoped>
.diagram-demo { display:grid;gap:20px;min-width:0;padding:20px; }
.diagram-demo__hint { margin:0;color:var(--qk-color-text-secondary);font-size:13px;line-height:1.6; }
.diagram-demo__result { display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border-top:1px solid var(--qk-color-border-subtle);padding-top:16px;font-size:13px; }
pre { overflow:auto;max-height:240px;font-size:12px;white-space:pre; }
summary { cursor:pointer;font-size:13px; }
@media (max-width:560px) { .diagram-demo { padding:12px; } }
</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: "弹窗使用独立副本，不会修改原模板。",
  };
}

```

## Component contract

| Prop / event | Purpose |
| --- | --- |
| `request` | Required: baseline `document`, synchronous `commit` and `release` |
| `ownerDocument` | Required: the mounted browser Document |
| `locale` | Controls and new template labels; Chinese by default, English supported |
| `languages` | Optional installed language subset; all three when omitted |
| `titleEditable` | Set explicitly to `true` for title input; Markdown hosts can set `false` |
| `saved` | Emitted after the host returns `saved`; refresh the baseline or close |
| `cancel` | User cancelled; the host releases the target and chooses the next action |

The component captures `request.document` when created. When switching objects or accepting a save, release the old request and mount with a fresh `key`. Merely replacing `request` does not reset the current draft.

## Cleanup and SSR

An embedded component does not release the host target for you. Call `request.release()` on cancellation, replacement or unmount. The component cleans up its own preview and CodeMirror. Set `ownerDocument` after `onMounted`; in Nuxt, use `ClientOnly` or a `.client.vue` boundary so server rendering never reads `document`.

Commit source and preview together. See [save and restore](/en/guide/persistence) for the complete contract.
