@grandaniel/vue-markdown-editor 1.1.3-dev.99079e2 → 1.1.3-dev.bc50199

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.
Files changed (2) hide show
  1. package/README.md +305 -5
  2. package/package.json +3 -2
package/README.md CHANGED
@@ -1,6 +1,41 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="docs/img/editor-1.png">
3
+ <img alt="vue-markdown-editor — A block-based, Notion-like markdown editor for Vue 3" src="docs/img/editor-1.png">
4
+ </picture>
5
+
1
6
  # @grandaniel/vue-markdown-editor
2
7
 
3
- Vue 3 markdown editor component library.
8
+ > A block-based, Notion-like Markdown editor for Vue 3. UI-first. Powered by [TipTap](https://tiptap.dev/).
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.
11
+
12
+ 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
+
14
+ ---
15
+
16
+ ## Table of Contents
17
+
18
+ - [Who uses it](#who-uses-it)
19
+ - [Installation](#installation)
20
+ - [Quick Start](#quick-start)
21
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
22
+ - [Image Upload](#image-upload)
23
+ - [API Reference](#api-reference)
24
+ - [Custom Styling](#custom-styling)
25
+ - [Supply Chain Security](#supply-chain-security)
26
+ - [Contributing](#contributing)
27
+ - [License](#license)
28
+
29
+ ---
30
+
31
+ ## Who uses it
32
+
33
+ | Project | How |
34
+ |---|---|
35
+ | **[heartbeat.systems](https://heartbeat.systems)** | Admin utility — content editors manage help articles, release notes, and in-app documentation through the block editor. |
36
+ | **[markdownstud.io](https://markdownstud.io)** | AI writing assistant — the editor serves as the primary composition surface where users draft, review, and polish AI-generated content. |
37
+
38
+ ---
4
39
 
5
40
  ## Installation
6
41
 
@@ -8,16 +43,34 @@ Vue 3 markdown editor component library.
8
43
  npm install @grandaniel/vue-markdown-editor
9
44
  ```
10
45
 
46
+ > **Peer dependency:** Vue `^3.5.0`
47
+ > **Node:** `>=22`
48
+
49
+ Import the component **and** the stylesheet:
50
+
51
+ ```ts
52
+ import { MarkdownEditor, useMarkdownEditor } from "@grandaniel/vue-markdown-editor";
53
+ import "@grandaniel/vue-markdown-editor/style.css";
54
+ ```
55
+
56
+ ---
57
+
11
58
  ## Quick Start
12
59
 
60
+ The editor is driven by a composable: `useMarkdownEditor()` creates the reactive state, and `<MarkdownEditor>` renders it.
61
+
13
62
  ```vue
14
63
  <script setup lang="ts">
15
64
  import { ref } from "vue";
16
- import { MarkdownEditor, useMarkdownEditor } from "@grandaniel/vue-markdown-editor";
17
- import "@grandaniel/vue-markdown-editor/dist/vue-markdown-editor.css";
65
+ import {
66
+ MarkdownEditor,
67
+ useMarkdownEditor,
68
+ type MarkdownAstNode,
69
+ } from "@grandaniel/vue-markdown-editor";
70
+ import "@grandaniel/vue-markdown-editor/style.css";
18
71
 
19
- const editor = useMarkdownEditor("# Hello\n\nStart writing...");
20
- const focusedNode = ref(null);
72
+ const editor = useMarkdownEditor("# Hello, world!\n\nStart writing here…");
73
+ const focusedNode = ref<MarkdownAstNode | null>(null);
21
74
  </script>
22
75
 
23
76
  <template>
@@ -27,3 +80,250 @@ const focusedNode = ref(null);
27
80
  />
28
81
  </template>
29
82
  ```
83
+
84
+ ### Reading the output
85
+
86
+ The composable keeps the raw Markdown in sync automatically. Read it at any time:
87
+
88
+ ```ts
89
+ console.log(editor.markdownContent.value);
90
+ // "# Hello, world!\n\nStart writing here…"
91
+ ```
92
+
93
+ You can also **programmatically set** the content:
94
+
95
+ ```ts
96
+ editor.markdownContent.value = "## New heading\n\nFresh content.";
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Keyboard Shortcuts
102
+
103
+ | Key | Action |
104
+ |---|---|
105
+ | <kbd>↑</kbd> / <kbd>↓</kbd> | Move focus between blocks |
106
+ | <kbd>Enter</kbd> | Split current block → insert new paragraph below |
107
+ | <kbd>Backspace</kbd> (empty block) | Delete the block, focus moves up |
108
+ | <kbd>Delete</kbd> (empty block) | Delete the block, focus stays at same index |
109
+ | Click blank area | Append a new empty paragraph at the bottom |
110
+
111
+ ### Auto type‑detection
112
+
113
+ Type `# `, `## `, or `### ` at the start of a paragraph and the block auto‑converts to the matching heading level.
114
+
115
+ ---
116
+
117
+ ## Image Upload
118
+
119
+ Images are first-class blocks with **src**, **alt text**, and **caption** fields. Right‑click any image → **Edit Attributes** to open the editing modal.
120
+
121
+ ### Paste‑to‑upload
122
+
123
+ 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
+
125
+ ```vue
126
+ <script setup lang="ts">
127
+ async function uploadImage(file: File): Promise<string> {
128
+ const formData = new FormData();
129
+ formData.append("image", file);
130
+
131
+ const res = await fetch("/api/upload", { method: "POST", body: formData });
132
+ const { url } = await res.json();
133
+ return url;
134
+ }
135
+ </script>
136
+
137
+ <template>
138
+ <MarkdownEditor
139
+ :editor="editor"
140
+ :image-upload-function="uploadImage"
141
+ />
142
+ </template>
143
+ ```
144
+
145
+ The serialized Markdown uses a custom block syntax for images:
146
+
147
+ ```markdown
148
+ """MarkdownModuleImage
149
+ src: https://example.com/photo.jpg
150
+ alt: A scenic mountain view
151
+ caption: Photo taken during the 2026 summit
152
+ """
153
+ ```
154
+
155
+ > **Note:** `imageUploadFunction` is optional. Without it, pasted images from the clipboard are ignored.
156
+
157
+ ---
158
+
159
+ ## API Reference
160
+
161
+ ### `useMarkdownEditor(initialContent?: string)`
162
+
163
+ Returns a reactive editor instance:
164
+
165
+ | Member | Type | Description |
166
+ |---|---|---|
167
+ | `markdownContent` | `Ref<string>` | Reactive raw Markdown. Read to serialize, write to load content. |
168
+ | `markdownNodes` | `Ref<MarkdownAstNode[]>` | Reactive array of AST nodes. |
169
+ | `deleteNode(index)` | `(index: number) => void` | Remove the node at `index`. |
170
+ | `addBlankNode(index?)` | `(index?: number) => number` | Insert an empty paragraph at `index` (or end). Returns the new index. |
171
+ | `addNodeWithType(index, type, content?)` | `(index: number, type: MarkdownNodeType, content?: string) => number` | Insert a typed node. Returns the new index. |
172
+ | `replaceNodeType(node, newType)` | `(node: MarkdownAstNode, type: MarkdownNodeType) => { newNode, index } \| null` | Convert between block types (e.g. paragraph → heading). |
173
+ | `moveNode(from, to)` | `(fromIndex: number, toIndex: number) => void` | Programmatically reorder a block. |
174
+
175
+ ### `MarkdownEditor` props
176
+
177
+ | Prop | Type | Required | Description |
178
+ |---|---|---|---|
179
+ | `editor` | `MarkdownEditorInstance` | ✓ | Instance from `useMarkdownEditor()`. |
180
+ | `focusedNode` | `MarkdownAstNode \| null` | — | For `v-model:focused-node` tracking. |
181
+ | `imageUploadFunction` | `(file: File) => Promise<string>` | — | Async callback for paste‑to‑upload. |
182
+
183
+ ### `MarkdownEditor` emits
184
+
185
+ | Event | Payload | Description |
186
+ |---|---|---|
187
+ | `update:focused-node` | `MarkdownAstNode \| null` | Fires when focus moves to a new block. |
188
+
189
+ ### `MarkdownEditor` slots
190
+
191
+ | Slot | Description |
192
+ |---|---|
193
+ | `after-controls` | Injected inside every block, after the drag‑handle / add / delete controls. |
194
+
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
+
208
+ ---
209
+
210
+ ## Custom Styling
211
+
212
+ All components use scoped SCSS. To override styles, use **global CSS** with higher specificity, or Vue's `:deep()` combinator from a parent component.
213
+
214
+ ### CSS class reference
215
+
216
+ | Class | Applies to |
217
+ |---|---|
218
+ | `.markdown-editor` | Root editor container |
219
+ | `.markdown-editor-module` | Individual block wrapper — `.is-focused` when active |
220
+ | `.markdown-editor-module-controls` | Left control bar (drag handle + add/delete buttons) |
221
+ | `.markdown-editor-module-content` | Content area inside a block |
222
+ | `.markdown-editor-module-content-focused` | Content area when the block is focused |
223
+ | `.markdown-editor-focus-controls` | Row containing drag‑handle, delete, and add buttons |
224
+ | `.drag-handle` | SortableJS drag handle (⠿) |
225
+ | `.focus-control-btn` | Delete / Add buttons in the control bar |
226
+ | `.markdown-editor-context-menu` | Floating block context menu (`z-index: 1000`) |
227
+ | `.markdown-editor-context-menu-block-item` | Full‑width context menu button |
228
+ | `.markdown-editor-context-menu-inline-item` | Inline toolbar button (`.is-active` when toggled) |
229
+ | `.markdown-editor-modal-overlay` | Modal backdrop (`z-index: 9999`) |
230
+ | `.markdown-editor-modal` | Modal container |
231
+ | `.markdown-editor-modal-header` | Modal title bar |
232
+ | `.markdown-editor-modal-title` | Modal heading text |
233
+ | `.markdown-editor-modal-close` | Close (✕) button |
234
+ | `.markdown-editor-modal-body` | Modal content area |
235
+ | `.markdown-editor-modal-footer` | Modal action bar |
236
+ | `.markdown-editor-modal-button` | Base modal button |
237
+ | `.markdown-editor-modal-button-primary` | Primary (Save) button — blue |
238
+ | `.markdown-editor-modal-button-secondary` | Secondary (Cancel) button — gray |
239
+ | `.markdown-module-image` | Image block wrapper |
240
+ | `.markdown-module-image-form` | Image edit form inside the modal |
241
+ | `.markdown-module-image-form-field` | Form field group (label + input) |
242
+
243
+ ### Styling TipTap content
244
+
245
+ Each text‑based block hosts its own TinyMCE‑style TipTap editor. Target `.tiptap` inside a block's content area:
246
+
247
+ ```css
248
+ /* Make all TipTap editors use your font */
249
+ .markdown-editor-module-content .tiptap {
250
+ font-family: "Georgia", serif;
251
+ font-size: 1.1rem;
252
+ line-height: 1.8;
253
+ }
254
+ ```
255
+
256
+ ### Do's
257
+
258
+ - ✅ Import the stylesheet: `import "@grandaniel/vue-markdown-editor/style.css"`
259
+ - ✅ Use **global** (unscoped) CSS or `:deep()` from a parent to override styles
260
+ - ✅ Target `.tiptap` inside `.markdown-editor-module-content` for editor typography
261
+ - ✅ Use `z-index` values above `1000` / `9999` for anything that must layer **above** context menus and modals
262
+
263
+ ### Don'ts
264
+
265
+ - ❌ Don't rely on CSS custom properties — the editor uses hard‑coded Tailwind‑scale colors (grays and blues)
266
+ - ❌ Don't override `z-index` on `.markdown-editor-context-menu` or `.markdown-editor-modal-overlay` — it will break layering
267
+ - ❌ Don't use `display: contents` on `.markdown-editor-module` — it interferes with SortableJS drag logic
268
+ - ❌ Don't set `outline: none` on `.markdown-editor-module-content` globally — the focus ring is intentional for keyboard navigation
269
+
270
+ ---
271
+
272
+ ## Supply Chain Security
273
+
274
+ We take package integrity seriously.
275
+
276
+ | Measure | Status |
277
+ |---|---|
278
+ | **npm package provenance** | ✅ Enabled — every publish includes [provenance attestations](https://docs.npmjs.com/generating-provenance-statements) via GitHub Actions and Sigstore. |
279
+ | **CI/CD** | ✅ GitHub Actions runs `npm ci` → `npm test` → `npm run build` → publish on every push to `dev` and `main`. |
280
+ | **Prerelease tags** | ✅ Non‑main branches publish with a `dev` dist‑tag (e.g. `1.1.3-dev.abc1234`). |
281
+ | **Dependabot** | 🔜 Planned — automated dependency update PRs will be enabled via `.github/dependabot.yml`. |
282
+
283
+ To verify provenance locally:
284
+
285
+ ```bash
286
+ npm audit signatures
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Contributing
292
+
293
+ We welcome contributions! Please follow the guidelines below.
294
+
295
+ ### PR Policy
296
+
297
+ 1. **Fork** the repository and create a feature branch off `dev`.
298
+ 2. **Keep changes focused** — one feature or fix per PR.
299
+ 3. **Add tests** for any new functionality. The project uses [Vitest](https://vitest.dev/) + [`@vue/test-utils`](https://test-utils.vuejs.org/).
300
+ 4. **Run the full check** before pushing:
301
+
302
+ ```bash
303
+ npm ci
304
+ npm run test
305
+ npm run type-check
306
+ npm run build
307
+ ```
308
+
309
+ 5. **Open a PR** against the `dev` branch with a clear description of what changed and why.
310
+
311
+ ### Dev setup
312
+
313
+ ```bash
314
+ # Clone and install
315
+ git clone https://github.com/danielgran/vue-markdown-editor.git
316
+ cd vue-markdown-editor
317
+ npm ci
318
+
319
+ # Start the dev server
320
+ npm run dev
321
+ ```
322
+
323
+ 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.
324
+
325
+ ---
326
+
327
+ ## License
328
+
329
+ [ISC](LICENSE) © 2026 [danielgran](https://github.com/danielgran)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grandaniel/vue-markdown-editor",
3
- "version": "1.1.3-dev.99079e2",
3
+ "version": "1.1.3-dev.bc50199",
4
4
  "description": "Vue 3 markdown editor component library.",
5
5
  "main": "./dist/vue-markdown-editor.mjs",
6
6
  "module": "./dist/vue-markdown-editor.mjs",
@@ -53,7 +53,8 @@
53
53
  "build": "vue-tsc -p tsconfig.build.json && vite build",
54
54
  "type-check": "vue-tsc --noEmit",
55
55
  "preview": "vite preview",
56
- "test": "vitest run"
56
+ "test": "vitest run",
57
+ "test:coverage": "vitest run --coverage"
57
58
  },
58
59
  "license": "ISC",
59
60
  "peerDependencies": {