From a52437b40b992061749a8a5fbcc19bc6afaeb7e9 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 26 Aug 2026 11:16:19 -0400 Subject: [PATCH] Make writing a guide less like operating an editor Seven changes, all aimed at the same thing: think about the procedure, not about blocks. Live preview beside the editor, rendered with the SAME component readers get, so what an author sees is what will be published - a separate preview implementation would drift within a month. Formatting buttons wrap the selection, so nobody needs to know the markup is Markdown. New blocks and pasted images land AFTER the block being worked on rather than at the bottom of the document, which removes the scroll-back that made adding a screenshot mid-guide annoying. Enter in a list makes the next item and focuses it, the way every other list in every other tool behaves; backspace on an already-empty item removes it and steps back up. Typing a ten-item checklist no longer needs the mouse. Templates, because the blank page is the real barrier to somebody writing a procedure down at all. The shapes are lifted from the KB articles the team already writes, so guides written here read like the ones written before them. Autosave on a three-second debounce with an honest status line, safe precisely because the server dedups on content - an autosave that changed nothing writes no version. Leaving with unsaved work now asks first. CLIPBOARD: the code block no longer writes to the clipboard. A page that programmatically copies a shell command is the exact shape of a ClickFix attack, where malware plants a command in your clipboard and talks you into pasting it into Run - and uBlock Origin blocked ours, correctly, because it was writing PowerShell on click. The button now SELECTS the code and says to press Ctrl+C: same convenience, decision back with the person, and nothing for security tooling to object to. AUTOCORRECT is on for prose and off for code and table cells. A code block that autocorrects produces a guide telling somebody to run a command that does not work - smart quotes replacing a straight one, an autocapitalised cmdlet - and it still LOOKS right to the author. The person at the bay gets the syntax error. --- .../frontend/components/GuideBlocks.vue | 55 ++- plugins/guides/frontend/utils/templates.js | 74 +++ plugins/guides/frontend/views/GuideEditor.vue | 459 +++++++++++++----- 3 files changed, 431 insertions(+), 157 deletions(-) create mode 100644 plugins/guides/frontend/utils/templates.js diff --git a/plugins/guides/frontend/components/GuideBlocks.vue b/plugins/guides/frontend/components/GuideBlocks.vue index 3ddd31b9..ebc24551 100644 --- a/plugins/guides/frontend/components/GuideBlocks.vue +++ b/plugins/guides/frontend/components/GuideBlocks.vue @@ -50,14 +50,26 @@
{{ block.language || 'text' }} {{ block.caption }} - -
-
{{ block.text }}
+
{{ block.text }}
@@ -96,7 +108,9 @@ const props = defineProps({ }) const emit = defineEmits(['toggle']) -const copied = ref('') +const selected = ref('') +const codeNodes = {} +function registerCode (id, el) { if (el) codeNodes[id] = el } function inline (text) { return renderInline(text) @@ -138,23 +152,16 @@ function anchorFor (block) { return 'g-' + (block.id || '') } -async function copy (block) { - try { - await navigator.clipboard.writeText(block.text || '') - copied.value = block.id - setTimeout(() => { copied.value = '' }, 2000) - } catch (err) { - // Clipboard needs a secure context. Select the text instead of failing - // silently, so the tech can copy it by hand. - const node = document.querySelector('.gb-code pre') - if (node && window.getSelection) { - const range = document.createRange() - range.selectNodeContents(node) - const selection = window.getSelection() - selection.removeAllRanges() - selection.addRange(range) - } - } +function select (block) { + const node = codeNodes[block.id] + if (!node || !window.getSelection) return + const range = document.createRange() + range.selectNodeContents(node) + const selection = window.getSelection() + selection.removeAllRanges() + selection.addRange(range) + selected.value = block.id + setTimeout(() => { selected.value = '' }, 4000) } diff --git a/plugins/guides/frontend/utils/templates.js b/plugins/guides/frontend/utils/templates.js new file mode 100644 index 00000000..1a166f81 --- /dev/null +++ b/plugins/guides/frontend/utils/templates.js @@ -0,0 +1,74 @@ +/** + * Starting points for a new guide. + * + * The blank page is the real barrier to somebody writing a procedure down. A + * skeleton with the right SHAPE - what you need first, the steps, how you know + * it worked, what to do when it does not - gets a half-decent guide out of + * somebody who would otherwise have written nothing. + * + * The shape is lifted from the KB articles the team already writes, so guides + * written here read like the ones written before them. + */ + +function id (prefix, n) { + return prefix + n +} + +export const templates = [ + { + key: 'blank', + name: 'Blank', + description: 'Start with nothing', + blocks: () => [], + }, + { + key: 'procedure', + name: 'Procedure', + description: 'Before you start, numbered steps, how to check it worked', + blocks: () => [ + { id: id('t', 1), type: 'paragraph', + text: 'What this covers, and when somebody would need it.' }, + { id: id('t', 2), type: 'callout', variant: 'warning', title: 'Before you start', + text: 'Anything that has to be true first, or that will bite halfway through.' }, + { id: id('t', 3), type: 'heading', text: 'Steps' }, + { id: id('t', 4), type: 'step', title: 'First step', text: '' }, + { id: id('t', 5), type: 'step', title: 'Second step', text: '' }, + { id: id('t', 6), type: 'heading', text: 'Check it worked' }, + { id: id('t', 7), type: 'checklist', items: [ + { id: id('ti', 1), text: '' }, + { id: id('ti', 2), text: '' }, + ] }, + ], + }, + { + key: 'troubleshooting', + name: 'Troubleshooting', + description: 'Symptom, likely causes, what to check first', + blocks: () => [ + { id: id('t', 1), type: 'paragraph', text: 'The symptom, as somebody would describe it.' }, + { id: id('t', 2), type: 'heading', text: 'Check these first' }, + { id: id('t', 3), type: 'checklist', items: [ + { id: id('ti', 1), text: '' }, + { id: id('ti', 2), text: '' }, + ] }, + { id: id('t', 4), type: 'heading', text: 'Likely causes' }, + { id: id('t', 5), type: 'table', + header: ['What you see', 'What it usually means'], rows: [['', '']] }, + { id: id('t', 6), type: 'callout', variant: 'info', + text: 'The thing that looks like the cause and is not.' }, + ], + }, + { + key: 'checklist', + name: 'Checklist', + description: 'A list to work through, nothing else', + blocks: () => [ + { id: id('t', 1), type: 'paragraph', text: 'When to use this list.' }, + { id: id('t', 2), type: 'checklist', items: [ + { id: id('ti', 1), text: '' }, + { id: id('ti', 2), text: '' }, + { id: id('ti', 3), text: '' }, + ] }, + ], + }, +] diff --git a/plugins/guides/frontend/views/GuideEditor.vue b/plugins/guides/frontend/views/GuideEditor.vue index 220c6a58..1413f69a 100644 --- a/plugins/guides/frontend/views/GuideEditor.vue +++ b/plugins/guides/frontend/views/GuideEditor.vue @@ -3,6 +3,10 @@ @@ -23,86 +25,138 @@
{{ error }}
- - - + + +
-
-
-
- {{ block.type }} - - - -
- - - + +
+ Start from: + +
+ +
+
+
+
+ {{ block.type }} + + + + + +
+ + + + +
+ +
+ + + + +
+
+ + + + + + +
+
+ {{ block.type === 'numbers' ? (i + 1) + '.' : '-' }} + + +
+ +
+ +
+ + + +
+ +
+
- - - - -
-
- - -
- +
+ Add: + +
-
- - - -
+

+ Paste or drag an image anywhere to drop it in below the block you are working on. + Text supports **bold**, *italic*, `code` and [links](https://example.com) - + or use the buttons. +

+
-
- -
+ +
+
Preview
+

{{ guide.title || 'Untitled guide' }}

+

{{ guide.summary }}

+
-
- Add: - - -
- -

- Paste or drag an image straight onto this page to add it. Text supports - **bold**, *italic*, `code` and [links](https://example.com). -

-

Version history

@@ -117,11 +171,13 @@