blixify-ui-web-mcp 0.1.28 → 0.1.29
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.
|
@@ -162,6 +162,7 @@ On error the editor draws a red outline (`border border-red-500 rounded`) — th
|
|
|
162
162
|
| `compress` | `{ maxFileSize?: number, fileQuality?: number}` | `null` | Image compression settings. `maxFileSize` in MB (default: 30), `fileQuality` 0.0-1.0 (default: 0.8) |
|
|
163
163
|
| `lib` | `{ axios?: any, mermaid?: () => Promise<any> }` | `null` | External libraries — axios for API calls; `mermaid` loader (e.g. `() => import("mermaid")`) to draw diagrams |
|
|
164
164
|
| `uploadEndpoint` | `string` | `null` | API endpoint for uploading media files |
|
|
165
|
+
| `onHeadingsChange` | `(headings: MarkdownHeading[]) => void` | `null` | Reports the document's `h1`–`h3` outline whenever it changes — feed it to `MarkdownOutline` |
|
|
165
166
|
|
|
166
167
|
##### Ref Methods
|
|
167
168
|
|
|
@@ -260,6 +261,85 @@ the block. The `/` menu gains **Diagram**, which inserts a starter flowchart.
|
|
|
260
261
|
- **The source is what is stored**, as a normal fenced block, so search and
|
|
261
262
|
anything reading the raw markdown still see the diagram's text.
|
|
262
263
|
|
|
264
|
+
##### Heading ids
|
|
265
|
+
|
|
266
|
+
Both renderers give `h1`–`h3` a slug id and report the outline through
|
|
267
|
+
`onHeadingsChange`, so a host can build its own navigation — a sticky rail, tabs,
|
|
268
|
+
a jump menu — without parsing the markdown itself or reaching into the rendered
|
|
269
|
+
DOM to find anchors.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
const [headings, setHeadings] = useState<MarkdownHeading[]>([]);
|
|
273
|
+
|
|
274
|
+
<MarkdownInput value={body} onChange={handleChange} onHeadingsChange={setHeadings} />;
|
|
275
|
+
|
|
276
|
+
// each entry is { id, level, text }; the id is on the rendered heading
|
|
277
|
+
document.getElementById(headings[0].id)?.scrollIntoView({ behavior: "smooth" });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
What the rail looks like is deliberately left out of this component — an outline in
|
|
281
|
+
a dashboard's own chrome is that app's design problem, and the only thing it needs
|
|
282
|
+
from here is where the sections are.
|
|
283
|
+
|
|
284
|
+
- **Ids are derived, never stored.** Slugs of the heading text, computed at
|
|
285
|
+
render, so a document written before any of this existed gets them with no
|
|
286
|
+
migration and no change to a single stored byte.
|
|
287
|
+
- **Each renderer carries the id the way its own DOM survives.** `MarkdownStatic`
|
|
288
|
+
injects it into the html string, because React re-commits that string on
|
|
289
|
+
re-render and wipes anything written to the mounted nodes afterwards. The editor
|
|
290
|
+
uses a ProseMirror decoration, which is rebuilt from the document on every
|
|
291
|
+
redraw and never enters the document — a node attribute would reach
|
|
292
|
+
`getHTML()`, which is turndown's input, and a DOM write is undone on the next
|
|
293
|
+
redraw.
|
|
294
|
+
- **One parser for both.** The editor and `MarkdownStatic` draw headings through
|
|
295
|
+
completely different paths, and an outline read from either would only be right
|
|
296
|
+
there. Both are labelled against the markdown instead, so a note and the page
|
|
297
|
+
published from it can never list different sections.
|
|
298
|
+
- **Fenced blocks are skipped**, so a `#` comment in a ```` ```bash ```` block or
|
|
299
|
+
a ```` ```mermaid ```` diagram is not mistaken for a heading. A closing fence
|
|
300
|
+
must be at least as long as the one that opened it.
|
|
301
|
+
- **Repeated headings get distinct ids** — `overview`, `overview-2`. Two
|
|
302
|
+
"Overview" sections in a long page are ordinary, and sharing an id would send
|
|
303
|
+
every click to the first.
|
|
304
|
+
|
|
305
|
+
##### Resizing an image
|
|
306
|
+
|
|
307
|
+
Hover or select an image in the editor and drag the handle on either edge. A
|
|
308
|
+
double click returns it to its natural size.
|
|
309
|
+
|
|
310
|
+
- **Only a width is stored, never a height.** The image keeps its own aspect
|
|
311
|
+
ratio, which is what makes one axis enough and makes it impossible to save a
|
|
312
|
+
distorted image.
|
|
313
|
+
- **A resized image is stored as html**, `<img src="…" alt="…" width="420" />`,
|
|
314
|
+
because markdown has no syntax for a size — the same route `<video>`, the file
|
|
315
|
+
attachment and the space embed take, and markdown-it runs with `html: true` so
|
|
316
|
+
it parses back on load. An image nobody resized still serialises as
|
|
317
|
+
``, so every document written before this existed round-trips
|
|
318
|
+
byte-for-byte.
|
|
319
|
+
- **The handle starts the drag in the capture phase.** ProseMirror handles pointer
|
|
320
|
+
events on the editor element to move the selection and to drag a node around the
|
|
321
|
+
document, and its listener sits on an ancestor of the handle — so a bubble-phase
|
|
322
|
+
handler never runs at all.
|
|
323
|
+
- **The document is edited once per drag, on release.** Writing the width on
|
|
324
|
+
every pointer move would put one transaction per mouse move into the history —
|
|
325
|
+
undo would take fifty presses — and fire the host's autosave just as often.
|
|
326
|
+
- **Capped at the width of the column** it sits in — measured from the editor
|
|
327
|
+
element, not from the node view wrapper, which is `fit-content` and therefore
|
|
328
|
+
exactly the image's current width.
|
|
329
|
+
- **`MarkdownStatic` needs nothing.** The width attribute is html a browser
|
|
330
|
+
already honours, under the `max-width: 100%` prose already sets, so a published
|
|
331
|
+
page shows the size the author chose and still fits a phone.
|
|
332
|
+
- **Works on a touch screen.** Two things are load-bearing and neither is
|
|
333
|
+
obvious: `touch-action: none` on the handle, or the browser claims the gesture
|
|
334
|
+
as a scroll and cancels the pointer stream a couple of dozen pixels in — which
|
|
335
|
+
reads as the handle being unresponsive rather than as a scroll — and a hit area
|
|
336
|
+
far larger than the 6px bar it draws. The drag also takes pointer capture, so
|
|
337
|
+
it keeps tracking after the finger leaves the handle, and a touch on the image
|
|
338
|
+
reveals the handles, since a phone never fires hover.
|
|
339
|
+
- **The tiptap extension's own `resize` option is deliberately not used.** It
|
|
340
|
+
commits a height as well as a width, which is a distorted image waiting to
|
|
341
|
+
happen, and it hides the image until `onload` fires.
|
|
342
|
+
|
|
263
343
|
##### The slash menu
|
|
264
344
|
|
|
265
345
|
Only a real edit opens it. The detection also runs on selection changes so that
|
|
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|
|
2
2
|
import { MarkdownInput as MarkdownInputImpl, MarkdownStatic } from ".";
|
|
3
3
|
import { StorySection } from "../../storyUtils";
|
|
4
4
|
|
|
5
|
-
const meta = { title: "Input/Markdown
|
|
5
|
+
const meta = { title: "Input/Markdown/Input" };
|
|
6
6
|
export default meta;
|
|
7
7
|
|
|
8
8
|
// INFO: the host passes mermaid in; the library never imports it
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { MarkdownInput, MarkdownStatic } from ".";
|
|
3
|
+
import { StorySection } from "../../storyUtils";
|
|
4
|
+
|
|
5
|
+
const meta = { title: "Input/Markdown/Image Resize" };
|
|
6
|
+
export default meta;
|
|
7
|
+
|
|
8
|
+
// INFO: a neutral remote placeholder — nothing here points at a project bucket
|
|
9
|
+
const IMAGE = "https://placehold.co/900x500/png";
|
|
10
|
+
|
|
11
|
+
const SIZED = [
|
|
12
|
+
"An image nobody has touched is stored as plain markdown:",
|
|
13
|
+
"",
|
|
14
|
+
``,
|
|
15
|
+
"",
|
|
16
|
+
"One that has been resized carries its width as html:",
|
|
17
|
+
"",
|
|
18
|
+
`<img src="${IMAGE}" alt="A wide placeholder" width="320" />`,
|
|
19
|
+
].join("\n");
|
|
20
|
+
|
|
21
|
+
export const Editor = () => {
|
|
22
|
+
const [value, setValue] = useState<string | undefined>(SIZED);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<StorySection
|
|
27
|
+
title="Drag to resize"
|
|
28
|
+
description="Hover or select an image and drag the handle on either edge. Double click returns it to its natural size. The width is previewed while dragging and written to the document once, on release — watch the markdown below, which only changes when the drag ends."
|
|
29
|
+
>
|
|
30
|
+
<div className="w-full">
|
|
31
|
+
<MarkdownInput
|
|
32
|
+
id="markdown"
|
|
33
|
+
value={value ?? ""}
|
|
34
|
+
placeholder="Press / to see the available commands"
|
|
35
|
+
borderless={true}
|
|
36
|
+
onChange={(_id: string, next?: string) => setValue(next)}
|
|
37
|
+
enableImageUpload={true}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</StorySection>
|
|
41
|
+
<StorySection
|
|
42
|
+
title="Stored markdown"
|
|
43
|
+
description="A resized image is html because markdown has no syntax for a size; an untouched one stays , so documents written before resizing existed serialise exactly as they did. Only a width is ever stored — the image keeps its own aspect ratio."
|
|
44
|
+
>
|
|
45
|
+
<pre
|
|
46
|
+
data-testid="stored-markdown"
|
|
47
|
+
className="w-full overflow-x-auto rounded bg-gray-50 p-3 text-xs text-gray-700"
|
|
48
|
+
>
|
|
49
|
+
{value}
|
|
50
|
+
</pre>
|
|
51
|
+
</StorySection>
|
|
52
|
+
<StorySection
|
|
53
|
+
title="Read-only"
|
|
54
|
+
description="The same document with no editor. The width attribute is html a browser already honours under the max-width prose sets, so a published page shows the size the author chose and still fits a phone."
|
|
55
|
+
>
|
|
56
|
+
<MarkdownStatic label="" value={value ?? ""} />
|
|
57
|
+
</StorySection>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|