diff --git a/frontend/src/components/note/view_rendered.tsx b/frontend/src/components/note/view_rendered.tsx new file mode 100644 index 00000000..dccd2592 --- /dev/null +++ b/frontend/src/components/note/view_rendered.tsx @@ -0,0 +1,83 @@ +import { Accessor, Component, createEffect, createResource } from 'solid-js'; +import render from '~/core/renderer'; +import hljs from 'highlight.js/lib/common'; +import { type Context } from '~/core/renderer'; + +type NoteViewRenderedProps = { + content: Accessor + context: Accessor +} + +const NoteViewRendered: Component = (props) => { + + const [contentRendered] = createResource(() => [props.content(), props.context()], ([content, context]) => { + try { + return render(content as string, context as Context) + } catch { + return "

Unable to render markdown, maybe you got the templating syntax incorrect?

" + } + }) + + let container: HTMLDivElement | undefined; + + createEffect(() => { + contentRendered(); + if (!container) { + return; + } + hljs.highlightAll(); + attachCopyButtons(container); + }) + + return ( +

+ ) +} + +/** + * Add a "Copy" button to the top-right of every
 block in the
+ * rendered note. The button uses the Clipboard API and shows a brief
+ * "Copied!" indicator. Idempotent: existing buttons are not duplicated.
+ */
+function attachCopyButtons(root: HTMLElement): void {
+  const blocks = root.querySelectorAll('pre > code');
+  for (const code of Array.from(blocks)) {
+    const pre = code.parentElement;
+    if (!pre || pre.dataset['copyButtonAttached'] === '1') {
+      continue;
+    }
+    pre.dataset['copyButtonAttached'] = '1';
+
+    if (pre.style.position === '' || pre.style.position == null) {
+      pre.style.position = 'relative';
+    }
+
+    const button = document.createElement('button');
+    button.type = 'button';
+    button.className = 'note-mark-copy-code';
+    button.setAttribute('aria-label', 'Copy code to clipboard');
+    button.textContent = 'Copy';
+
+    button.addEventListener('click', async () => {
+      try {
+        await navigator.clipboard.writeText(code.innerText);
+        const original = button.textContent;
+        button.textContent = 'Copied!';
+        button.classList.add('is-copied');
+        window.setTimeout(() => {
+          button.textContent = original;
+          button.classList.remove('is-copied');
+        }, 1500);
+      } catch {
+        button.textContent = 'Failed';
+        window.setTimeout(() => {
+          button.textContent = 'Copy';
+        }, 1500);
+      }
+    });
+
+    pre.appendChild(button);
+  }
+}
+
+export default NoteViewRendered;
diff --git a/frontend/src/index.css b/frontend/src/index.css
index a524ceb3..da785e56 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -132,4 +132,21 @@
     .breadcrumb-disabled:hover {
         @apply no-underline cursor-not-allowed;
     }
+
+    .note-mark-copy-code {
+        @apply absolute top-2 right-2 px-2 py-1 rounded-field text-xs;
+        @apply bg-base-100/80 text-base-content border border-base-content/20;
+        @apply opacity-0 transition-opacity duration-150;
+        @apply cursor-pointer;
+    }
+
+    pre:hover .note-mark-copy-code,
+    .note-mark-copy-code:focus,
+    .note-mark-copy-code.is-copied {
+        @apply opacity-100;
+    }
+
+    .note-mark-copy-code.is-copied {
+        @apply bg-success/20 border-success/40;
+    }
 }