# Playground

Edit source, switch templates, preview, download and save a diagram in this browser.

Choose a language and edit its source. Selecting a template does not replace your draft until you click Apply template. Each language retains its draft and undo history during this editing session. Switching the interface language never translates existing 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: "弹窗使用独立副本，不会修改原模板。",
  };
}

```

## Where saves go

Save diagram writes to this site's browser storage; there is no upload service. Reloading or returning restores the last successful save. **Leaving this page discards unsaved changes.** Cancel restores the last saved version.

SVG / PNG downloads use the current valid preview and do not require a save. The JSON download contains the last saved source, language, title, revision and PNG. The Markdown view contains a standard fence and source, without the title or preview image.

If browser storage is disabled or full, saving fails and the draft stays open. SVG / PNG downloads remain available. This example is a single-page local host, with no cloud sync or cross-tab collaboration.

## Keyboard and recovery

| Action | Shortcut |
| --- | --- |
| Indent / outdent | Tab / Shift-Tab |
| Leave the source pane | Esc, then Tab |
| Find / replace | Ctrl / ⌘ F |
| Undo | Ctrl / ⌘ Z |

The preview follows after a short delay. Composition must finish before rendering. Invalid, stale or pending previews cannot be saved. Correct the source or choose Refresh preview to retry without losing the original text.

See the [template catalog](/en/reference/templates) for syntax starting points and [save and restore](/en/guide/persistence) for the host contract.
