From d43d80da835a58ebc52ac0a55ecc76aa4b54cecf Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:27:30 +0100 Subject: [PATCH 1/8] add option to export only selected text --- src/app.css | 12 ++ .../modals/ExportSelectionModal.svelte | 115 ++++++++++++++++++ .../modals/topbar/ExportModal.svelte | 2 +- src/lib/components/text/MiniRenderer.svelte | 5 + src/lib/tiptap/extensions/ExportButton.ts | 73 +++++++++++ src/routes/+page.svelte | 25 ++++ 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/modals/ExportSelectionModal.svelte create mode 100644 src/lib/tiptap/extensions/ExportButton.ts diff --git a/src/app.css b/src/app.css index 187fd53..f5fd4f5 100644 --- a/src/app.css +++ b/src/app.css @@ -54,6 +54,13 @@ strong { @apply text-xl; } +.tiptap-minirenderer { + @apply h-full w-full p-2; + @apply focus:outline-none; + font-family: Minecraft; + @apply text-xl; +} + .tiptap p.is-editor-empty:first-child::before { color: #7f7f7f; content: attr(data-placeholder); @@ -87,6 +94,11 @@ strong { filter: blur(3px); } +.export-button { + @apply items-center p-1 px-1.5 bg-zinc-900 text-zinc-200 hover:text-white rounded-md font-lexend text-xs h-6; + @apply w-0 invisible md:w-fit md:visible; /* hide on mobile */ +} + /* Useful */ .nomob { diff --git a/src/lib/components/modals/ExportSelectionModal.svelte b/src/lib/components/modals/ExportSelectionModal.svelte new file mode 100644 index 0000000..95c4452 --- /dev/null +++ b/src/lib/components/modals/ExportSelectionModal.svelte @@ -0,0 +1,115 @@ + + + +
+ {#key value} +

Selected text:

+
+ +
+ +

Output:

+ + {#if $appSettings.syntaxHighlight} + + {:else} +
+                {editor ? convert(
+                    value,
+                    shouldOptimise,
+                    "standard",
+                    jsonOutput
+                ) : "Loading..."}
+            
+ {/if} +
+ +
+ + +
+ + + + {/key} +
+
diff --git a/src/lib/components/modals/topbar/ExportModal.svelte b/src/lib/components/modals/topbar/ExportModal.svelte index 7cfc145..e42c976 100644 --- a/src/lib/components/modals/topbar/ExportModal.svelte +++ b/src/lib/components/modals/topbar/ExportModal.svelte @@ -7,6 +7,7 @@ import { Highlight } from "svelte-highlight"; import typescript from "svelte-highlight/languages/typescript"; import { appSettings } from "$lib/settings"; + import { json } from "svelte-highlight/languages"; // Icons import IconItem from "~icons/tabler/swords"; @@ -17,7 +18,6 @@ import IconCopy from "~icons/tabler/copy"; import IconCheck from "~icons/tabler/check"; import IconDownload from "~icons/tabler/download"; - import { json } from "svelte-highlight/languages"; // UI let showOutput: string | null = $state(null); diff --git a/src/lib/components/text/MiniRenderer.svelte b/src/lib/components/text/MiniRenderer.svelte index f050c13..e796f5f 100644 --- a/src/lib/components/text/MiniRenderer.svelte +++ b/src/lib/components/text/MiniRenderer.svelte @@ -58,6 +58,11 @@ FontsExtension, ], content: value, + editorProps: { + attributes: { + class: 'tiptap-minirenderer', + }, + }, }).setEditable(false); }); diff --git a/src/lib/tiptap/extensions/ExportButton.ts b/src/lib/tiptap/extensions/ExportButton.ts new file mode 100644 index 0000000..6d8ca1e --- /dev/null +++ b/src/lib/tiptap/extensions/ExportButton.ts @@ -0,0 +1,73 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; +import { EditorView } from '@tiptap/pm/view'; + +// Define the shape of configuration options for type safety +export interface ExportButtonOptions { + onClick: () => void; +} + +export const ExportButtonExtension = Extension.create({ + name: 'ExportButton', + + addOptions() { + return { + onClick: () => { }, + }; + }, + + addProseMirrorPlugins() { + const { onClick } = this.options; + let buttonDom: HTMLButtonElement | null = null; + + return [ + new Plugin({ + key: new PluginKey('ExportButton'), + view(editorView: EditorView) { + return { + update(view: EditorView) { + const { state } = view; + const { selection } = state; + + const parentNode = view.dom.parentNode as HTMLElement | null; + if (!parentNode) return; + + if ((selection.empty || !view.hasFocus()) && buttonDom) { + buttonDom.style.display = 'none'; + return; + } + + if (!buttonDom) { + buttonDom = document.createElement('button'); + buttonDom.innerText = '↪ Export this'; + buttonDom.className = 'export-button'; + buttonDom.style.position = 'absolute'; + buttonDom.style.zIndex = '10'; + + buttonDom.addEventListener('mousedown', (e: MouseEvent) => e.preventDefault()); + buttonDom.addEventListener('click', onClick); + + parentNode.appendChild(buttonDom); + } + + const coords = view.coordsAtPos(selection.to); + const top = coords.top + 19; + const left = coords.left - 90; + + buttonDom.style.display = 'block'; + buttonDom.style.top = `${top}px`; + buttonDom.style.left = `${left + 5}px`; + }, + + destroy() { + if (buttonDom) { + buttonDom.remove(); + buttonDom = null; + } + } + }; + }, + }), + ]; + }, +}); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 53a6d7c..457c63b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -36,6 +36,7 @@ import TopUI from "$lib/components/TopUI.svelte"; import { onDestroy, onMount } from "svelte"; import { appSettings } from "$lib/settings"; + import { ExportButtonExtension } from "$lib/tiptap/extensions/ExportButton"; let tiptapJSON: JSONContent = $state()!; @@ -49,6 +50,8 @@ let recentlyCopied = $state(false); let finalOutput = $derived(editor ? convert(tiptapJSON, shouldOptimise) : "Loading..."); + + let exportSelectionDialog: Modal = $state()!; async function loadData() { if (localStorage.getItem("content")) { @@ -113,6 +116,12 @@ placeholder: "Write text here, style it with the options above, and the output text components will appear at the bottom. You can also import text components with the Import button above!", }), + ExportButtonExtension.configure({ + onClick: () => { + exportSelectionDialog.open() + }, + }), + ], onTransaction: ({ editor: newEditor }) => { editor = undefined; @@ -122,6 +131,18 @@ tiptapJSON = editor.getJSON(); debounce(saveContent, 1000)(); }, + // onSelectionUpdate: ({ editor }) => { + // const { state } = editor; + // const { from, to, empty } = state.selection; + + // // Do nothing if it's just a cursor click without a highlighted range + // if (empty) return; + + // // Extract the selection slice as JSON + // const selectionJson = state.doc.slice(from, to).content.toJSON(); + + // console.log("Selected content as JSON:", selectionJson); + // } }); appSettings.subscribe(() => { @@ -390,3 +411,7 @@ {#await import("$lib/components/modals/topbar/ExportModal.svelte") then modal} {/await} + +{#await import("$lib/components/modals/ExportSelectionModal.svelte") then modal} + +{/await} From 7d5d82a7bee27bd94c9e4a5bf550d242e1768d7f Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:33:11 +0100 Subject: [PATCH 2/8] add settings option to hide export this button --- src/lib/components/modals/topbar/SettingsModal.svelte | 9 +++++++++ src/lib/settings.ts | 2 ++ src/lib/tiptap/extensions/ExportButton.ts | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/src/lib/components/modals/topbar/SettingsModal.svelte b/src/lib/components/modals/topbar/SettingsModal.svelte index a655ae2..6f0d927 100644 --- a/src/lib/components/modals/topbar/SettingsModal.svelte +++ b/src/lib/components/modals/topbar/SettingsModal.svelte @@ -42,6 +42,15 @@ +
+ + +
+