@grandaniel/vue-markdown-editor 1.1.3-dev.ed12560 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,9 +5,13 @@
5
5
 
6
6
  # @grandaniel/vue-markdown-editor
7
7
 
8
- > A block-based, Notion-like Markdown editor for Vue 3. UI-first. Powered by [TipTap](https://tiptap.dev/).
8
+ > A block-based, Notion-like Markdown editor **and** SSR-safe renderer for Vue 3. UI-first. Powered by [TipTap](https://tiptap.dev/).
9
9
 
10
- **@grandaniel/vue-markdown-editor** is a rich block-editing experience where every Markdown element — headlines, paragraphs, lists, images — is its own independently editable, draggable block. Built for content-first workflows: write, reorder, and format with keyboard shortcuts and an always-visible drag handle.
10
+ **@grandaniel/vue-markdown-editor** provides two complementary packages:
11
+
12
+ - **Editor** — A rich block-editing experience where every Markdown element — headlines, paragraphs, lists, images — is its own independently editable, draggable block. Built for content-first workflows: write, reorder, and format with keyboard shortcuts and an always-visible drag handle.
13
+
14
+ - **Renderer** — A lightweight, SSR-safe component that renders Markdown strings to semantic HTML. Uses the same `remark-parse` pipeline as the editor, so custom module blocks render identically. Fully customizable: override any default render component with your own Vue components.
11
15
 
12
16
  Images are first-class citizens: paste an image to auto-upload, then edit its **src**, **alt text**, and **caption** in a dedicated modal. The editor serializes back to clean Markdown automatically.
13
17
 
@@ -17,10 +21,16 @@ Images are first-class citizens: paste an image to auto-upload, then edit its **
17
21
 
18
22
  - [Who uses it](#who-uses-it)
19
23
  - [Installation](#installation)
20
- - [Quick Start](#quick-start)
21
- - [Keyboard Shortcuts](#keyboard-shortcuts)
22
- - [Image Upload](#image-upload)
23
- - [API Reference](#api-reference)
24
+ - [Editor](#editor)
25
+ - [Quick Start](#editor-quick-start)
26
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
27
+ - [Image Upload](#image-upload)
28
+ - [API Reference](#editor-api-reference)
29
+ - [Renderer](#renderer)
30
+ - [Quick Start](#renderer-quick-start)
31
+ - [Custom Render Components](#custom-render-components)
32
+ - [API Reference](#renderer-api-reference)
33
+ - [Exported Types & Utilities](#exported-types--utilities)
24
34
  - [Custom Styling](#custom-styling)
25
35
  - [Supply Chain Security](#supply-chain-security)
26
36
  - [Contributing](#contributing)
@@ -46,19 +56,21 @@ npm install @grandaniel/vue-markdown-editor
46
56
  > **Peer dependency:** Vue `^3.5.0`
47
57
  > **Node:** `>=22`
48
58
 
49
- Import the component **and** the stylesheet:
59
+ Import the components **and** the stylesheet:
50
60
 
51
61
  ```ts
52
- import { MarkdownEditor, useMarkdownEditor } from "@grandaniel/vue-markdown-editor";
62
+ import { MarkdownEditor, MarkdownRenderer, useMarkdownEditor, useMarkdownRenderer } from "@grandaniel/vue-markdown-editor";
53
63
  import "@grandaniel/vue-markdown-editor/style.css";
54
64
  ```
55
65
 
56
66
  ---
57
67
 
58
- ## Quick Start
68
+ ## Editor
59
69
 
60
70
  The editor is driven by a composable: `useMarkdownEditor()` creates the reactive state, and `<MarkdownEditor>` renders it.
61
71
 
72
+ ### Editor Quick Start
73
+
62
74
  ```vue
63
75
  <script setup lang="ts">
64
76
  import { ref } from "vue";
@@ -81,7 +93,7 @@ const focusedNode = ref<MarkdownAstNode | null>(null);
81
93
  </template>
82
94
  ```
83
95
 
84
- ### Reading the output
96
+ #### Reading the output
85
97
 
86
98
  The composable keeps the raw Markdown in sync automatically. Read it at any time:
87
99
 
@@ -98,7 +110,7 @@ editor.markdownContent.value = "## New heading\n\nFresh content.";
98
110
 
99
111
  ---
100
112
 
101
- ## Keyboard Shortcuts
113
+ ### Keyboard Shortcuts
102
114
 
103
115
  | Key | Action |
104
116
  |---|---|
@@ -108,17 +120,17 @@ editor.markdownContent.value = "## New heading\n\nFresh content.";
108
120
  | <kbd>Delete</kbd> (empty block) | Delete the block, focus stays at same index |
109
121
  | Click blank area | Append a new empty paragraph at the bottom |
110
122
 
111
- ### Auto type‑detection
123
+ #### Auto type‑detection
112
124
 
113
125
  Type `# `, `## `, or `### ` at the start of a paragraph and the block auto‑converts to the matching heading level.
114
126
 
115
127
  ---
116
128
 
117
- ## Image Upload
129
+ ### Image Upload
118
130
 
119
131
  Images are first-class blocks with **src**, **alt text**, and **caption** fields. Right‑click any image → **Edit Attributes** to open the editing modal.
120
132
 
121
- ### Paste‑to‑upload
133
+ #### Paste‑to‑upload
122
134
 
123
135
  Pass an `imageUploadFunction` prop — any pasted image (`Ctrl+V`) from the clipboard will be sent through your upload handler and inserted as a new image block:
124
136
 
@@ -156,9 +168,9 @@ caption: Photo taken during the 2026 summit
156
168
 
157
169
  ---
158
170
 
159
- ## API Reference
171
+ ### Editor API Reference
160
172
 
161
- ### `useMarkdownEditor(initialContent?: string)`
173
+ #### `useMarkdownEditor(initialContent?: string)`
162
174
 
163
175
  Returns a reactive editor instance:
164
176
 
@@ -172,7 +184,7 @@ Returns a reactive editor instance:
172
184
  | `replaceNodeType(node, newType)` | `(node: MarkdownAstNode, type: MarkdownNodeType) => { newNode, index } \| null` | Convert between block types (e.g. paragraph → heading). |
173
185
  | `moveNode(from, to)` | `(fromIndex: number, toIndex: number) => void` | Programmatically reorder a block. |
174
186
 
175
- ### `MarkdownEditor` props
187
+ #### `MarkdownEditor` props
176
188
 
177
189
  | Prop | Type | Required | Description |
178
190
  |---|---|---|---|
@@ -180,40 +192,28 @@ Returns a reactive editor instance:
180
192
  | `focusedNode` | `MarkdownAstNode \| null` | — | For `v-model:focused-node` tracking. |
181
193
  | `imageUploadFunction` | `(file: File) => Promise<string>` | — | Async callback for paste‑to‑upload. |
182
194
 
183
- ### `MarkdownEditor` emits
195
+ #### `MarkdownEditor` emits
184
196
 
185
197
  | Event | Payload | Description |
186
198
  |---|---|---|
187
199
  | `update:focused-node` | `MarkdownAstNode \| null` | Fires when focus moves to a new block. |
188
200
 
189
- ### `MarkdownEditor` slots
201
+ #### `MarkdownEditor` slots
190
202
 
191
203
  | Slot | Description |
192
204
  |---|---|
193
205
  | `after-controls` | Injected inside every block, after the drag‑handle / add / delete controls. |
194
206
 
195
- ### Exported types & utilities
196
-
197
- | Export | Kind |
198
- |---|---|
199
- | `MarkdownEditorInstance` | Type — return type of `useMarkdownEditor()`. |
200
- | `MarkdownAstNode` | Class — AST node with `id`, `type`, `componentState`, `editingState`. |
201
- | `MarkdownAstNodeType` | Enum — `PARAGRAPH`, `HEADLINE1`, `HEADLINE2`, `HEADLINE3`, `IMAGE`, `LIST`. |
202
- | `ImageNode` | Type alias — `MarkdownAstNode<MarkdownModuleImageState>`. |
203
- | `TextNode` | Type alias — `MarkdownAstNode<MarkdownModuleTextState>`. |
204
- | `TextishNodeType` | Type — union of `PARAGRAPH \| HEADLINE1 \| HEADLINE2 \| HEADLINE3 \| LIST`. |
205
- | `isTextNodeState(node)` | Type guard for text‑based nodes. |
206
- | `isTextNodeType(type)` | Type guard for text‑based node types. |
207
- | `MarkdownRenderer` | Component — renders Markdown to plain HTML. SSR‑safe. |
208
-
209
207
  ---
210
208
 
211
- ## MarkdownRenderer
209
+ ## Renderer
212
210
 
213
- `<MarkdownRenderer>` converts a Markdown string to plain HTML. It uses the same `remark-parse` pipeline as the editor, so custom module blocks (like `"""MarkdownModuleImage"""`) render as rich `<figure>` elements automatically.
211
+ `<MarkdownRenderer>` converts a Markdown string to semantic HTML. It uses the same `remark-parse` pipeline as the editor, so custom module blocks (like `"""MarkdownModuleImage"""`) render as rich `<figure>` elements automatically.
214
212
 
215
213
  The renderer is **SSR‑safe** — no browser APIs, no TipTap. It works in `nuxt generate`, `vite-ssg`, and any server‑side rendering context.
216
214
 
215
+ ### Renderer Quick Start
216
+
217
217
  ```vue
218
218
  <script setup lang="ts">
219
219
  import { MarkdownRenderer } from "@grandaniel/vue-markdown-editor";
@@ -238,7 +238,7 @@ caption: Photo caption
238
238
  </template>
239
239
  ```
240
240
 
241
- ### Live preview alongside the editor
241
+ #### Live preview alongside the editor
242
242
 
243
243
  Bind the editor's reactive `markdownContent` to the renderer:
244
244
 
@@ -257,11 +257,116 @@ const editor = useMarkdownEditor("# Start writing…");
257
257
  </template>
258
258
  ```
259
259
 
260
- ### Props
260
+ ---
261
+
262
+ ### Custom Render Components
263
+
264
+ You can override any default render component with your own Vue component. Use `useMarkdownRenderer()` to create a renderer instance, then call `overrideComponent()` to swap in your custom component for a specific node type.
265
+
266
+ Each override is **strictly typed** — the replacement component must accept a `state` prop matching the correct state class for that node type (e.g. `MarkdownModuleImageState` for images, `MarkdownModuleTextState` for paragraphs and headings).
267
+
268
+ #### Overriding the image render component
269
+
270
+ ```vue
271
+ <script setup lang="ts">
272
+ import {
273
+ MarkdownRenderer,
274
+ useMarkdownRenderer,
275
+ MarkdownAstNodeType,
276
+ type MarkdownModuleImageState,
277
+ } from "@grandaniel/vue-markdown-editor";
278
+ import CustomImageRender from "./CustomImageRender.vue";
279
+
280
+ // Create a renderer instance and override the image component
281
+ const renderer = useMarkdownRenderer();
282
+ renderer.overrideComponent(MarkdownAstNodeType.IMAGE, CustomImageRender);
283
+
284
+ const markdown = `"""MarkdownModuleImage
285
+ src: https://example.com/photo.jpg
286
+ alt: A scenic view
287
+ caption: My custom caption
288
+ """
289
+ `;
290
+ </script>
291
+
292
+ <template>
293
+ <MarkdownRenderer
294
+ :markdown="markdown"
295
+ :renderer="renderer"
296
+ />
297
+ </template>
298
+ ```
299
+
300
+ Your custom component receives the state as a typed prop:
301
+
302
+ ```vue
303
+ <!-- CustomImageRender.vue -->
304
+ <script setup lang="ts">
305
+ import { type MarkdownModuleImageState } from "@grandaniel/vue-markdown-editor";
306
+
307
+ defineProps<{ state: MarkdownModuleImageState }>();
308
+ </script>
309
+
310
+ <template>
311
+ <div class="my-custom-image-wrapper">
312
+ <img :src="state.src" :alt="state.alt" />
313
+ <span class="my-caption">{{ state.caption }}</span>
314
+ </div>
315
+ </template>
316
+ ```
317
+
318
+ #### Available node types and their state classes
319
+
320
+ | Node Type | State Class | Prop Interface |
321
+ |---|---|---|
322
+ | `MarkdownAstNodeType.PARAGRAPH` | `MarkdownModuleTextState` | `{ text: string }` |
323
+ | `MarkdownAstNodeType.HEADLINE1` | `MarkdownModuleTextState` | `{ text: string }` |
324
+ | `MarkdownAstNodeType.HEADLINE2` | `MarkdownModuleTextState` | `{ text: string }` |
325
+ | `MarkdownAstNodeType.HEADLINE3` | `MarkdownModuleTextState` | `{ text: string }` |
326
+ | `MarkdownAstNodeType.LIST` | `MarkdownModuleListState` | `{ items: MarkdownModuleTextState[] }` |
327
+ | `MarkdownAstNodeType.IMAGE` | `MarkdownModuleImageState` | `{ src: string; alt: string; caption: string }` |
328
+ | `MarkdownAstNodeType.FILE` | `MarkdownModuleFileState` | `{ url: string; fileName: string; fileSize: number; mimeType: string; uploadError: string }` |
329
+
330
+ ---
331
+
332
+ ### Renderer API Reference
333
+
334
+ #### `useMarkdownRenderer()`
335
+
336
+ Returns a reactive renderer instance with a customizable component registry:
337
+
338
+ | Member | Type | Description |
339
+ |---|---|---|
340
+ | `componentRegistry` | `Reactive<{ [K in MarkdownNodeType]: RenderComponent<RenderStateMap[K]> }>` | Reactive map of node types to their current render components. |
341
+ | `overrideComponent(type, component)` | `<K extends MarkdownNodeType>(type: K, component: RenderComponent<RenderStateMap[K]>) => void` | Replace the render component for a given node type. Strictly typed — the component must accept the correct `state` prop. |
342
+
343
+ #### `MarkdownRenderer` props
261
344
 
262
345
  | Prop | Type | Required | Description |
263
346
  |---|---|---|---|
264
347
  | `markdown` | `string` | ✓ | Raw Markdown string to render as HTML. |
348
+ | `renderer` | `MarkdownRendererInstance` | — | Instance from `useMarkdownRenderer()`. Optional — when omitted, the built-in default render components are used. Provide this to supply custom render components via `overrideComponent()`. |
349
+
350
+ ---
351
+
352
+ ## Exported Types & Utilities
353
+
354
+ | Export | Kind |
355
+ |---|---|
356
+ | `MarkdownEditorInstance` | Type — return type of `useMarkdownEditor()`. |
357
+ | `MarkdownRendererInstance` | Type — return type of `useMarkdownRenderer()`. |
358
+ | `MarkdownAstNode` | Class — AST node with `id`, `type`, `componentState`, `editingState`. |
359
+ | `MarkdownAstNodeType` | Enum — `PARAGRAPH`, `HEADLINE1`, `HEADLINE2`, `HEADLINE3`, `IMAGE`, `LIST`, `FILE`. |
360
+ | `ImageNode` | Type alias — `MarkdownAstNode<MarkdownModuleImageState>`. |
361
+ | `TextNode` | Type alias — `MarkdownAstNode<MarkdownModuleTextState>`. |
362
+ | `FileNode` | Type alias — `MarkdownAstNode<MarkdownModuleFileState>`. |
363
+ | `TextishNodeType` | Type — union of `PARAGRAPH \| HEADLINE1 \| HEADLINE2 \| HEADLINE3 \| LIST`. |
364
+ | `isTextNodeState(node)` | Type guard for text‑based nodes. |
365
+ | `isTextNodeType(type)` | Type guard for text‑based node types. |
366
+ | `RenderComponent<TState>` | Type — a Vue component that accepts `{ state: TState }` as props. |
367
+ | `RenderStateMap` | Interface — maps each `MarkdownNodeType` to its state class for strict typing. |
368
+ | `MarkdownModuleImageState` | Class — state for image nodes (`src`, `alt`, `caption`). |
369
+ | `MarkdownModuleFileState` | Class — state for file nodes (`url`, `fileName`, `fileSize`, `mimeType`, `uploadError`). |
265
370
 
266
371
  ---
267
372
 
@@ -385,3 +490,122 @@ The dev server launches at `http://localhost:4010`. The entry point is `dev/App.
385
490
  ## License
386
491
 
387
492
  [ISC](LICENSE) © 2026 [danielgran](https://github.com/danielgran)
493
+
494
+ All components use scoped SCSS. To override styles, use **global CSS** with higher specificity, or Vue's `:deep()` combinator from a parent component.
495
+
496
+ ### CSS class reference
497
+
498
+ | Class | Applies to |
499
+ |---|---|
500
+ | `.markdown-editor` | Root editor container |
501
+ | `.markdown-editor-module` | Individual block wrapper — `.is-focused` when active |
502
+ | `.markdown-editor-module-controls` | Left control bar (drag handle + add/delete buttons) |
503
+ | `.markdown-editor-module-content` | Content area inside a block |
504
+ | `.markdown-editor-module-content-focused` | Content area when the block is focused |
505
+ | `.markdown-editor-focus-controls` | Row containing drag‑handle, delete, and add buttons |
506
+ | `.drag-handle` | SortableJS drag handle (⠿) |
507
+ | `.focus-control-btn` | Delete / Add buttons in the control bar |
508
+ | `.markdown-editor-context-menu` | Floating block context menu (`z-index: 1000`) |
509
+ | `.markdown-editor-context-menu-block-item` | Full‑width context menu button |
510
+ | `.markdown-editor-context-menu-inline-item` | Inline toolbar button (`.is-active` when toggled) |
511
+ | `.markdown-editor-modal-overlay` | Modal backdrop (`z-index: 9999`) |
512
+ | `.markdown-editor-modal` | Modal container |
513
+ | `.markdown-editor-modal-header` | Modal title bar |
514
+ | `.markdown-editor-modal-title` | Modal heading text |
515
+ | `.markdown-editor-modal-close` | Close (✕) button |
516
+ | `.markdown-editor-modal-body` | Modal content area |
517
+ | `.markdown-editor-modal-footer` | Modal action bar |
518
+ | `.markdown-editor-modal-button` | Base modal button |
519
+ | `.markdown-editor-modal-button-primary` | Primary (Save) button — blue |
520
+ | `.markdown-editor-modal-button-secondary` | Secondary (Cancel) button — gray |
521
+ | `.markdown-module-image` | Image block wrapper |
522
+ | `.markdown-module-image-form` | Image edit form inside the modal |
523
+ | `.markdown-module-image-form-field` | Form field group (label + input) |
524
+
525
+ ### Styling TipTap content
526
+
527
+ Each text‑based block hosts its own TinyMCE‑style TipTap editor. Target `.tiptap` inside a block's content area:
528
+
529
+ ```css
530
+ /* Make all TipTap editors use your font */
531
+ .markdown-editor-module-content .tiptap {
532
+ font-family: "Georgia", serif;
533
+ font-size: 1.1rem;
534
+ line-height: 1.8;
535
+ }
536
+ ```
537
+
538
+ ### Do's
539
+
540
+ - ✅ Import the stylesheet: `import "@grandaniel/vue-markdown-editor/style.css"`
541
+ - ✅ Use **global** (unscoped) CSS or `:deep()` from a parent to override styles
542
+ - ✅ Target `.tiptap` inside `.markdown-editor-module-content` for editor typography
543
+ - ✅ Use `z-index` values above `1000` / `9999` for anything that must layer **above** context menus and modals
544
+
545
+ ### Don'ts
546
+
547
+ - ❌ Don't rely on CSS custom properties — the editor uses hard‑coded Tailwind‑scale colors (grays and blues)
548
+ - ❌ Don't override `z-index` on `.markdown-editor-context-menu` or `.markdown-editor-modal-overlay` — it will break layering
549
+ - ❌ Don't use `display: contents` on `.markdown-editor-module` — it interferes with SortableJS drag logic
550
+ - ❌ Don't set `outline: none` on `.markdown-editor-module-content` globally — the focus ring is intentional for keyboard navigation
551
+
552
+ ---
553
+
554
+ ## Supply Chain Security
555
+
556
+ We take package integrity seriously.
557
+
558
+ | Measure | Status |
559
+ |---|---|
560
+ | **npm package provenance** | ✅ Enabled — every publish includes [provenance attestations](https://docs.npmjs.com/generating-provenance-statements) via GitHub Actions and Sigstore. |
561
+ | **CI/CD** | ✅ GitHub Actions runs `npm ci` → `npm test` → `npm run build` → publish on every push to `dev` and `main`. |
562
+ | **Prerelease tags** | ✅ Non‑main branches publish with a `dev` dist‑tag (e.g. `1.1.3-dev.abc1234`). |
563
+ | **Dependabot** | 🔜 Planned — automated dependency update PRs will be enabled via `.github/dependabot.yml`. |
564
+
565
+ To verify provenance locally:
566
+
567
+ ```bash
568
+ npm audit signatures
569
+ ```
570
+
571
+ ---
572
+
573
+ ## Contributing
574
+
575
+ We welcome contributions! Please follow the guidelines below.
576
+
577
+ ### PR Policy
578
+
579
+ 1. **Fork** the repository and create a feature branch off `dev`.
580
+ 2. **Keep changes focused** — one feature or fix per PR.
581
+ 3. **Add tests** for any new functionality. The project uses [Vitest](https://vitest.dev/) + [`@vue/test-utils`](https://test-utils.vuejs.org/).
582
+ 4. **Run the full check** before pushing:
583
+
584
+ ```bash
585
+ npm ci
586
+ npm run test
587
+ npm run type-check
588
+ npm run build
589
+ ```
590
+
591
+ 5. **Open a PR** against the `dev` branch with a clear description of what changed and why.
592
+
593
+ ### Dev setup
594
+
595
+ ```bash
596
+ # Clone and install
597
+ git clone https://github.com/danielgran/vue-markdown-editor.git
598
+ cd vue-markdown-editor
599
+ npm ci
600
+
601
+ # Start the dev server
602
+ npm run dev
603
+ ```
604
+
605
+ The dev server launches at `http://localhost:4010`. The entry point is `dev/App.vue` — a full showcase that demonstrates every editor feature: block types, drag & drop, image upload, context menus, keyboard navigation, and Markdown output serialization. Use it as a playground while developing.
606
+
607
+ ---
608
+
609
+ ## License
610
+
611
+ [ISC](LICENSE) © 2026 [danielgran](https://github.com/danielgran)