diff --git a/docs/dist/v1/chartifact-toolbar.css b/docs/dist/v1/chartifact-toolbar.css
index 15abb1d7..6000df51 100644
--- a/docs/dist/v1/chartifact-toolbar.css
+++ b/docs/dist/v1/chartifact-toolbar.css
@@ -24,8 +24,25 @@
/* Base styles for the toolbar */
.chartifact-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
padding: 0.5em 1em;
- font-weight: bold;
+ font-family: sans-serif;
+}
+
+.chartifact-toolbar a {
+ text-decoration: none;
+ color: inherit;
+}
+
+.chartifact-toolbar button, .chartifact-toolbar select {
+ font-size: 9pt;
+}
+
+/* Remove margin from all toolbar divs by default */
+.chartifact-toolbar > div {
+ display: inline-block;
}
/* Default (OS light theme) */
@@ -37,7 +54,7 @@
/* OS dark theme fallback */
@media (prefers-color-scheme: dark) {
.chartifact-toolbar {
- background-color: #1e1e1e;
+ background-color: #2e2e2e;
color: #ffffff;
border-bottom-color: rgba(255, 255, 255, 0.1);
}
diff --git a/docs/dist/v1/chartifact.host.umd.js b/docs/dist/v1/chartifact.host.umd.js
index 40869953..08edcfa2 100644
--- a/docs/dist/v1/chartifact.host.umd.js
+++ b/docs/dist/v1/chartifact.host.umd.js
@@ -907,7 +907,7 @@ ${guardedJs}
return;
}
let docIndex = 0;
- const folderSpan = host.toolbar.toolbarElement.querySelector("#folderSpan");
+ const { folderSpan } = host.toolbar;
folderSpan.style.display = "";
folderSpan.innerText = `Folder: ${folder.title} (${folder.docs.length} documents)`;
const prevBtn = document.createElement("button");
@@ -941,7 +941,8 @@ ${guardedJs}
prevBtn.disabled = docIndex === 0;
nextBtn.disabled = docIndex === folder.docs.length - 1;
pageSelect.value = (docIndex + 1).toString();
- resolveUrl(folderUrl, folder.docs[docIndex].href, host);
+ const title = folder.docs[docIndex].title || `Page ${docIndex + 1}`;
+ resolveUrl(title, folderUrl, folder.docs[docIndex].href, host);
}
pageSelect.onchange = () => {
const selectedPage = parseInt(pageSelect.value, 10);
@@ -979,7 +980,7 @@ ${guardedJs}
folderSpan.appendChild(pageSelect);
folderSpan.appendChild(nextBtn);
}
- async function resolveUrl(base, relativeOrAbsolute, host) {
+ async function resolveUrl(title, base, relativeOrAbsolute, host) {
let url;
try {
url = base ? new URL(relativeOrAbsolute, base).href : relativeOrAbsolute;
@@ -999,11 +1000,11 @@ ${guardedJs}
return;
}
if (result.idoc) {
- host.render(void 0, result.idoc);
+ host.render(title, void 0, result.idoc);
} else if (result.markdown) {
- host.render(result.markdown, void 0);
+ host.render(title, result.markdown, void 0);
} else if (result.folder) {
- host.render("Nested folders are not supported", void 0);
+ host.render("Error", "Nested folders are not supported", void 0);
} else {
host.errorHandler(
"Invalid document format",
@@ -1011,7 +1012,7 @@ ${guardedJs}
);
}
}
- function determineContent(url, content, host, handle) {
+ function determineContent(urlOrTitle, content, host, handle) {
const result = _determineContent(content);
if (handle) {
if (result.error) {
@@ -1021,16 +1022,16 @@ ${guardedJs}
);
return;
} else if (result.idoc) {
- host.render(void 0, result.idoc);
+ host.render(urlOrTitle, void 0, result.idoc);
} else if (result.folder) {
- loadFolder(url, result.folder, host);
+ loadFolder(urlOrTitle, result.folder, host);
} else if (result.markdown) {
- host.render(result.markdown, void 0);
+ host.render(urlOrTitle, result.markdown, void 0);
}
}
return result;
}
- function _determineContent(content, host) {
+ function _determineContent(content) {
if (!content) {
return {
error: "Content is empty",
@@ -1107,7 +1108,7 @@ ${guardedJs}
);
return;
}
- determineContent(null, content, host, true);
+ determineContent(file.name, content, host, true);
};
reader.onerror = (e) => {
host.errorHandler(
@@ -1152,7 +1153,7 @@ ${guardedJs}
);
return;
}
- determineContent(null, content, host, true);
+ determineContent("clipboard-content", content, host, true);
});
handled = true;
break;
@@ -1206,7 +1207,7 @@ ${guardedJs}
);
return;
}
- determineContent(null, content, host, true);
+ determineContent("dropped-content", content, host, true);
} else {
host.errorHandler(
"Unsupported drop content",
@@ -1259,9 +1260,9 @@ ${guardedJs}
const message = event.data;
if (message.type == "hostRenderRequest") {
if (message.markdown) {
- host.render(message.markdown, void 0);
+ host.render(message.title, message.markdown, void 0);
} else if (message.interactiveDocument) {
- host.render(void 0, message.interactiveDocument);
+ host.render(message.title, void 0, message.interactiveDocument);
} else {
}
}
@@ -1394,7 +1395,7 @@ ${guardedJs}
${message}
${details}`;
- this.render(markdown, void 0);
+ this.render("Error", markdown, void 0);
} else {
this.previewDiv.innerHTML = "";
const h1 = document.createElement("h1");
@@ -1408,7 +1409,10 @@ ${details}`;
this.previewDiv.appendChild(pDetails);
}
}
- render(markdown, interactiveDocument) {
+ render(title, markdown, interactiveDocument) {
+ if (this.toolbar) {
+ this.toolbar.filename = title;
+ }
if (interactiveDocument) {
this.onSetMode("json", null, interactiveDocument);
this.renderInteractiveDocument(interactiveDocument);
@@ -1461,26 +1465,69 @@ ${details}`;
constructor(toolbarElementOrSelector, options = {}) {
__publicField(this, "options");
__publicField(this, "toolbarElement");
+ __publicField(this, "folderSpan");
+ __publicField(this, "tweakButton");
+ __publicField(this, "downloadButton");
+ __publicField(this, "mode");
+ __publicField(this, "filename");
+ var _a, _b;
this.options = options;
+ this.filename = options.filename || "sample";
+ this.mode = options.mode || "markdown";
this.toolbarElement = typeof toolbarElementOrSelector === "string" ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
if (!this.toolbarElement) {
throw new Error("Toolbar element not found");
}
- const html = `Chartifact viewer
-
-
+ const html = `
+
+
+
+
+
+
`;
this.toolbarElement.innerHTML = html;
+ this.tweakButton = this.toolbarElement.querySelector("#tweak");
+ this.folderSpan = this.toolbarElement.querySelector("#folderSpan");
+ this.downloadButton = this.toolbarElement.querySelector("#download");
if (this.options.tweakButton) {
this.showTweakButton();
}
- }
- showTweakButton() {
- const tweakButton = this.toolbarElement.querySelector("#tweak");
- tweakButton.style.display = "";
- tweakButton == null ? void 0 : tweakButton.addEventListener("click", () => {
+ if (this.options.downloadButton) {
+ this.showDownloadButton();
+ }
+ (_a = this.tweakButton) == null ? void 0 : _a.addEventListener("click", () => {
this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none";
});
+ (_b = this.downloadButton) == null ? void 0 : _b.addEventListener("click", () => {
+ const textarea = this.options.textarea;
+ if (!textarea)
+ return;
+ const content = textarea.value;
+ const ext = this.mode === "markdown" ? "idoc.md" : "idoc.json";
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}.${ext}`;
+ const blob = new Blob([content], {
+ type: this.mode === "markdown" ? "text/markdown" : "application/json"
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 0);
+ });
+ }
+ showTweakButton() {
+ this.tweakButton.style.display = "";
+ }
+ showDownloadButton() {
+ this.downloadButton.style.display = "";
}
manageTextareaVisibilityForAgents() {
const { textarea } = this.options;
@@ -1498,6 +1545,18 @@ ${details}`;
}, 300);
}
}
+ function filenameWithoutPathOrExtension(filename) {
+ const base = filename.split(/[\\/]/).pop() || filename;
+ const idocIdx = base.indexOf(".idoc");
+ if (idocIdx !== -1) {
+ return base.substring(0, idocIdx);
+ }
+ const lastDot = base.lastIndexOf(".");
+ if (lastDot > 0) {
+ return base.substring(0, lastDot);
+ }
+ return base;
+ }
function create(toolbarElementOrSelector, options = {}) {
const toolbar = new Toolbar(toolbarElementOrSelector, options);
toolbar.manageTextareaVisibilityForAgents();
diff --git a/docs/dist/v1/chartifact.markdown.umd.js b/docs/dist/v1/chartifact.markdown.umd.js
index 13d144d6..0831c7de 100644
--- a/docs/dist/v1/chartifact.markdown.umd.js
+++ b/docs/dist/v1/chartifact.markdown.umd.js
@@ -2202,6 +2202,7 @@ ${reconstitutedRules.join("\n\n")}
}
}
async beginListening() {
+ this.active = true;
this.log("beginListening", "begin initial batch", this.signalDeps);
for (const peer of this.peers) {
const batch = {};
@@ -2246,7 +2247,6 @@ ${reconstitutedRules.join("\n\n")}
this.log(peer.id, "No shared signals");
}
}
- this.active = true;
}
deactivate() {
if (this.signalDeps) {
diff --git a/docs/dist/v1/chartifact.sandbox.umd.js b/docs/dist/v1/chartifact.sandbox.umd.js
index f4a5a234..7479aa32 100644
--- a/docs/dist/v1/chartifact.sandbox.umd.js
+++ b/docs/dist/v1/chartifact.sandbox.umd.js
@@ -322,26 +322,69 @@ document.addEventListener('DOMContentLoaded', () => {
constructor(toolbarElementOrSelector, options = {}) {
__publicField(this, "options");
__publicField(this, "toolbarElement");
+ __publicField(this, "folderSpan");
+ __publicField(this, "tweakButton");
+ __publicField(this, "downloadButton");
+ __publicField(this, "mode");
+ __publicField(this, "filename");
+ var _a, _b;
this.options = options;
+ this.filename = options.filename || "sample";
+ this.mode = options.mode || "markdown";
this.toolbarElement = typeof toolbarElementOrSelector === "string" ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
if (!this.toolbarElement) {
throw new Error("Toolbar element not found");
}
- const html = `Chartifact viewer
-
-
+ const html = `
+
+
+
+
+
+
`;
this.toolbarElement.innerHTML = html;
+ this.tweakButton = this.toolbarElement.querySelector("#tweak");
+ this.folderSpan = this.toolbarElement.querySelector("#folderSpan");
+ this.downloadButton = this.toolbarElement.querySelector("#download");
if (this.options.tweakButton) {
this.showTweakButton();
}
- }
- showTweakButton() {
- const tweakButton = this.toolbarElement.querySelector("#tweak");
- tweakButton.style.display = "";
- tweakButton == null ? void 0 : tweakButton.addEventListener("click", () => {
+ if (this.options.downloadButton) {
+ this.showDownloadButton();
+ }
+ (_a = this.tweakButton) == null ? void 0 : _a.addEventListener("click", () => {
this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none";
});
+ (_b = this.downloadButton) == null ? void 0 : _b.addEventListener("click", () => {
+ const textarea = this.options.textarea;
+ if (!textarea)
+ return;
+ const content = textarea.value;
+ const ext = this.mode === "markdown" ? "idoc.md" : "idoc.json";
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}.${ext}`;
+ const blob = new Blob([content], {
+ type: this.mode === "markdown" ? "text/markdown" : "application/json"
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 0);
+ });
+ }
+ showTweakButton() {
+ this.tweakButton.style.display = "";
+ }
+ showDownloadButton() {
+ this.downloadButton.style.display = "";
}
manageTextareaVisibilityForAgents() {
const { textarea } = this.options;
@@ -359,6 +402,18 @@ document.addEventListener('DOMContentLoaded', () => {
}, 300);
}
}
+ function filenameWithoutPathOrExtension(filename) {
+ const base = filename.split(/[\\/]/).pop() || filename;
+ const idocIdx = base.indexOf(".idoc");
+ if (idocIdx !== -1) {
+ return base.substring(0, idocIdx);
+ }
+ const lastDot = base.lastIndexOf(".");
+ if (lastDot > 0) {
+ return base.substring(0, lastDot);
+ }
+ return base;
+ }
function create(toolbarElementOrSelector, options = {}) {
const toolbar = new Toolbar(toolbarElementOrSelector, options);
toolbar.manageTextareaVisibilityForAgents();
diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts
index feb53550..3cbb23a7 100644
--- a/packages/common/src/messages.ts
+++ b/packages/common/src/messages.ts
@@ -31,6 +31,7 @@ export type SandboxApprovalMessage = {
export interface HostRenderRequestMessage {
type: 'hostRenderRequest';
+ title: string;
markdown?: string;
interactiveDocument?: InteractiveDocument
}
diff --git a/packages/host-test/src/index.ts b/packages/host-test/src/index.ts
index e5b53519..78c8c03a 100644
--- a/packages/host-test/src/index.ts
+++ b/packages/host-test/src/index.ts
@@ -9,6 +9,7 @@ window.addEventListener('message', (event) => {
if (hostStatusMessage.type === 'hostStatus' && hostStatusMessage.hostStatus === 'ready') {
const message: Chartifact.common.HostRenderRequestMessage = {
type: 'hostRenderRequest',
+ title: 'sample',
markdown: `# Auto-loaded Content
This markdown was automatically sent when the iframe became ready.
diff --git a/packages/host/dev/index.ts b/packages/host/dev/index.ts
index 0b1ee11d..2e624bbe 100644
--- a/packages/host/dev/index.ts
+++ b/packages/host/dev/index.ts
@@ -50,6 +50,7 @@ const host = new Listener({
onSetMode: (mode, markdown, interactiveDocument) => {
switch (mode) {
case 'json':
+ toolbar.mode = 'json';
textarea.value = JSON.stringify(interactiveDocument, null, 2);
render = () => {
const json = textarea.value;
@@ -72,6 +73,7 @@ const host = new Listener({
};
break;
case 'markdown':
+ toolbar.mode = 'markdown';
textarea.value = markdown;
render = () => {
const markdown = textarea.value;
@@ -82,5 +84,6 @@ const host = new Listener({
return;
}
toolbar.showTweakButton();
+ toolbar.showDownloadButton();
},
});
diff --git a/packages/host/src/clipboard.ts b/packages/host/src/clipboard.ts
index 4ee4e023..3dbdb8ed 100644
--- a/packages/host/src/clipboard.ts
+++ b/packages/host/src/clipboard.ts
@@ -38,7 +38,7 @@ export function setupClipboardHandling(host: Listener) {
);
return;
}
- determineContent(null, content, host, true);
+ determineContent('clipboard-content', content, host, true);
});
handled = true;
break;
diff --git a/packages/host/src/dragdrop.ts b/packages/host/src/dragdrop.ts
index 113ce1b5..3650d639 100644
--- a/packages/host/src/dragdrop.ts
+++ b/packages/host/src/dragdrop.ts
@@ -39,7 +39,7 @@ export function setupDragDropHandling(host: Listener) {
);
return;
}
- determineContent(null, content, host, true);
+ determineContent('dropped-content', content, host, true);
} else {
host.errorHandler(
'Unsupported drop content',
diff --git a/packages/host/src/file.ts b/packages/host/src/file.ts
index 6f9ea48a..8ad16653 100644
--- a/packages/host/src/file.ts
+++ b/packages/host/src/file.ts
@@ -25,7 +25,7 @@ export function readFile(file: File, host: Listener) {
);
return;
}
- determineContent(null, content, host, true);
+ determineContent(file.name, content, host, true);
};
reader.onerror = (e) => {
host.errorHandler(
diff --git a/packages/host/src/folder.ts b/packages/host/src/folder.ts
index 075ea4e1..76cb8567 100644
--- a/packages/host/src/folder.ts
+++ b/packages/host/src/folder.ts
@@ -34,7 +34,7 @@ export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
let docIndex = 0;
- const folderSpan = host.toolbar.toolbarElement.querySelector('#folderSpan') as HTMLSpanElement;
+ const { folderSpan } = host.toolbar;
folderSpan.style.display = '';
folderSpan.innerText = `Folder: ${folder.title} (${folder.docs.length} documents)`;
@@ -77,7 +77,9 @@ export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
prevBtn.disabled = docIndex === 0;
nextBtn.disabled = docIndex === folder.docs.length - 1;
pageSelect.value = (docIndex + 1).toString();
- resolveUrl(folderUrl, folder.docs[docIndex].href, host);
+
+ const title = folder.docs[docIndex].title || `Page ${docIndex + 1}`;
+ resolveUrl(title, folderUrl, folder.docs[docIndex].href, host);
}
pageSelect.onchange = () => {
@@ -129,7 +131,7 @@ export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
//resolveUrl(folderUrl, folder.docUrls[docIndex], host);
}
-async function resolveUrl(base: string, relativeOrAbsolute: string, host: Listener) {
+async function resolveUrl(title: string, base: string, relativeOrAbsolute: string, host: Listener) {
let url: string;
try {
url = base ? new URL(relativeOrAbsolute, base).href : relativeOrAbsolute;
@@ -149,11 +151,11 @@ async function resolveUrl(base: string, relativeOrAbsolute: string, host: Listen
return;
}
if (result.idoc) {
- host.render(undefined, result.idoc);
+ host.render(title, undefined, result.idoc);
} else if (result.markdown) {
- host.render(result.markdown, undefined);
+ host.render(title, result.markdown, undefined);
} else if (result.folder) {
- host.render('Nested folders are not supported', undefined);
+ host.render('Error', 'Nested folders are not supported', undefined);
} else {
host.errorHandler(
'Invalid document format',
diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts
index 6d50a850..d66e0690 100644
--- a/packages/host/src/listener.ts
+++ b/packages/host/src/listener.ts
@@ -165,7 +165,7 @@ export class Listener {
//try to show the message in the sandbox, since it works well with paging folder content
if (this.sandboxReady) {
const markdown = `# Error:\n${message}\n\n${details}`;
- this.render(markdown, undefined);
+ this.render('Error', markdown, undefined);
} else {
// Clear previous content
this.previewDiv.innerHTML = '';
@@ -181,7 +181,10 @@ export class Listener {
}
}
- public render(markdown?: string, interactiveDocument?: InteractiveDocument) {
+ public render(title: string, markdown: string | null, interactiveDocument: InteractiveDocument | null) {
+ if (this.toolbar) {
+ this.toolbar.filename = title;
+ }
if (interactiveDocument) {
this.onSetMode('json', null, interactiveDocument);
this.renderInteractiveDocument(interactiveDocument);
diff --git a/packages/host/src/post-receive.ts b/packages/host/src/post-receive.ts
index f5d095f2..6abb1a82 100644
--- a/packages/host/src/post-receive.ts
+++ b/packages/host/src/post-receive.ts
@@ -20,9 +20,9 @@ export function setupPostMessageHandling(host: Listener) {
const message = event.data as HostRenderRequestMessage;
if (message.type == 'hostRenderRequest') {
if (message.markdown) {
- host.render(message.markdown, undefined);
+ host.render( message.title, message.markdown, undefined);
} else if (message.interactiveDocument) {
- host.render(undefined, message.interactiveDocument);
+ host.render( message.title, undefined, message.interactiveDocument);
} else {
//do nothing, as messages may be directed to the page for other purposes
}
diff --git a/packages/host/src/string.ts b/packages/host/src/string.ts
index 87e560d1..459f8ed6 100644
--- a/packages/host/src/string.ts
+++ b/packages/host/src/string.ts
@@ -15,8 +15,8 @@ export interface ContentResult {
errorDetail?: string;
}
-export function determineContent(url:string, content: string, host: Listener, handle: boolean): ContentResult {
- const result = _determineContent(content, host);
+export function determineContent(urlOrTitle: string, content: string, host: Listener, handle: boolean): ContentResult {
+ const result = _determineContent(content);
if (handle) {
if (result.error) {
host.errorHandler(
@@ -25,17 +25,17 @@ export function determineContent(url:string, content: string, host: Listener, ha
);
return;
} else if (result.idoc) {
- host.render(undefined, result.idoc);
+ host.render(urlOrTitle, undefined, result.idoc);
} else if (result.folder) {
- loadFolder(url, result.folder, host);
+ loadFolder(urlOrTitle, result.folder, host);
} else if (result.markdown) {
- host.render(result.markdown, undefined);
+ host.render(urlOrTitle, result.markdown, undefined);
}
}
return result;
}
-function _determineContent(content: string, host: Listener): ContentResult {
+function _determineContent(content: string): ContentResult {
if (!content) {
return {
error: 'Content is empty',
diff --git a/packages/toolbar/chartifact-toolbar.css b/packages/toolbar/chartifact-toolbar.css
index 15abb1d7..6000df51 100644
--- a/packages/toolbar/chartifact-toolbar.css
+++ b/packages/toolbar/chartifact-toolbar.css
@@ -24,8 +24,25 @@
/* Base styles for the toolbar */
.chartifact-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
padding: 0.5em 1em;
- font-weight: bold;
+ font-family: sans-serif;
+}
+
+.chartifact-toolbar a {
+ text-decoration: none;
+ color: inherit;
+}
+
+.chartifact-toolbar button, .chartifact-toolbar select {
+ font-size: 9pt;
+}
+
+/* Remove margin from all toolbar divs by default */
+.chartifact-toolbar > div {
+ display: inline-block;
}
/* Default (OS light theme) */
@@ -37,7 +54,7 @@
/* OS dark theme fallback */
@media (prefers-color-scheme: dark) {
.chartifact-toolbar {
- background-color: #1e1e1e;
+ background-color: #2e2e2e;
color: #ffffff;
border-bottom-color: rgba(255, 255, 255, 0.1);
}
diff --git a/packages/toolbar/src/toolbar.ts b/packages/toolbar/src/toolbar.ts
index 11d4b8d6..449b8ada 100644
--- a/packages/toolbar/src/toolbar.ts
+++ b/packages/toolbar/src/toolbar.ts
@@ -5,37 +5,85 @@
export interface ToolbarOptions {
tweakButton?: boolean;
+ downloadButton?: boolean;
textarea?: HTMLTextAreaElement;
+ mode?: 'markdown' | 'json';
+ filename?: string;
}
export class Toolbar {
public toolbarElement: HTMLElement;
+ public folderSpan: HTMLElement;
+ public tweakButton: HTMLButtonElement;
+ public downloadButton: HTMLButtonElement;
+ public mode: 'markdown' | 'json';
+ public filename: string;
constructor(toolbarElementOrSelector: HTMLElement | string, public options: ToolbarOptions = {}) {
+ this.filename = options.filename || 'sample';
+ this.mode = options.mode || 'markdown';
this.toolbarElement = typeof toolbarElementOrSelector === 'string' ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
if (!this.toolbarElement) {
throw new Error('Toolbar element not found');
}
- const html = `Chartifact viewer
-
-
+ const html = `
+
+
+
+
+
+
`;
this.toolbarElement.innerHTML = html;
+ this.tweakButton = this.toolbarElement.querySelector('#tweak') as HTMLButtonElement;
+ this.folderSpan = this.toolbarElement.querySelector('#folderSpan') as HTMLElement;
+ this.downloadButton = this.toolbarElement.querySelector('#download') as HTMLButtonElement;
+
if (this.options.tweakButton) {
this.showTweakButton();
}
- }
+ if (this.options.downloadButton) {
+ this.showDownloadButton();
+ }
- showTweakButton() {
- const tweakButton = this.toolbarElement.querySelector('#tweak') as HTMLButtonElement;
- tweakButton.style.display = '';
- tweakButton?.addEventListener('click', () => {
+ this.tweakButton?.addEventListener('click', () => {
this.options.textarea.style.display = this.options.textarea.style.display === 'none' ? '' : 'none';
});
+
+ this.downloadButton?.addEventListener('click', () => {
+ const textarea = this.options.textarea;
+ if (!textarea) return;
+ const content = textarea.value;
+ const ext = this.mode === 'markdown' ? 'idoc.md' : 'idoc.json';
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}.${ext}`;
+ const blob = new Blob([content], {
+ type: this.mode === 'markdown' ? 'text/markdown' : 'application/json'
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 0);
+ });
+ }
+
+ showTweakButton() {
+ this.tweakButton.style.display = '';
+ }
+
+ showDownloadButton() {
+ this.downloadButton.style.display = '';
}
manageTextareaVisibilityForAgents() {
@@ -62,3 +110,47 @@ export class Toolbar {
}
}
+
+function filenameWithoutPathOrExtension(filename: string) {
+ // Remove everything before the last slash or backslash
+ const base = filename.split(/[\\/]/).pop() || filename;
+ // If .idoc appears, remove it and everything after
+ const idocIdx = base.indexOf('.idoc');
+ if (idocIdx !== -1) {
+ return base.substring(0, idocIdx);
+ }
+ // Otherwise, remove the last extension if present
+ const lastDot = base.lastIndexOf('.');
+ if (lastDot > 0) {
+ return base.substring(0, lastDot);
+ }
+ return base;
+}
+
+// --- TESTS for filenameWithoutPathOrExtension ---
+
+// const filenameTests: [string, string][] = [
+// // [input, expected]
+// ["foo.md", "foo"],
+// ["foo.idoc.md", "foo"],
+// ["foo.idoc.json", "foo"],
+// ["foo", "foo"],
+// ["foo.bar.baz.md", "foo.bar.baz"],
+// ["C:\\folder\\foo.md", "foo"],
+// ["/home/user/foo.md", "foo"],
+// ["folder/foo.idoc.md", "foo"],
+// ["folder\\foo.idoc.json", "foo"],
+// ["folder.with.dots/foo.bar.baz.md", "foo.bar.baz"],
+// ["folder.with.dots\\foo.bar.baz.md", "foo.bar.baz"],
+// ["", ""],
+// ];
+
+// filenameTests.forEach(([input, expected], i) => {
+// const result = filenameWithoutPathOrExtension(input);
+// const pass = result === expected;
+// // eslint-disable-next-line no-console
+// console.log(
+// `${pass ? '✅' : '❌'} Test ${i + 1}: ${pass ? 'PASS' : `FAIL\n Input: ${input}\n Got: ${result}\n Expected: ${expected}`}`
+// );
+// });
+
diff --git a/packages/vscode/src/web/command-preview.ts b/packages/vscode/src/web/command-preview.ts
index ea64dbf8..b2817961 100644
--- a/packages/vscode/src/web/command-preview.ts
+++ b/packages/vscode/src/web/command-preview.ts
@@ -111,11 +111,14 @@ export class PreviewManager {
private getFileContentAndRender(fileUri: vscode.Uri, uriFsPath: string) {
vscode.workspace.fs.readFile(fileUri).then(uint8array => {
+ const title = uriFsPath;
+
// If the file is a markdown file, we can send the markdown content
if (uriFsPath.endsWith('.md')) {
const markdown = new TextDecoder().decode(uint8array);
this.render({
type: 'hostRenderRequest',
+ title,
markdown,
});
} else if (uriFsPath.endsWith('.json')) {
@@ -125,6 +128,7 @@ export class PreviewManager {
const interactiveDocument = JSON.parse(jsonContent);
this.render({
type: 'hostRenderRequest',
+ title,
interactiveDocument,
});
} catch (error) {