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} + + + + + JSON output + + + { + navigator.clipboard.writeText( + convert( + value, + shouldOptimise + ) + ); + recentlyCopied = true; + setTimeout(() => (recentlyCopied = false), 2000); + }}> + {#if !recentlyCopied} + + Copy + {:else} + + Copied! + {/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 @@ + + + + Hide "Export this" button + If enabled, the selection export feature ("export this" button) will be disabled + + + = createPersistentStore("settings", showCharacterCount: true, syntaxHighlight: true, realisticLineHeight: false, + hideSelectionExport: false, fontSize: 1, }); diff --git a/src/lib/tiptap/extensions/ExportButton.ts b/src/lib/tiptap/extensions/ExportButton.ts index 6d8ca1e..7fd5c03 100644 --- a/src/lib/tiptap/extensions/ExportButton.ts +++ b/src/lib/tiptap/extensions/ExportButton.ts @@ -1,6 +1,8 @@ import { Extension } from '@tiptap/core'; import { Plugin, PluginKey } from '@tiptap/pm/state'; import { EditorView } from '@tiptap/pm/view'; +import { appSettings } from "$lib/settings"; +import { get } from 'svelte/store'; // Define the shape of configuration options for type safety export interface ExportButtonOptions { @@ -26,6 +28,10 @@ export const ExportButtonExtension = Extension.create({ view(editorView: EditorView) { return { update(view: EditorView) { + if (get(appSettings).hideSelectionExport == true) { + return; + } + const { state } = view; const { selection } = state; From c6b3ad165c83525d63be495f62bd7afd2dc1c801 Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:34:37 +0100 Subject: [PATCH 3/8] fix modal errors --- src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 457c63b..662be2c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -413,5 +413,5 @@ {/await} {#await import("$lib/components/modals/ExportSelectionModal.svelte") then modal} - + {/await} From 209fc8adcabf31c9b0b23845e39edca78ae6fce1 Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:34:04 +0100 Subject: [PATCH 4/8] shrink max output height --- src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 662be2c..e328a47 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -259,7 +259,7 @@ {/if} - + Date: Tue, 11 Aug 2026 22:43:36 +0100 Subject: [PATCH 5/8] minor toolbar changes --- src/app.css | 2 +- src/lib/components/toolbar/Toolbar.svelte | 32 ++++++++----------- .../components/toolbar/ToolbarButton.svelte | 5 +-- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/app.css b/src/app.css index f5fd4f5..4a82a96 100644 --- a/src/app.css +++ b/src/app.css @@ -96,7 +96,7 @@ strong { .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 */ + @apply w-0 invisible sm:w-fit sm:visible; /* hide on mobile */ } /* Useful */ diff --git a/src/lib/components/toolbar/Toolbar.svelte b/src/lib/components/toolbar/Toolbar.svelte index ce61965..645bb94 100644 --- a/src/lib/components/toolbar/Toolbar.svelte +++ b/src/lib/components/toolbar/Toolbar.svelte @@ -143,7 +143,8 @@ ? editor!.chain().focus().unsetFont().run() : fontDialog.open()} styleVar={editor.getAttributes("textStyle").font} - ariaLabel="Font" /> + ariaLabel="Font" + desktopOnly /> @@ -158,14 +159,16 @@ Icon={IconGradient} onClick={gradientDialog.open} ariaLabel="Color Gradient" /> - + {#each colourMap as colour} - editor!.chain().focus().setColor(colour.value).run()} - styleVar={editor.isActive("textStyle", { color: colour.value })} - colour={colour.value} - ariaLabel={toTitleCase(colour.name.replace("_", " "))} /> + editor!.chain().focus().setColor(colour.value).run()} + {@attach tooltip} + style="color: {colour.value || 'inherit'}" + class="rounded-md py-1 px-0 text-lg font-medium hover:bg-white/3 {editor.isActive("textStyle", { color: colour.value }) ? ' bg-zinc-800' : ''}"> + + {/each} {#if editor.getAttributes("textStyle").color} @@ -258,17 +261,8 @@ editor?.commands.redo()} ariaLabel="Redo" Icon={IconRedo} /> - - - + + {/if} diff --git a/src/lib/components/toolbar/ToolbarButton.svelte b/src/lib/components/toolbar/ToolbarButton.svelte index 1b47a09..b263653 100644 --- a/src/lib/components/toolbar/ToolbarButton.svelte +++ b/src/lib/components/toolbar/ToolbarButton.svelte @@ -8,9 +8,10 @@ ariaLabel: string; Icon: Component; colour?: string; + desktopOnly?: boolean }; - const { onClick, styleVar, Icon, ariaLabel, colour }: Props = $props(); + const { onClick, styleVar, Icon, ariaLabel, colour, desktopOnly }: Props = $props(); + class="toolbar-btn{styleVar ? ' bg-zinc-800' : ''} {desktopOnly ? 'nomob' : ''}"> From ef1ffbcec7d28138fa1b2760121240ed427b11f2 Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:49:50 +0100 Subject: [PATCH 6/8] on second thoughts, this makes no sense --- src/lib/components/toolbar/Toolbar.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/components/toolbar/Toolbar.svelte b/src/lib/components/toolbar/Toolbar.svelte index 645bb94..7d11729 100644 --- a/src/lib/components/toolbar/Toolbar.svelte +++ b/src/lib/components/toolbar/Toolbar.svelte @@ -143,8 +143,7 @@ ? editor!.chain().focus().unsetFont().run() : fontDialog.open()} styleVar={editor.getAttributes("textStyle").font} - ariaLabel="Font" - desktopOnly /> + ariaLabel="Font" /> From 97d73a8242e0c79da6d2eb25abf5fdfdd617812e Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:03:02 +0100 Subject: [PATCH 7/8] fix tests --- src/lib/components/toolbar/Toolbar.svelte | 2 +- src/lib/settings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/toolbar/Toolbar.svelte b/src/lib/components/toolbar/Toolbar.svelte index 7d11729..95d82d1 100644 --- a/src/lib/components/toolbar/Toolbar.svelte +++ b/src/lib/components/toolbar/Toolbar.svelte @@ -158,7 +158,7 @@ Icon={IconGradient} onClick={gradientDialog.open} ariaLabel="Color Gradient" /> - + {#each colourMap as colour} = createPersistentStore("settings", showCharacterCount: true, syntaxHighlight: true, realisticLineHeight: false, - hideSelectionExport: false, + hideSelectionExport: true, fontSize: 1, }); From 937f1360475425f1ee2bc96906614e05da4185fb Mon Sep 17 00:00:00 2001 From: Silabear <56885288+Silabear@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:14:07 +0100 Subject: [PATCH 8/8] remove commented out code --- src/routes/+page.svelte | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e328a47..a3d964e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -130,19 +130,7 @@ onUpdate: ({ editor }) => { 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(() => {
Selected text:
Output:
+ {#if $appSettings.syntaxHighlight} + + {:else} + + {editor ? convert( + value, + shouldOptimise, + "standard", + jsonOutput + ) : "Loading..."} + + {/if} +
+ {editor ? convert( + value, + shouldOptimise, + "standard", + jsonOutput + ) : "Loading..."} +