From 8b7d524eca0a6ef0554e34b77cb4ca137a05b663 Mon Sep 17 00:00:00 2001 From: Harsh Vadhiya Date: Sat, 13 Jun 2026 12:46:19 +0530 Subject: [PATCH] feat: add code block copy button and popup markdown previewer with history --- background/messages.js | 36 +++++--- background/storage.js | 17 +++- content/index.css | 98 ++++++++++++++++++++ content/index.js | 53 +++++++++++ content/mathjax.js | 21 +++-- content/preview.html | 17 ++++ content/preview.js | 75 ++++++++++++++++ content/prism.js | 17 +++- manifest.chrome.json | 3 +- manifest.firefox.json | 3 +- popup/index.css | 153 ++++++++++++++++++++++++++++++-- popup/index.js | 197 ++++++++++++++++++++++++++++++++++++++++- 12 files changed, 652 insertions(+), 38 deletions(-) create mode 100644 content/preview.html create mode 100644 content/preview.js diff --git a/background/messages.js b/background/messages.js index 14a1603..12bd672 100644 --- a/background/messages.js +++ b/background/messages.js @@ -26,22 +26,30 @@ md.messages = ({storage: {defaults, state, set}, compilers, mathjax, xhr, webreq }) } else if (req.message === 'prism') { - chrome.scripting.executeScript({ - target: {tabId: sender.tab.id}, - files: [ - `/vendor/prism/prism-${req.language}.min.js`, - ], - injectImmediately: true - }, sendResponse) + if (sender.tab) { + chrome.scripting.executeScript({ + target: {tabId: sender.tab.id}, + files: [ + `/vendor/prism/prism-${req.language}.min.js`, + ], + injectImmediately: true + }, sendResponse) + } else { + sendResponse() + } } else if (req.message === 'mathjax') { - chrome.scripting.executeScript({ - target: {tabId: sender.tab.id}, - files: [ - `/vendor/mathjax/extensions/${req.extension}.js`, - ], - injectImmediately: true - }, sendResponse) + if (sender.tab) { + chrome.scripting.executeScript({ + target: {tabId: sender.tab.id}, + files: [ + `/vendor/mathjax/extensions/${req.extension}.js`, + ], + injectImmediately: true + }, sendResponse) + } else { + sendResponse() + } } // popup diff --git a/background/storage.js b/background/storage.js index 8f5e403..1a6911b 100644 --- a/background/storage.js +++ b/background/storage.js @@ -82,10 +82,19 @@ md.storage.bug = (res) => { // reload extension bug chrome.permissions.getAll((permissions) => { var origins = Object.keys(res.origins || {}) - chrome.permissions.remove({ - origins: permissions.origins - .filter((origin) => origins.indexOf(origin.slice(0, -2)) === -1) - }) + var originsToRemove = (permissions.origins || []) + .filter((origin) => origins.indexOf(origin.slice(0, -2)) === -1) + if (originsToRemove.length) { + var promise = chrome.permissions.remove({ + origins: originsToRemove + }, () => { + // Ignore error when trying to remove required permissions + var error = chrome.runtime.lastError + }) + if (promise && promise.catch) { + promise.catch(() => {}) + } + } }) } diff --git a/content/index.css b/content/index.css index 63b8dd9..7ee75ff 100644 --- a/content/index.css +++ b/content/index.css @@ -359,3 +359,101 @@ img.emojione { /* prevent img stretch */ width: auto; } + +/*---------------------------------------------------------------------------*/ +/*copy button*/ + +#_html pre { + position: relative; +} + +#_html pre .markdown-copy-button { + position: absolute; + top: 8px; + right: 8px; + opacity: 0; + transition: opacity 0.2s ease, background-color 0.2s ease, border-color 0.2s ease; + border: 1px solid var(--copy-btn-border); + background-color: var(--copy-btn-bg); + color: var(--copy-btn-color); + padding: 6px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + user-select: none; + -webkit-user-select: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* Show button on pre hover and focus */ +#_html pre:hover .markdown-copy-button, +#_html pre .markdown-copy-button:focus { + opacity: 1; +} + +#_html pre .markdown-copy-button:hover { + background-color: var(--copy-btn-bg-hover); + border-color: var(--copy-btn-border-hover); + color: var(--copy-btn-color-hover); +} + +#_html pre .markdown-copy-button.copied { + background-color: var(--copy-btn-bg-copied); + border-color: var(--copy-btn-border-copied); + color: var(--copy-btn-color-copied); +} + +/* Base Theme Variables */ +body { + --copy-btn-bg: rgba(255, 255, 255, 0.85); + --copy-btn-bg-hover: rgba(240, 240, 240, 0.95); + --copy-btn-border: #e1e4e8; + --copy-btn-border-hover: #b0b5bc; + --copy-btn-color: #57606a; + --copy-btn-color-hover: #24292f; + --copy-btn-bg-copied: #dcffe4; + --copy-btn-border-copied: #85e89d; + --copy-btn-color-copied: #22863a; +} + +@media (prefers-color-scheme: dark) { + body { + --copy-btn-bg: rgba(33, 38, 45, 0.85); + --copy-btn-bg-hover: rgba(48, 54, 61, 0.95); + --copy-btn-border: #30363d; + --copy-btn-border-hover: #8b949e; + --copy-btn-color: #8b949e; + --copy-btn-color-hover: #c9d1d9; + --copy-btn-bg-copied: rgba(56, 139, 253, 0.15); + --copy-btn-border-copied: rgba(56, 139, 253, 0.4); + --copy-btn-color-copied: #58a6ff; + } +} + +._color-light { + --copy-btn-bg: rgba(255, 255, 255, 0.85); + --copy-btn-bg-hover: rgba(240, 240, 240, 0.95); + --copy-btn-border: #e1e4e8; + --copy-btn-border-hover: #b0b5bc; + --copy-btn-color: #57606a; + --copy-btn-color-hover: #24292f; + --copy-btn-bg-copied: #dcffe4; + --copy-btn-border-copied: #85e89d; + --copy-btn-color-copied: #22863a; +} + +._color-dark { + --copy-btn-bg: rgba(33, 38, 45, 0.85); + --copy-btn-bg-hover: rgba(48, 54, 61, 0.95); + --copy-btn-border: #30363d; + --copy-btn-border-hover: #8b949e; + --copy-btn-color: #8b949e; + --copy-btn-color-hover: #c9d1d9; + --copy-btn-bg-copied: rgba(56, 139, 253, 0.15); + --copy-btn-border-copied: rgba(56, 139, 253, 0.4); + --copy-btn-color-copied: #58a6ff; +} + diff --git a/content/index.js b/content/index.js index b7cf50e..21ac864 100644 --- a/content/index.js +++ b/content/index.js @@ -110,6 +110,59 @@ var update = (update) => { if (state.content.mathjax) { setTimeout(() => mj.render(), 60) } + + setTimeout(() => addCopyButtons(), 80) +} + +var addCopyButtons = () => { + var preElements = document.querySelectorAll('#_html pre') + preElements.forEach((pre) => { + if (pre.querySelector('code.mermaid') || pre.querySelector('svg[id^=mermaid]')) { + return + } + if (pre.querySelector('.markdown-copy-button')) { + return + } + + var button = document.createElement('button') + button.className = 'markdown-copy-button' + button.type = 'button' + button.title = 'Copy code' + button.setAttribute('aria-label', 'Copy code') + + button.innerHTML = ` + + + + + + ` + + button.addEventListener('click', () => { + var codeElement = pre.querySelector('code') + var text = codeElement ? codeElement.innerText : pre.innerText + + navigator.clipboard.writeText(text).then(() => { + var copyIcon = button.querySelector('.copy-icon') + var checkIcon = button.querySelector('.check-icon') + copyIcon.style.display = 'none' + checkIcon.style.display = 'inline' + button.classList.add('copied') + + setTimeout(() => { + copyIcon.style.display = 'inline' + checkIcon.style.display = 'none' + button.classList.remove('copied') + }, 2000) + }).catch((err) => { + console.error('Failed to copy text: ', err) + }) + }) + + pre.appendChild(button) + }) } var render = (md) => { diff --git a/content/mathjax.js b/content/mathjax.js index d105b90..a360961 100644 --- a/content/mathjax.js +++ b/content/mathjax.js @@ -4,11 +4,22 @@ var MathJax = { pathFilters: [ ({name}) => name.startsWith('[tex]') ? false : true // keep the name ], - require: (path) => path.startsWith('[tex]') ? - chrome.runtime.sendMessage({ - message: 'mathjax', - extension: path.replace('[tex]/', '') - }) : null + require: (path) => { + if (path.startsWith('[tex]')) { + var extension = path.replace('[tex]/', '') + if (location.protocol.endsWith('-extension:')) { + var script = document.createElement('script') + script.src = `/vendor/mathjax/extensions/${extension}.js` + document.body.appendChild(script) + return null + } + return chrome.runtime.sendMessage({ + message: 'mathjax', + extension + }) + } + return null + } }, tex: { inlineMath: [ diff --git a/content/preview.html b/content/preview.html new file mode 100644 index 0000000..06bffbd --- /dev/null +++ b/content/preview.html @@ -0,0 +1,17 @@ + + + + + + Markdown Preview + + + +

+  
+
+
diff --git a/content/preview.js b/content/preview.js
new file mode 100644
index 0000000..99ab5bc
--- /dev/null
+++ b/content/preview.js
@@ -0,0 +1,75 @@
+;(async () => {
+  const localData = await chrome.storage.local.get('temporaryMarkdown')
+  const markdown = localData.temporaryMarkdown || ''
+
+  const pre = document.querySelector('pre')
+  pre.textContent = markdown
+
+  const state = await chrome.storage.sync.get()
+  
+  window.args = {
+    theme: state.theme,
+    raw: state.raw,
+    themes: state.themes,
+    content: state.content,
+    compiler: state.compiler,
+    custom: state.custom,
+    icon: state.settings.icon,
+  }
+
+  const loadCSS = (href) => {
+    return new Promise((resolve) => {
+      const link = document.createElement('link')
+      link.rel = 'stylesheet'
+      link.type = 'text/css'
+      link.href = href
+      link.onload = resolve
+      document.head.appendChild(link)
+    })
+  }
+
+  const loadScript = (src) => {
+    return new Promise((resolve, reject) => {
+      const script = document.createElement('script')
+      script.src = src
+      script.onload = resolve
+      script.onerror = reject
+      document.body.appendChild(script)
+    })
+  }
+
+  await Promise.all([
+    loadCSS('/content/index.css'),
+    loadCSS('/content/themes.css')
+  ])
+
+  await loadScript('/vendor/mithril.min.js')
+
+  if (window.args.content.syntax) {
+    await loadScript('/vendor/prism.min.js')
+    await loadScript('/vendor/prism-autoloader.min.js')
+    await loadScript('/content/prism.js')
+  }
+
+  if (window.args.content.emoji) {
+    await loadScript('/content/emoji.js')
+  }
+
+  if (window.args.content.mermaid) {
+    await loadScript('/vendor/mermaid.min.js')
+    await loadScript('/vendor/panzoom.min.js')
+    await loadScript('/content/mermaid.js')
+  }
+
+  if (window.args.content.mathjax) {
+    await loadScript('/content/mathjax.js')
+    await loadScript('/vendor/mathjax/tex-mml-chtml.js')
+  }
+
+  await loadScript('/content/index.js')
+  await loadScript('/content/scroll.js')
+
+  if (window.args.content.autoreload) {
+    await loadScript('/content/autoreload.js')
+  }
+})()
diff --git a/content/prism.js b/content/prism.js
index e615315..6b9effe 100644
--- a/content/prism.js
+++ b/content/prism.js
@@ -1,7 +1,16 @@
 
 Prism.plugins.autoloader.addScript = (language, done) => {
-  chrome.runtime.sendMessage({
-    message: 'prism',
-    language
-  }, done)
+  if (location.protocol.endsWith('-extension:')) {
+    var script = document.createElement('script')
+    script.src = `/vendor/prism/prism-${language}.min.js`
+    script.onload = () => done()
+    script.onerror = () => done()
+    document.body.appendChild(script)
+  }
+  else {
+    chrome.runtime.sendMessage({
+      message: 'prism',
+      language
+    }, done)
+  }
 }
diff --git a/manifest.chrome.json b/manifest.chrome.json
index 7f52256..9a93aef 100644
--- a/manifest.chrome.json
+++ b/manifest.chrome.json
@@ -52,7 +52,8 @@
 
   "permissions": [
     "storage",
-    "scripting"
+    "scripting",
+    "activeTab"
   ],
 
   "host_permissions": [
diff --git a/manifest.firefox.json b/manifest.firefox.json
index 8a0a721..cf325df 100644
--- a/manifest.firefox.json
+++ b/manifest.firefox.json
@@ -74,7 +74,8 @@
 
   "permissions": [
     "storage",
-    "scripting"
+    "scripting",
+    "activeTab"
   ],
 
   "host_permissions": [
diff --git a/popup/index.css b/popup/index.css
index 15542f3..0610617 100644
--- a/popup/index.css
+++ b/popup/index.css
@@ -67,9 +67,9 @@ body.dark .m-button {
 /*button*/
 
 /*defaults button*/
-#popup button:nth-child(2) { float: right; }
+#popup > button:nth-child(2) { float: right; }
 /*advanced options button*/
-#popup button:nth-child(2n+3) { float: right; margin-top: 20px; }
+#popup > button:nth-child(2n+3) { float: right; margin-top: 20px; }
 
 .m-button {
   background-color: #ececec !important;
@@ -145,14 +145,16 @@ body.dark .m-button {
   line-height: 36px;
   border-bottom: 1px solid #e0e0e0;
   display: inline-block;
-  width: 33.33%;
-  max-width: 33.33%;
-  min-width: 33.33%;
+  width: 20%;
+  max-width: 20%;
+  min-width: 20%;
   height: 36px;
   max-height: 36px;
   min-height: 36px;
   padding: 0;
   align-content: stretch;
+  font-size: 11px !important;
+  text-align: center;
 }
 .m-tabs .mdc-tab-bar__indicator { background: #607d8b; }
 
@@ -160,6 +162,147 @@ body.dark .m-button {
 .m-panel { display: none; }
 .m-panel.is-active { display: block; }
 
+/* preview tab styling */
+.preview-textarea {
+  width: 100%;
+  height: 180px;
+  box-sizing: border-box;
+  margin-top: 15px;
+  padding: 10px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  font-family: monospace;
+  font-size: 13px;
+  resize: vertical;
+  background-color: #f9f9f9;
+  color: #333;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+.preview-textarea:focus {
+  outline: none;
+  border-color: #607d8b;
+  box-shadow: 0 0 0 2px rgba(96, 125, 139, 0.25);
+}
+
+/* preview controls layout */
+.preview-controls {
+  display: flex;
+  gap: 10px;
+  margin-top: 15px;
+}
+.clear-btn {
+  flex: 1;
+  background-color: #f5f5f5 !important;
+  color: #333 !important;
+  margin-top: 0 !important;
+}
+.preview-btn-half {
+  flex: 1;
+  margin-top: 0 !important;
+}
+
+/* history panel styling */
+.history-scroll {
+  max-height: 250px;
+  margin-top: 15px;
+}
+.history-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 10px;
+  border-bottom: 1px solid #eee;
+  font-family: sans-serif !important;
+  transition: background-color 0.2s ease;
+}
+.history-item:hover {
+  background-color: #f7f7f7;
+}
+.history-info {
+  flex: 1;
+  min-width: 0;
+}
+.history-title {
+  font-size: 13px;
+  font-weight: bold;
+  color: #333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.history-time {
+  font-size: 11px;
+  color: #888;
+  margin-top: 2px;
+}
+.history-actions {
+  display: flex;
+  gap: 3px;
+  margin-left: 10px;
+}
+.action-btn {
+  border: none;
+  background: none;
+  font-size: 16px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 4px;
+  transition: background-color 0.2s ease, color 0.2s ease;
+  line-height: 1;
+}
+.reopen-btn {
+  color: #607d8b;
+}
+.reopen-btn:hover {
+  background-color: rgba(96, 125, 139, 0.15);
+}
+.rename-btn {
+  color: #ff9800;
+}
+.rename-btn:hover {
+  background-color: rgba(255, 152, 0, 0.15);
+}
+.delete-btn {
+  color: #d32f2f;
+}
+.delete-btn:hover {
+  background-color: rgba(211, 47, 47, 0.15);
+}
+.save-btn {
+  color: #2e7d32;
+}
+.save-btn:hover {
+  background-color: rgba(46, 125, 50, 0.15);
+}
+.discard-btn {
+  color: #c62828;
+}
+.discard-btn:hover {
+  background-color: rgba(198, 40, 40, 0.15);
+}
+.history-empty {
+  text-align: center;
+  padding: 30px;
+  color: #999;
+  font-size: 14px;
+  font-family: sans-serif !important;
+}
+
+/* inline rename input styling */
+.edit-title-input {
+  font-size: 13px;
+  padding: 2px 5px;
+  border: 1px solid #607d8b;
+  border-radius: 4px;
+  width: 100%;
+  box-sizing: border-box;
+}
+.edit-title-input:focus {
+  outline: none;
+  box-shadow: 0 0 0 2px rgba(96, 125, 139, 0.25);
+}
+
+
 /*---------------------------------------------------------------------------*/
 /*options scroll*/
 
diff --git a/popup/index.js b/popup/index.js
index fe01bae..458e906 100644
--- a/popup/index.js
+++ b/popup/index.js
@@ -53,7 +53,7 @@ var Popup = () => {
     ],
     raw: false,
     tab: '',
-    tabs: ['theme', 'compiler', 'content'],
+    tabs: ['theme', 'compiler', 'content', 'preview', 'history'],
     compilers: [],
     description: {
       themes: {},
@@ -67,7 +67,29 @@ var Popup = () => {
         syntax: 'Syntax highlighting for fenced code blocks',
       }
     },
-    settings: {}
+    settings: {},
+    tempMarkdown: '',
+    history: [],
+    editingId: null,
+    editingTitle: ''
+  }
+
+  var openPreview = (previewUrl) => {
+    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
+      var activeTab = tabs[0]
+      var isNewTab = activeTab && activeTab.url && (
+        activeTab.url === 'chrome://newtab/' ||
+        activeTab.url === 'about:newtab' ||
+        activeTab.url === 'about:home' ||
+        activeTab.url === 'about:blank' ||
+        activeTab.url.startsWith('chrome-search://')
+      )
+      if (isNewTab) {
+        chrome.tabs.update(activeTab.id, { url: previewUrl })
+      } else {
+        chrome.tabs.create({ url: previewUrl })
+      }
+    })
   }
 
   var events = {
@@ -141,6 +163,89 @@ var Popup = () => {
 
     advanced: () => {
       chrome.runtime.sendMessage({message: 'popup.advanced'})
+    },
+
+    preview: () => {
+      chrome.storage.local.set({ temporaryMarkdown: state.tempMarkdown || '' }, () => {
+        chrome.storage.local.get('previewHistory', (localRes) => {
+          var history = localRes.previewHistory || []
+          
+          var title = ''
+          var lines = (state.tempMarkdown || '').trim().split('\n')
+          if (lines.length && lines[0].startsWith('#')) {
+            title = lines[0].replace(/^#+\s*/, '').trim()
+          }
+          if (!title) {
+            var snippet = (state.tempMarkdown || '').trim().substring(0, 30)
+            title = snippet ? (snippet + (state.tempMarkdown.length > 30 ? '...' : '')) : 'Empty Markdown'
+          }
+          
+          var historyItem = {
+            id: Date.now().toString(),
+            title: title,
+            timestamp: new Date().toLocaleString(),
+            markdown: state.tempMarkdown || ''
+          }
+          
+          history.unshift(historyItem)
+          if (history.length > 50) {
+            history = history.slice(0, 50)
+          }
+          state.history = history
+          
+          chrome.storage.local.set({ previewHistory: history }, () => {
+            openPreview(chrome.runtime.getURL('/content/preview.html'))
+          })
+        })
+      })
+    },
+
+    clear: () => {
+      state.tempMarkdown = ''
+      chrome.storage.local.set({ temporaryMarkdown: '' }, () => {
+        m.redraw()
+      })
+    },
+
+    reopenHistory: (item) => {
+      chrome.storage.local.set({ temporaryMarkdown: item.markdown }, () => {
+        openPreview(chrome.runtime.getURL('/content/preview.html'))
+      })
+    },
+
+    startRename: (item) => {
+      state.editingId = item.id
+      state.editingTitle = item.title
+    },
+
+    saveRename: (itemId) => {
+      if (state.editingTitle.trim() !== '') {
+        var history = state.history.map((item) => {
+          if (item.id === itemId) {
+            item.title = state.editingTitle.trim()
+          }
+          return item
+        })
+        state.history = history
+        chrome.storage.local.set({ previewHistory: history }, () => {
+          state.editingId = null
+          state.editingTitle = ''
+          m.redraw()
+        })
+      }
+    },
+
+    cancelRename: () => {
+      state.editingId = null
+      state.editingTitle = ''
+    },
+
+    deleteHistory: (itemId) => {
+      var history = state.history.filter((item) => item.id !== itemId)
+      state.history = history
+      chrome.storage.local.set({ previewHistory: history }, () => {
+        m.redraw()
+      })
     }
   }
 
@@ -152,7 +257,7 @@ var Popup = () => {
     state.themes = res.themes
 
     state.raw = res.raw
-    state.tab = localStorage.getItem('tab') || 'theme'
+    state.tab = localStorage.getItem('tab') || 'preview'
     state.compilers = res.compilers
     state.description.compiler = res.description
 
@@ -162,7 +267,13 @@ var Popup = () => {
     m.redraw()
   }
 
-  chrome.runtime.sendMessage({message: 'popup'}, init)
+  chrome.runtime.sendMessage({message: 'popup'}, (res) => {
+    chrome.storage.local.get(['temporaryMarkdown', 'previewHistory'], (localRes) => {
+      state.tempMarkdown = localRes.temporaryMarkdown || ''
+      state.history = localRes.previewHistory || []
+      init(res)
+    })
+  })
 
   var oncreate = {
     ripple: (vnode) => {
@@ -293,6 +404,84 @@ var Popup = () => {
               m('span.mdc-switch-label', key)
             ))
           )
+        ),
+        // preview
+        m('.m-panel', {
+          class: state.tab === 'preview' ? 'is-active' : ''
+          },
+          m('textarea.preview-textarea', {
+            placeholder: 'Paste your Markdown here...',
+            value: state.tempMarkdown,
+            oninput: (e) => {
+              state.tempMarkdown = e.target.value
+            }
+          }),
+          m('.preview-controls',
+            m('button.mdc-button mdc-button--raised m-button clear-btn', {
+              oncreate: oncreate.ripple,
+              onclick: events.clear
+            }, 'Clear'),
+            m('button.mdc-button mdc-button--raised m-button preview-btn-half', {
+              oncreate: oncreate.ripple,
+              onclick: events.preview
+            }, 'Preview')
+          )
+        ),
+        // history
+        m('.m-panel', {
+          class: state.tab === 'history' ? 'is-active' : ''
+          },
+          m('.scroll.history-scroll',
+            state.history && state.history.length ?
+              state.history.map((item) =>
+                m('.history-item',
+                  m('.history-info',
+                    state.editingId === item.id ?
+                      m('input.edit-title-input', {
+                        value: state.editingTitle,
+                        oninput: (e) => {
+                          state.editingTitle = e.target.value
+                        },
+                        onkeydown: (e) => {
+                          if (e.key === 'Enter') events.saveRename(item.id)
+                          if (e.key === 'Escape') events.cancelRename()
+                        }
+                      })
+                    : [
+                        m('.history-title', item.title),
+                        m('.history-time', item.timestamp)
+                      ]
+                  ),
+                  state.editingId === item.id ?
+                    m('.history-actions',
+                      m('button.action-btn.save-btn', {
+                        title: 'Save',
+                        onclick: () => events.saveRename(item.id)
+                      }, m.trust('')),
+                      m('button.action-btn.discard-btn', {
+                        title: 'Discard',
+                        onclick: () => events.cancelRename()
+                      }, m.trust(''))
+                    )
+                  :
+                    m('.history-actions',
+                      m('button.action-btn.reopen-btn', {
+                        title: 'Open preview',
+                        onclick: () => events.reopenHistory(item)
+                      }, m.trust('')),
+                      m('button.action-btn.rename-btn', {
+                        title: 'Rename',
+                        onclick: () => events.startRename(item)
+                      }, m.trust('')),
+                      m('button.action-btn.delete-btn', {
+                        title: 'Delete',
+                        onclick: () => events.deleteHistory(item.id)
+                      }, m.trust(''))
+                    )
+                )
+              )
+            : m('.history-empty', 'No preview history found.')
+          )
         )
       ),