diff --git a/client/web/src/App.vue b/client/web/src/App.vue
index cbf729e0..53d123db 100644
--- a/client/web/src/App.vue
+++ b/client/web/src/App.vue
@@ -217,7 +217,7 @@ img {
diff --git a/client/web/src/utilities/document.ts b/client/web/src/utilities/document.ts
deleted file mode 100644
index 0f89cc18..00000000
--- a/client/web/src/utilities/document.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { reactive, readonly } from "vue";
-
-const state = reactive({
- title: "",
- unsaved: false,
-});
-
-export function setDocumentTitle(title: string) {
- state.title = title;
-}
-
-export function setUnsavedState(isUnsaved: boolean) {
- state.unsaved = isUnsaved;
-}
-
-export function documentTitle(): string {
- return state.title;
-}
-
-export function documentIsUnsaved(): boolean {
- return state.unsaved;
-}
-
-export default readonly(state);
diff --git a/client/web/src/utilities/documents.ts b/client/web/src/utilities/documents.ts
new file mode 100644
index 00000000..2806904c
--- /dev/null
+++ b/client/web/src/utilities/documents.ts
@@ -0,0 +1,97 @@
+import { reactive, readonly } from "vue";
+
+import { createDialog, dismissDialog } from "@/utilities/dialog";
+import { ResponseType, registerResponseHandler, Response, SetActiveDocument, UpdateOpenDocumentsList, DisplayConfirmationToCloseDocument } from "@/utilities/response-handler";
+
+const wasm = import("@/../wasm/pkg");
+
+const state = reactive({
+ title: "",
+ unsaved: false,
+ documents: [] as Array,
+ activeDocumentIndex: 0,
+});
+
+export async function selectDocument(tabIndex: number) {
+ const { select_document } = await wasm;
+ select_document(tabIndex);
+}
+
+export async function closeDocumentWithConfirmation(tabIndex: number) {
+ selectDocument(tabIndex);
+
+ const tabLabel = state.documents[tabIndex];
+
+ // TODO: Rename to "Save changes before closing?" when we can actually save documents somewhere, not just export SVGs
+ createDialog("File", "Close without exporting SVG?", tabLabel, [
+ {
+ kind: "TextButton",
+ callback: async () => {
+ (await wasm).export_document();
+ dismissDialog();
+ },
+ props: { label: "Export", emphasized: true, minWidth: 96 },
+ },
+ {
+ kind: "TextButton",
+ callback: async () => {
+ (await wasm).close_document(tabIndex);
+ dismissDialog();
+ },
+ props: { label: "Discard", minWidth: 96 },
+ },
+ {
+ kind: "TextButton",
+ callback: async () => {
+ dismissDialog();
+ },
+ props: { label: "Cancel", minWidth: 96 },
+ },
+ ]);
+}
+
+export async function closeAllDocumentsWithConfirmation() {
+ createDialog("Copy", "Close all documents?", "Unsaved work will be lost!", [
+ {
+ kind: "TextButton",
+ callback: async () => {
+ (await wasm).close_all_documents();
+ dismissDialog();
+ },
+ props: { label: "Discard All", minWidth: 96 },
+ },
+ {
+ kind: "TextButton",
+ callback: async () => {
+ dismissDialog();
+ },
+ props: { label: "Cancel", minWidth: 96 },
+ },
+ ]);
+}
+
+export default readonly(state);
+
+registerResponseHandler(ResponseType.UpdateOpenDocumentsList, (responseData: Response) => {
+ const documentListData = responseData as UpdateOpenDocumentsList;
+ if (documentListData) {
+ state.documents = documentListData.open_documents;
+ state.title = state.documents[state.activeDocumentIndex];
+ }
+});
+registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
+ const documentData = responseData as SetActiveDocument;
+ if (documentData) {
+ state.activeDocumentIndex = documentData.document_index;
+ state.title = state.documents[state.activeDocumentIndex];
+ }
+});
+registerResponseHandler(ResponseType.DisplayConfirmationToCloseDocument, (responseData: Response) => {
+ const data = responseData as DisplayConfirmationToCloseDocument;
+ closeDocumentWithConfirmation(data.document_index);
+});
+registerResponseHandler(ResponseType.DisplayConfirmationToCloseAllDocuments, (_responseData: Response) => {
+ closeAllDocumentsWithConfirmation();
+});
+
+(async () => (await wasm).get_open_documents_list())();