@danxbot/ui 7.6.0 → 7.7.1

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.
@@ -0,0 +1,153 @@
1
+ import { type LocalImageState } from "./image-decoration";
2
+ import { type SourceSelection } from "./position";
3
+ import type { MarkdownSurface } from "./surface";
4
+ /**
5
+ * Stores one image and says where it now lives.
6
+ *
7
+ * `signal` aborts when the editor unmounts; an uploader that ignores it still
8
+ * works, its result is simply discarded. The returned `url` must be one an
9
+ * `<img>` may load (`http(s)`, a relative URL, or `data:image/*` —
10
+ * `sanitizeImageSrc`) and never another `blob:`; anything else is reported
11
+ * as a failed upload rather than written into the document.
12
+ */
13
+ export type ImageUploader = (file: File, options: {
14
+ signal: AbortSignal;
15
+ }) => Promise<{
16
+ url: string;
17
+ }>;
18
+ /**
19
+ * What the consumer needs to decide whether it may save (PLN-10 R-1).
20
+ *
21
+ * Counts IMAGES IN THE CURRENT DOCUMENT, one per construct — the same unit
22
+ * `findUnstoredImages` returns, so `unstored === findUnstoredImages(value).length`
23
+ * always. `uploading` are being stored, `failed` cannot be stored without the
24
+ * author acting (a rejected upload, or a `blob:` this editor did not mint), and
25
+ * `unstored` is every image not stored yet — `uploading + failed` plus those
26
+ * with no storage configured. `unstored > 0` means the value must not be saved.
27
+ */
28
+ export interface ImageUploadState {
29
+ uploading: number;
30
+ failed: number;
31
+ unstored: number;
32
+ }
33
+ /** The editor's own view: the public counts, plus what its chrome needs. */
34
+ export interface ImagePipelineSnapshot extends ImageUploadState {
35
+ /** Images that cannot be stored because there is no uploader. */
36
+ local: number;
37
+ /** Failed images this editor still holds the File for, so Retry can work. */
38
+ retryable: number;
39
+ }
40
+ /**
41
+ * The browser facilities the pipeline touches, injectable so a test never
42
+ * needs a real object URL or a real network.
43
+ */
44
+ export interface ImagePipelineEnvironment {
45
+ createObjectURL: (file: File) => string;
46
+ revokeObjectURL: (url: string) => void;
47
+ /**
48
+ * Settles once `url` has loaded or failed to. The swap waits for it so the
49
+ * stored picture is already in the cache when it replaces the local one —
50
+ * otherwise the image blinks empty for as long as the download takes.
51
+ */
52
+ preload: (url: string) => Promise<unknown>;
53
+ /**
54
+ * The longest the swap waits for `preload`. A stored URL whose download
55
+ * stalls would otherwise hold an image that IS stored in "uploading" for
56
+ * ever — save disabled, page-leave guarded — for want of a cache warm-up.
57
+ */
58
+ preloadTimeoutMs: number;
59
+ }
60
+ export interface ImagePipelineOptions {
61
+ /** The uploader in force right now, or null when there is no storage. */
62
+ uploader: () => ImageUploader | null;
63
+ /** Called whenever the snapshot changes, and once when attached. */
64
+ onChange: (snapshot: ImagePipelineSnapshot) => void;
65
+ environment?: Partial<ImagePipelineEnvironment>;
66
+ }
67
+ export declare class ImagePipeline {
68
+ private readonly options;
69
+ private surface;
70
+ private readonly entries;
71
+ private readonly env;
72
+ private last;
73
+ private disposed;
74
+ constructor(options: ImagePipelineOptions);
75
+ /**
76
+ * The surface's `ImageResolver`: the state of an object URL this editor
77
+ * minted, or null for anything else. An arrow property so it can be handed
78
+ * to the surface before the surface exists.
79
+ */
80
+ readonly resolve: (src: string) => LocalImageState | null;
81
+ /** Connects the surface this pipeline edits, and reports the first snapshot. */
82
+ attach(surface: MarkdownSurface): void;
83
+ /**
84
+ * Binds the overlay buttons' Retry and Remove. Returns the unbind function.
85
+ *
86
+ * ONE DELEGATED LISTENER on the root, because the buttons live inside
87
+ * decorations the reconciler rebuilds whenever an image's state changes — a
88
+ * listener on a button would be thrown away with it.
89
+ */
90
+ bind(): () => void;
91
+ /**
92
+ * THE pipeline: every image file that enters the editor comes through here —
93
+ * paste, drop and the file picker alike.
94
+ *
95
+ * `at` is where the images go: the caret for a paste or a pick, the drop
96
+ * point for a drop. With none (an editor that was never focused), they go at
97
+ * the end of the document — never at offset 0 in front of whatever the
98
+ * author wrote first. Non-image files are ignored; the picker's `accept`
99
+ * is a hint the user can override, not a guarantee.
100
+ *
101
+ * Returns the object URLs it inserted, in order.
102
+ */
103
+ insert(files: readonly File[], at: SourceSelection | null): string[];
104
+ /**
105
+ * Uploads a failed image again, with the SAME File it was inserted with.
106
+ *
107
+ * With no uploader configured any more, the image becomes local-only rather
108
+ * than staying "failed" — the honest state for something nothing can store.
109
+ */
110
+ retry(src: string): void;
111
+ /** Retries every failed image still in the document. */
112
+ retryFailed(): void;
113
+ /**
114
+ * Starts uploading every local-only image once an uploader exists.
115
+ *
116
+ * The component calls this when its uploader changes: an image pasted
117
+ * before an app's storage provider mounted should not stay unsaveable when
118
+ * the storage it needed arrives a moment later.
119
+ */
120
+ uploaderChanged(): void;
121
+ /**
122
+ * Recomputes the snapshot from the CURRENT document, and releases object
123
+ * URLs nothing can reach any more. Called after every change to the source.
124
+ *
125
+ * THE COUNTS COME FROM `findUnstoredImages`, the same exported function a
126
+ * consumer's save path calls — one detection source, never a second opinion
127
+ * the two could disagree on (DX-3134).
128
+ *
129
+ * AN OBJECT URL IS RELEASED WHEN ITS IMAGE IS GONE AND SETTLED: not in the
130
+ * document, not in any undo or redo entry (Ctrl+Z could bring it back), and
131
+ * not mid-upload (the upload will swap it wherever it still is). Revoking one
132
+ * any earlier turns an undo into a broken image; never revoking leaks the
133
+ * file's bytes for the life of the page.
134
+ */
135
+ sync(): void;
136
+ /**
137
+ * Stops everything: aborts every upload in flight and releases every object
138
+ * URL. The editor is going away, so nothing it minted can be drawn anywhere
139
+ * again — no other editor will render another's `blob:`.
140
+ */
141
+ dispose(): void;
142
+ private upload;
143
+ /**
144
+ * Resolves when `work` settles, `preloadTimeoutMs` passes, or `signal`
145
+ * aborts — never rejects. Every timer and listener is cleaned up.
146
+ */
147
+ private settled;
148
+ private release;
149
+ /** An image's state moved: redraw its decoration and re-report. */
150
+ private changed;
151
+ private removeImage;
152
+ private requireSurface;
153
+ }
@@ -0,0 +1,68 @@
1
+ /** An image whose picture exists only in this page's memory. */
2
+ export interface UnstoredImage {
3
+ /** The `blob:` URL exactly as the source spells it. */
4
+ src: string;
5
+ alt: string;
6
+ /** The whole `![alt](src)` construct, as source offsets. */
7
+ start: number;
8
+ end: number;
9
+ }
10
+ /** Whether an image src names a picture that has not been stored anywhere. */
11
+ export declare function isUnstoredImageSrc(src: string): boolean;
12
+ /**
13
+ * Every image in `markdown` whose src is a `blob:` URL, in source order.
14
+ *
15
+ * THE SINGLE DETECTION SOURCE. The editor's own warning strip, its
16
+ * `onImageUploadStateChange` counts and its `beforeunload` guard all read this
17
+ * function, and it is exported so a consumer's save path can re-check the
18
+ * exact value it is about to persist with the same answer (DX-3134, PLN-10
19
+ * "Pending-state event").
20
+ *
21
+ * Read from the PARSER, not with a regex, for the reason `constructAt` gives:
22
+ * the parser has already decided what is an image, including the cases a
23
+ * pattern gets wrong — a `![x](blob:…)` inside a code span is prose, not a
24
+ * picture, and saving it loses nothing.
25
+ */
26
+ export declare function findUnstoredImages(markdown: string): UnstoredImage[];
27
+ /**
28
+ * A URL written so the destination parses as exactly that URL.
29
+ *
30
+ * Whitespace ends a destination, an unbalanced parenthesis ends the construct,
31
+ * and a backslash before punctuation is an escape markdown would consume — so
32
+ * all of them are percent-encoded, which is the same URL to every server and
33
+ * the only spelling that survives being written into markdown.
34
+ */
35
+ export declare function markdownDestination(url: string): string;
36
+ /** `![alt](src)`, with both halves escaped so they read back as themselves. */
37
+ export declare function imageMarkdown(src: string, alt: string): string;
38
+ /**
39
+ * The alt text a file brings with it: its name without the extension.
40
+ *
41
+ * A pasted screenshot is called `image.png` by every browser, so the result is
42
+ * "image" — not a description, but an honest placeholder the author can see
43
+ * and replace, which is better than the empty alt that makes an image
44
+ * invisible to a screen reader.
45
+ */
46
+ export declare function altFromFileName(name: string): string;
47
+ /** The result of replacing every occurrence of one string with another. */
48
+ export interface Substitution {
49
+ source: string;
50
+ /** Carries an offset in the old source to the same place in the new one. */
51
+ map: (offset: number) => number;
52
+ }
53
+ /**
54
+ * Replaces EVERY occurrence of `from` in `source` with `to`, or returns null
55
+ * when there is none.
56
+ *
57
+ * A PLAIN TEXT REPLACEMENT, deliberately, and only safe because of what `from`
58
+ * is: an object URL this editor minted, which embeds a random UUID and was
59
+ * never typed by anyone. Every occurrence of it is that image, whatever
60
+ * construct holds it now — including one the author has since turned into a
61
+ * link, which a parser-driven swap would leave pointing at a dead handle in
62
+ * the saved value (DX-3134).
63
+ *
64
+ * `map` is what lets the caret survive the swap: offsets before an occurrence
65
+ * stay, offsets after one shift by the length change, and an offset that was
66
+ * INSIDE a replaced URL lands at the end of its replacement.
67
+ */
68
+ export declare function substituteAll(source: string, from: string, to: string): Substitution | null;
@@ -1,3 +1,5 @@
1
+ import { type SourceSelection } from "./position";
2
+ import { type EditorRequest } from "./hotkeys";
1
3
  import type { MarkdownSurface } from "./surface";
2
4
  export interface InputOptions {
3
5
  /**
@@ -8,8 +10,25 @@ export interface InputOptions {
8
10
  * broken, and this is how that gets noticed before a user reports it.
9
11
  */
10
12
  onUnhandled?: (inputType: string) => void;
13
+ /**
14
+ * Receives the image files of a paste or a drop, and where they go (DX-3134).
15
+ *
16
+ * `at` is the caret for a paste and the drop point for a drop, already on a
17
+ * caret stop. Without this option files are not handled here at all, and a
18
+ * paste or drop behaves exactly as it always did.
19
+ */
20
+ onImageFiles?: (files: File[], at: SourceSelection | null) => void;
21
+ /**
22
+ * Receives a hotkey that asks the EDITOR to do something rather than making
23
+ * an edit — the Insert-image chord opening a file picker (DX-3134). A
24
+ * request with no receiver lets the key fall through, like a command that
25
+ * declined.
26
+ */
27
+ onRequest?: (request: EditorRequest) => void;
11
28
  }
12
29
  /**
13
30
  * Binds the input layer to a surface. Returns the unbind function.
14
31
  */
15
32
  export declare function attachInput(surface: MarkdownSurface, options?: InputOptions): () => void;
33
+ /** Marks the surface while image files are dragged over it, for the affordance. */
34
+ export declare const DROP_TARGET_ATTR = "data-md-drop-target";
@@ -1,4 +1,5 @@
1
1
  import type { PNode } from "./project";
2
+ import type { ImageResolver } from "./image-decoration";
2
3
  /**
3
4
  * Thrown when a patch would have had to disturb the node the browser is
4
5
  * composing into.
@@ -17,6 +18,11 @@ export interface PatchOptions {
17
18
  * removed; a patch that would need to do any of those throws.
18
19
  */
19
20
  protect?: Text | null;
21
+ /**
22
+ * Which image srcs the calling editor minted, and their state (DX-3134).
23
+ * Absent means "none" — every `blob:` is then refused, as it always was.
24
+ */
25
+ images?: ImageResolver;
20
26
  }
21
27
  export interface PatchStats {
22
28
  /** Text nodes whose `data` was assigned. */
@@ -2,6 +2,7 @@ import { type CaretMap } from "./caret-map";
2
2
  import { type Projection, type ProjectOptions } from "./project";
3
3
  import { type SourceSelection } from "./position";
4
4
  import { type EditKind, type HistoryOptions } from "./history";
5
+ import type { ImageResolver } from "./image-decoration";
5
6
  export interface SurfaceOptions {
6
7
  /** Called after every committed change, with the new source. */
7
8
  onChange?: (source: string) => void;
@@ -20,6 +21,12 @@ export interface SurfaceOptions {
20
21
  * eats the user's text if it is swallowed.
21
22
  */
22
23
  onDesync?: (detail: string) => void;
24
+ /**
25
+ * Which object URLs this surface's editor minted, and what state each is in
26
+ * (DX-3134). Consulted on every render, so a state change only needs
27
+ * `refresh()`. Absent: no `blob:` src ever renders.
28
+ */
29
+ images?: ImageResolver;
23
30
  }
24
31
  export interface Splice {
25
32
  start: number;
@@ -30,6 +37,17 @@ export declare class MarkdownSurface {
30
37
  readonly root: HTMLElement;
31
38
  private source;
32
39
  private readonly options;
40
+ /**
41
+ * Upload swaps that arrived mid-composition, applied at `compositionend`.
42
+ *
43
+ * DX-3134: the composing text node carries the source range it came from,
44
+ * and `reconcileComposed` writes the composed text back into exactly that
45
+ * range. A swap applied mid-composition changes the length of the source
46
+ * before the node — the range would then point at the wrong characters, and
47
+ * the composed text would land inside a URL. So the swap waits, and runs
48
+ * right after the composition has been read back.
49
+ */
50
+ private pendingSubstitutions;
33
51
  /** The text node the browser is composing into, if any. */
34
52
  private composing;
35
53
  /** A projection that arrived while composing, applied at `compositionend`. */
@@ -82,6 +100,51 @@ export declare class MarkdownSurface {
82
100
  * exactly right or exactly wrong, with nothing in between.
83
101
  */
84
102
  apply(splice: Splice, select?: SourceSelection, kind?: EditKind): void;
103
+ /**
104
+ * Replaces every occurrence of `from` with `to` — in the document AND in
105
+ * every undo and redo entry — without recording an edit.
106
+ *
107
+ * THIS IS THE UPLOAD SWAP (DX-3134), and it is deliberately not an edit.
108
+ * When an image finishes uploading, its object URL is replaced by the stored
109
+ * URL. Recorded as an ordinary edit, Ctrl+Z would hand the object URL back —
110
+ * and a document holding it is a document that cannot be saved, for an image
111
+ * that WAS saved. Rewriting the history too means no undo, redo or later
112
+ * edit can resurrect a URL that no longer means anything.
113
+ *
114
+ * The caret stays where it was in the text the user can see: it is mapped
115
+ * through the substitution rather than moved, and only written back if it was
116
+ * in this surface to begin with — an upload finishing must never pull focus
117
+ * out of whatever the user moved on to. `onChange` fires, because the value
118
+ * genuinely changed and the consumer is the one who saves it.
119
+ *
120
+ * WHILE COMPOSING, the swap is queued and applied at `compositionend` — see
121
+ * `pendingSubstitutions`. A caller that needs to know when it has landed
122
+ * watches for `from` to leave the source (`onChange` fires when it does).
123
+ */
124
+ substitute(from: string, to: string): void;
125
+ /**
126
+ * The selection, but only while this surface HAS FOCUS.
127
+ *
128
+ * DX-3134: a render that is not the user's own edit — an upload finishing, an
129
+ * image's state moving — writes the caret back only when the author is
130
+ * actually in this document. The document selection can still sit inside the
131
+ * surface after focus has gone to a button or another field, and writing a
132
+ * selection into an editing host is one of the things that pulls focus back
133
+ * into it: an upload landing would then yank the author out of whatever they
134
+ * moved on to.
135
+ */
136
+ private focusedSelection;
137
+ /** Whether any undo or redo entry still holds `text` — see `History.includes`. */
138
+ remembers(text: string): boolean;
139
+ /**
140
+ * Re-renders the current document, keeping the caret.
141
+ *
142
+ * For a change the SOURCE does not show: an image's upload state lives in
143
+ * the resolver, so the decoration has to be redrawn when it moves (DX-3134).
144
+ * Unchanged text nodes are not touched, so the caret is only rewritten when
145
+ * this surface has focus — and then to the offsets it already had.
146
+ */
147
+ refresh(): void;
85
148
  /** Replaces the whole document, e.g. when the consumer's value prop changes. */
86
149
  setSource(source: string): void;
87
150
  get canUndo(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "7.6.0",
3
+ "version": "7.7.1",
4
4
  "type": "module",
5
5
  "description": "Danxbot — a domain-agnostic React design system with motion as a first-class primitive.",
6
6
  "license": "MIT",