@particle-academy/react-fancy 5.42.1 → 5.44.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/CHANGELOG.md +65 -0
- package/dist/{Composer.types-CuSPnaOt.d.cts → PastePills.types-CDyotkUb.d.cts} +35 -82
- package/dist/{Composer.types-CuSPnaOt.d.ts → PastePills.types-CDyotkUb.d.ts} +35 -82
- package/dist/PastedFilesInput-CrgNoEBd.d.ts +141 -0
- package/dist/PastedFilesInput-DQ1eZaHk.d.cts +141 -0
- package/dist/{chunk-IPNJMHJ3.cjs → chunk-2QIGXSSZ.cjs} +105 -3
- package/dist/chunk-2QIGXSSZ.cjs.map +1 -0
- package/dist/{chunk-BZOU7VJQ.cjs → chunk-BR27AXFE.cjs} +16 -10
- package/dist/chunk-BR27AXFE.cjs.map +1 -0
- package/dist/{chunk-G4WYCWIQ.js → chunk-DMFJJ73P.js} +3 -3
- package/dist/{chunk-G4WYCWIQ.js.map → chunk-DMFJJ73P.js.map} +1 -1
- package/dist/{chunk-2VIDYJUG.js → chunk-EEWRLFTI.js} +107 -5
- package/dist/chunk-EEWRLFTI.js.map +1 -0
- package/dist/{chunk-SF5OAVGD.js → chunk-JZCAMPIA.js} +12 -4
- package/dist/chunk-JZCAMPIA.js.map +1 -0
- package/dist/{chunk-2JOWGYAJ.js → chunk-KYZNCVRD.js} +213 -179
- package/dist/chunk-KYZNCVRD.js.map +1 -0
- package/dist/{chunk-MXAVGOHW.js → chunk-L637KPEA.js} +12 -6
- package/dist/chunk-L637KPEA.js.map +1 -0
- package/dist/{chunk-6TZPM43Q.cjs → chunk-MSRYJETY.cjs} +219 -178
- package/dist/chunk-MSRYJETY.cjs.map +1 -0
- package/dist/{chunk-K4NHVCY6.cjs → chunk-S5ZYG5MG.cjs} +8 -8
- package/dist/{chunk-K4NHVCY6.cjs.map → chunk-S5ZYG5MG.cjs.map} +1 -1
- package/dist/{chunk-7VLRHXLQ.cjs → chunk-WN45ZE37.cjs} +171 -50
- package/dist/chunk-WN45ZE37.cjs.map +1 -0
- package/dist/{chunk-UUBHUSUP.cjs → chunk-Z3FIU77M.cjs} +14 -6
- package/dist/chunk-Z3FIU77M.cjs.map +1 -0
- package/dist/{chunk-NUPQJ5SJ.js → chunk-ZVXOL5H3.js} +173 -52
- package/dist/chunk-ZVXOL5H3.js.map +1 -0
- package/dist/composer.cjs +4 -4
- package/dist/composer.d.cts +93 -3
- package/dist/composer.d.ts +93 -3
- package/dist/composer.js +2 -2
- package/dist/{index-BsIBOwaK.d.cts → index-BF5qsQur.d.cts} +8 -2
- package/dist/{index-S4FC1yFK.d.ts → index-BZON-gXr.d.ts} +8 -2
- package/dist/index.cjs +70 -58
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +11 -11
- package/dist/input.cjs +6 -2
- package/dist/input.d.cts +34 -0
- package/dist/input.d.ts +34 -0
- package/dist/input.js +5 -1
- package/dist/inputs.cjs +8 -6
- package/dist/inputs.d.cts +1 -0
- package/dist/inputs.d.ts +1 -0
- package/dist/inputs.js +4 -2
- package/dist/json-editor.cjs +15 -13
- package/dist/json-editor.js +4 -2
- package/dist/paste-pills.cjs +65 -0
- package/dist/paste-pills.cjs.map +1 -0
- package/dist/paste-pills.d.cts +34 -0
- package/dist/paste-pills.d.ts +34 -0
- package/dist/paste-pills.js +8 -0
- package/dist/paste-pills.js.map +1 -0
- package/dist/prompt-input.cjs +3 -3
- package/dist/prompt-input.d.cts +2 -2
- package/dist/prompt-input.d.ts +2 -2
- package/dist/prompt-input.js +2 -2
- package/dist/tree-nav.cjs +4 -3
- package/dist/tree-nav.d.cts +14 -7
- package/dist/tree-nav.d.ts +14 -7
- package/dist/tree-nav.js +2 -1
- package/package.json +11 -1
- package/dist/chunk-2JOWGYAJ.js.map +0 -1
- package/dist/chunk-2VIDYJUG.js.map +0 -1
- package/dist/chunk-6TZPM43Q.cjs.map +0 -1
- package/dist/chunk-7VLRHXLQ.cjs.map +0 -1
- package/dist/chunk-BZOU7VJQ.cjs.map +0 -1
- package/dist/chunk-IPNJMHJ3.cjs.map +0 -1
- package/dist/chunk-MXAVGOHW.js.map +0 -1
- package/dist/chunk-NUPQJ5SJ.js.map +0 -1
- package/dist/chunk-SF5OAVGD.js.map +0 -1
- package/dist/chunk-UUBHUSUP.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [5.44.0] - 2026-10-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`Input` holds a long paste as a pill**, as `Composer` and `PromptInput` do:
|
|
19
|
+
`<Input name="q" pasteThreshold={500} />`. The pill sits in the field (one
|
|
20
|
+
line, so no line of its own); clicking it puts the full text back. A hidden
|
|
21
|
+
`<input name={name}>` posts the value with every pill expanded, so a plain
|
|
22
|
+
`<form>` submit sends the whole paste. Enter still submits the form.
|
|
23
|
+
**Omit `pasteThreshold` and `pasteImages` and nothing changes**: it is the same
|
|
24
|
+
`<input>` as before. With either set, `ref` points at the hidden input,
|
|
25
|
+
`onValueChange` receives the value with a marker per pill, and `onChange` is
|
|
26
|
+
not called.
|
|
27
|
+
- **Pasted files reach the form.** `pasteImages` on `Input` holds a pasted image
|
|
28
|
+
as a pill AND puts the file in a hidden `<input type="file" multiple>` in the
|
|
29
|
+
same form (`{name}_files[]` by default, or `pasteFilesName`), kept in step with
|
|
30
|
+
the pills: delete one and its file leaves the input. `Composer` and
|
|
31
|
+
`PromptInput` take `pasteFilesName` for the same hidden input.
|
|
32
|
+
- **`PastePills`** (`@particle-academy/react-fancy/paste-pills`): the paste-pill
|
|
33
|
+
core in one place: `usePastePills`, `useInlinePaste`, `InlinePasteEditor`,
|
|
34
|
+
`PastedFilesInput` and the marker helpers. `Composer`, `PromptInput` and
|
|
35
|
+
`Input` all import it, so `npx fancy-cli add` copies it once, as a dependency,
|
|
36
|
+
instead of a copy per component.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- The paste-pill files moved from `components/Composer/` and
|
|
41
|
+
`hooks/use-paste-pills.ts` into `components/PastePills/`. **What to do:**
|
|
42
|
+
nothing. Every public export is unchanged, and `hooks/use-paste-pills` still
|
|
43
|
+
re-exports the hook.
|
|
44
|
+
|
|
45
|
+
## [5.43.0] - 2026-10-17
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **A click in a big `TreeNav` no longer re-renders the whole tree.** Selection,
|
|
50
|
+
expansion and drag state lived in one context that every row read, so one click
|
|
51
|
+
re-rendered every row. Measured in a real browser on Genie 2's file browser
|
|
52
|
+
shape (5,000 nodes, 6 levels, all expanded): **175–267 ms per click before,
|
|
53
|
+
17–20 ms after**, with no change to your code. Rows now subscribe to their own
|
|
54
|
+
slice (am I selected / expanded / the drop target?), handlers are stable, and
|
|
55
|
+
expansion is a Set lookup instead of an array scan per row.
|
|
56
|
+
|
|
57
|
+
`useTreeNav()` is unchanged and still live: a component that reads it gets
|
|
58
|
+
current `selectedId` / `expandedIds` exactly as before.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **`<TreeNav virtualize height={600}>` renders only the rows in view.** The
|
|
63
|
+
visible rows (expanded folders only) are flattened and windowed by
|
|
64
|
+
`VirtualList`; selection, expansion, context menus and drag-and-drop behave as
|
|
65
|
+
in the nested tree. Same tree, all expanded: **33 rows in the DOM instead of
|
|
66
|
+
5,000, and 30 ms to mount instead of 614 ms.** `rowHeight` seeds the row
|
|
67
|
+
estimate (default 26). The nested DOM wrapper
|
|
68
|
+
`data-react-fancy-tree-node-children` does not exist in this mode — rows are
|
|
69
|
+
flat, indented by `paddingLeft`.
|
|
70
|
+
|
|
71
|
+
- **`TreeNav` has tests.** It had none, which is why a refactor this size starts
|
|
72
|
+
with them: selection, toggling, lazy mounting, `useTreeNav`, virtual rows and
|
|
73
|
+
drag-and-drop in both modes.
|
|
74
|
+
|
|
75
|
+
**What you must DO: nothing.** `virtualize` is off by default and the nested tree
|
|
76
|
+
renders the same DOM as before. Collapsed folders were already cheap — their
|
|
77
|
+
children are not mounted until expanded — and still are.
|
|
78
|
+
|
|
14
79
|
## [5.42.1] - 2026-10-17
|
|
15
80
|
|
|
16
81
|
### Fixed
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* A paste held beside a composer rather than inserted into it.
|
|
5
3
|
*
|
|
@@ -137,8 +135,39 @@ interface HeldImage {
|
|
|
137
135
|
file: File;
|
|
138
136
|
label: string;
|
|
139
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* U+FFFC OBJECT REPLACEMENT CHARACTER — Unicode's own "an embedded object sits
|
|
140
|
+
* here". A marker is the id wrapped in a pair of them.
|
|
141
|
+
*
|
|
142
|
+
* Why the message carries a marker at all: with inline placement the message
|
|
143
|
+
* ITSELF is the record of what is attached and where, so there is one answer to
|
|
144
|
+
* "what am I sending?" rather than a text plus a parallel list that can disagree
|
|
145
|
+
* the moment someone backspaces a pill. This is the MOIC estate's design for their
|
|
146
|
+
* own composer, and the reason they withdrew from the beside-the-input layout.
|
|
147
|
+
*
|
|
148
|
+
* Only a marker whose id the composer actually holds is a pill; anything else is
|
|
149
|
+
* left as text, so a stray U+FFFC in pasted content cannot invent an attachment.
|
|
150
|
+
*/
|
|
151
|
+
declare const PILL_MARK = "\uFFFC";
|
|
140
152
|
/** The marker for a held paste or image, for a host or agent writing `value` itself. */
|
|
141
153
|
declare function pasteMarker(id: string): string;
|
|
154
|
+
type ValueSegment = {
|
|
155
|
+
type: "text";
|
|
156
|
+
text: string;
|
|
157
|
+
} | {
|
|
158
|
+
type: "pill";
|
|
159
|
+
id: string;
|
|
160
|
+
};
|
|
161
|
+
/** Split a value into text and pills. Markers for ids not in `known` stay text. */
|
|
162
|
+
declare function segmentValue(value: string, known: ReadonlySet<string>): ValueSegment[];
|
|
163
|
+
/** The id of every marker the value contains, in order, held or not. */
|
|
164
|
+
declare function markedIds(value: string): string[];
|
|
165
|
+
/**
|
|
166
|
+
* What actually gets sent: every text pill replaced by its full text, every image
|
|
167
|
+
* pill by `[Pasted image N]` so the reader of the message can see where the image
|
|
168
|
+
* sat. The images themselves go to `onSubmit`'s second argument.
|
|
169
|
+
*/
|
|
170
|
+
declare function expandMarkers(value: string, pastes: readonly HeldPaste[], images: readonly HeldImage[]): string;
|
|
142
171
|
|
|
143
172
|
/** What a hover preview is being asked to show. */
|
|
144
173
|
type ComposerPastePreview = {
|
|
@@ -166,84 +195,8 @@ interface ComposerPasteClassNames {
|
|
|
166
195
|
/** The editable surface itself. */
|
|
167
196
|
input?: string;
|
|
168
197
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* appear in it. Each one's place in the sent text is marked `[Pasted image N]`,
|
|
173
|
-
* matching its `label`.
|
|
174
|
-
*/
|
|
175
|
-
images: HeldImage[];
|
|
176
|
-
}
|
|
177
|
-
interface ComposerProps {
|
|
178
|
-
value?: string;
|
|
179
|
-
defaultValue?: string;
|
|
180
|
-
onChange?: (value: string) => void;
|
|
181
|
-
/**
|
|
182
|
-
* The second argument only ever carries anything with `pasteImages` on. A
|
|
183
|
-
* one-argument handler stays valid.
|
|
184
|
-
*/
|
|
185
|
-
onSubmit?: (value: string, extras: ComposerSubmitExtras) => void;
|
|
186
|
-
placeholder?: string;
|
|
187
|
-
actions?: ReactNode;
|
|
188
|
-
disabled?: boolean;
|
|
189
|
-
className?: string;
|
|
190
|
-
/**
|
|
191
|
-
* Hold a paste of at least this many characters as a pill, instead of inserting
|
|
192
|
-
* it. Try 500 — about a long paragraph.
|
|
193
|
-
*
|
|
194
|
-
* **Omit it and paste behaves exactly as it always has.** Opt-in on purpose:
|
|
195
|
-
* paste is a fundamental interaction and this library is past 1.0, so a minor
|
|
196
|
-
* release must not quietly change what pasting does for every consumer. There is
|
|
197
|
-
* no default threshold for the same reason.
|
|
198
|
-
*
|
|
199
|
-
* Below the threshold nothing changes at all — no prevented event, no pill, no
|
|
200
|
-
* ceremony for an address or a name.
|
|
201
|
-
*/
|
|
202
|
-
pasteThreshold?: number;
|
|
203
|
-
/**
|
|
204
|
-
* Where a pill goes.
|
|
205
|
-
*
|
|
206
|
-
* - `"above"` (default, and the only behaviour before 5.38.0) — in a row above
|
|
207
|
-
* the input; a send appends held text after what was typed.
|
|
208
|
-
* - `"inline"` — **in the message, where the paste happened.** A text pill gets
|
|
209
|
-
* a line to itself; clicking it puts the full text back exactly there; an
|
|
210
|
-
* image pill sits in the sentence. Backspace deletes a pill like a character.
|
|
211
|
-
*
|
|
212
|
-
* With `"inline"`, `value` carries a marker for each pill (see `pasteMarker`),
|
|
213
|
-
* because the message is then the one record of what is attached and where.
|
|
214
|
-
* `onSubmit` always receives the expanded text, never a marker.
|
|
215
|
-
*/
|
|
216
|
-
pastePlacement?: "above" | "inline";
|
|
217
|
-
/**
|
|
218
|
-
* Hold a pasted IMAGE as a pill (`"inline"` placement only). Off by default —
|
|
219
|
-
* turning it on is a promise to read `onSubmit`'s second argument, because a pill
|
|
220
|
-
* reads as "attached" and an image a host never sends is an attachment the
|
|
221
|
-
* person believes went and did not.
|
|
222
|
-
*
|
|
223
|
-
* A clipboard carrying text as well as an image (copying from a document often
|
|
224
|
-
* includes a picture of the selection) pastes the TEXT: the image there is a
|
|
225
|
-
* rendering, not the thing that was copied.
|
|
226
|
-
*/
|
|
227
|
-
pasteImages?: boolean;
|
|
228
|
-
/** Characters of a held text paste shown at EACH end of its hover preview. Default 20. */
|
|
229
|
-
previewChars?: number;
|
|
230
|
-
/** Replace the hover popover's content. Its box is styled by `classNames.preview`. */
|
|
231
|
-
renderPastePreview?: (preview: ComposerPastePreview) => ReactNode;
|
|
232
|
-
/** Restyle the pills and their popover. */
|
|
233
|
-
classNames?: ComposerPasteClassNames;
|
|
234
|
-
/**
|
|
235
|
-
* The held pastes, if you want to own them.
|
|
236
|
-
*
|
|
237
|
-
* Controllable rather than internal because the component contract forbids
|
|
238
|
-
* internal-only state for anything an agent might need to read or write — an
|
|
239
|
-
* agent driving this surface has to be able to see that a paste is attached, and
|
|
240
|
-
* to attach one itself.
|
|
241
|
-
*/
|
|
242
|
-
heldPastes?: HeldPaste[];
|
|
243
|
-
onHeldPastesChange?: (pastes: HeldPaste[]) => void;
|
|
244
|
-
/** The held images, if you want to own them. Same reasoning as `heldPastes`. */
|
|
245
|
-
heldImages?: HeldImage[];
|
|
246
|
-
onHeldImagesChange?: (images: HeldImage[]) => void;
|
|
247
|
-
}
|
|
198
|
+
/** The same shapes under the names they have outside `Composer`. */
|
|
199
|
+
type PastePreview = ComposerPastePreview;
|
|
200
|
+
type PasteClassNames = ComposerPasteClassNames;
|
|
248
201
|
|
|
249
|
-
export { type
|
|
202
|
+
export { type ComposerPastePreview as C, type HeldImage as H, PILL_MARK as P, type UsePastePillsOptions as U, type ValueSegment as V, type ComposerPasteClassNames as a, type HeldPaste as b, type PasteClassNames as c, type PasteEventLike as d, type PastePreview as e, type UsePastePillsResult as f, expandMarkers as g, markedIds as m, pasteMarker as p, segmentValue as s, usePastePills as u };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* A paste held beside a composer rather than inserted into it.
|
|
5
3
|
*
|
|
@@ -137,8 +135,39 @@ interface HeldImage {
|
|
|
137
135
|
file: File;
|
|
138
136
|
label: string;
|
|
139
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* U+FFFC OBJECT REPLACEMENT CHARACTER — Unicode's own "an embedded object sits
|
|
140
|
+
* here". A marker is the id wrapped in a pair of them.
|
|
141
|
+
*
|
|
142
|
+
* Why the message carries a marker at all: with inline placement the message
|
|
143
|
+
* ITSELF is the record of what is attached and where, so there is one answer to
|
|
144
|
+
* "what am I sending?" rather than a text plus a parallel list that can disagree
|
|
145
|
+
* the moment someone backspaces a pill. This is the MOIC estate's design for their
|
|
146
|
+
* own composer, and the reason they withdrew from the beside-the-input layout.
|
|
147
|
+
*
|
|
148
|
+
* Only a marker whose id the composer actually holds is a pill; anything else is
|
|
149
|
+
* left as text, so a stray U+FFFC in pasted content cannot invent an attachment.
|
|
150
|
+
*/
|
|
151
|
+
declare const PILL_MARK = "\uFFFC";
|
|
140
152
|
/** The marker for a held paste or image, for a host or agent writing `value` itself. */
|
|
141
153
|
declare function pasteMarker(id: string): string;
|
|
154
|
+
type ValueSegment = {
|
|
155
|
+
type: "text";
|
|
156
|
+
text: string;
|
|
157
|
+
} | {
|
|
158
|
+
type: "pill";
|
|
159
|
+
id: string;
|
|
160
|
+
};
|
|
161
|
+
/** Split a value into text and pills. Markers for ids not in `known` stay text. */
|
|
162
|
+
declare function segmentValue(value: string, known: ReadonlySet<string>): ValueSegment[];
|
|
163
|
+
/** The id of every marker the value contains, in order, held or not. */
|
|
164
|
+
declare function markedIds(value: string): string[];
|
|
165
|
+
/**
|
|
166
|
+
* What actually gets sent: every text pill replaced by its full text, every image
|
|
167
|
+
* pill by `[Pasted image N]` so the reader of the message can see where the image
|
|
168
|
+
* sat. The images themselves go to `onSubmit`'s second argument.
|
|
169
|
+
*/
|
|
170
|
+
declare function expandMarkers(value: string, pastes: readonly HeldPaste[], images: readonly HeldImage[]): string;
|
|
142
171
|
|
|
143
172
|
/** What a hover preview is being asked to show. */
|
|
144
173
|
type ComposerPastePreview = {
|
|
@@ -166,84 +195,8 @@ interface ComposerPasteClassNames {
|
|
|
166
195
|
/** The editable surface itself. */
|
|
167
196
|
input?: string;
|
|
168
197
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* appear in it. Each one's place in the sent text is marked `[Pasted image N]`,
|
|
173
|
-
* matching its `label`.
|
|
174
|
-
*/
|
|
175
|
-
images: HeldImage[];
|
|
176
|
-
}
|
|
177
|
-
interface ComposerProps {
|
|
178
|
-
value?: string;
|
|
179
|
-
defaultValue?: string;
|
|
180
|
-
onChange?: (value: string) => void;
|
|
181
|
-
/**
|
|
182
|
-
* The second argument only ever carries anything with `pasteImages` on. A
|
|
183
|
-
* one-argument handler stays valid.
|
|
184
|
-
*/
|
|
185
|
-
onSubmit?: (value: string, extras: ComposerSubmitExtras) => void;
|
|
186
|
-
placeholder?: string;
|
|
187
|
-
actions?: ReactNode;
|
|
188
|
-
disabled?: boolean;
|
|
189
|
-
className?: string;
|
|
190
|
-
/**
|
|
191
|
-
* Hold a paste of at least this many characters as a pill, instead of inserting
|
|
192
|
-
* it. Try 500 — about a long paragraph.
|
|
193
|
-
*
|
|
194
|
-
* **Omit it and paste behaves exactly as it always has.** Opt-in on purpose:
|
|
195
|
-
* paste is a fundamental interaction and this library is past 1.0, so a minor
|
|
196
|
-
* release must not quietly change what pasting does for every consumer. There is
|
|
197
|
-
* no default threshold for the same reason.
|
|
198
|
-
*
|
|
199
|
-
* Below the threshold nothing changes at all — no prevented event, no pill, no
|
|
200
|
-
* ceremony for an address or a name.
|
|
201
|
-
*/
|
|
202
|
-
pasteThreshold?: number;
|
|
203
|
-
/**
|
|
204
|
-
* Where a pill goes.
|
|
205
|
-
*
|
|
206
|
-
* - `"above"` (default, and the only behaviour before 5.38.0) — in a row above
|
|
207
|
-
* the input; a send appends held text after what was typed.
|
|
208
|
-
* - `"inline"` — **in the message, where the paste happened.** A text pill gets
|
|
209
|
-
* a line to itself; clicking it puts the full text back exactly there; an
|
|
210
|
-
* image pill sits in the sentence. Backspace deletes a pill like a character.
|
|
211
|
-
*
|
|
212
|
-
* With `"inline"`, `value` carries a marker for each pill (see `pasteMarker`),
|
|
213
|
-
* because the message is then the one record of what is attached and where.
|
|
214
|
-
* `onSubmit` always receives the expanded text, never a marker.
|
|
215
|
-
*/
|
|
216
|
-
pastePlacement?: "above" | "inline";
|
|
217
|
-
/**
|
|
218
|
-
* Hold a pasted IMAGE as a pill (`"inline"` placement only). Off by default —
|
|
219
|
-
* turning it on is a promise to read `onSubmit`'s second argument, because a pill
|
|
220
|
-
* reads as "attached" and an image a host never sends is an attachment the
|
|
221
|
-
* person believes went and did not.
|
|
222
|
-
*
|
|
223
|
-
* A clipboard carrying text as well as an image (copying from a document often
|
|
224
|
-
* includes a picture of the selection) pastes the TEXT: the image there is a
|
|
225
|
-
* rendering, not the thing that was copied.
|
|
226
|
-
*/
|
|
227
|
-
pasteImages?: boolean;
|
|
228
|
-
/** Characters of a held text paste shown at EACH end of its hover preview. Default 20. */
|
|
229
|
-
previewChars?: number;
|
|
230
|
-
/** Replace the hover popover's content. Its box is styled by `classNames.preview`. */
|
|
231
|
-
renderPastePreview?: (preview: ComposerPastePreview) => ReactNode;
|
|
232
|
-
/** Restyle the pills and their popover. */
|
|
233
|
-
classNames?: ComposerPasteClassNames;
|
|
234
|
-
/**
|
|
235
|
-
* The held pastes, if you want to own them.
|
|
236
|
-
*
|
|
237
|
-
* Controllable rather than internal because the component contract forbids
|
|
238
|
-
* internal-only state for anything an agent might need to read or write — an
|
|
239
|
-
* agent driving this surface has to be able to see that a paste is attached, and
|
|
240
|
-
* to attach one itself.
|
|
241
|
-
*/
|
|
242
|
-
heldPastes?: HeldPaste[];
|
|
243
|
-
onHeldPastesChange?: (pastes: HeldPaste[]) => void;
|
|
244
|
-
/** The held images, if you want to own them. Same reasoning as `heldPastes`. */
|
|
245
|
-
heldImages?: HeldImage[];
|
|
246
|
-
onHeldImagesChange?: (images: HeldImage[]) => void;
|
|
247
|
-
}
|
|
198
|
+
/** The same shapes under the names they have outside `Composer`. */
|
|
199
|
+
type PastePreview = ComposerPastePreview;
|
|
200
|
+
type PasteClassNames = ComposerPasteClassNames;
|
|
248
201
|
|
|
249
|
-
export { type
|
|
202
|
+
export { type ComposerPastePreview as C, type HeldImage as H, PILL_MARK as P, type UsePastePillsOptions as U, type ValueSegment as V, type ComposerPasteClassNames as a, type HeldPaste as b, type PasteClassNames as c, type PasteEventLike as d, type PastePreview as e, type UsePastePillsResult as f, expandMarkers as g, markedIds as m, pasteMarker as p, segmentValue as s, usePastePills as u };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
|
+
import { C as ComposerPastePreview, a as ComposerPasteClassNames, b as HeldPaste, H as HeldImage, f as UsePastePillsResult } from './PastePills.types-CDyotkUb.js';
|
|
4
|
+
|
|
5
|
+
/** The attribute on the placeholder span a pill is portalled into. */
|
|
6
|
+
declare const HOST_ATTR = "data-react-fancy-composer-paste-host";
|
|
7
|
+
/** What a paste becomes, as decided by `Composer`. `block` pills get their own line. */
|
|
8
|
+
interface PillToInsert {
|
|
9
|
+
id: string;
|
|
10
|
+
block: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface InlinePasteEditorHandle {
|
|
13
|
+
focus(): void;
|
|
14
|
+
/** Swap a pill for plain text, where it sits. */
|
|
15
|
+
replacePill(id: string, text: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Replace the whole value and put the caret at `caret` (an offset into the
|
|
18
|
+
* value). For a host that edits text itself — `PromptInput`'s `/` and `@`
|
|
19
|
+
* pickers — where rebuilding from `value` alone would lose the caret.
|
|
20
|
+
*/
|
|
21
|
+
setValue(value: string, caret: number): void;
|
|
22
|
+
}
|
|
23
|
+
interface InlinePasteEditorProps {
|
|
24
|
+
value: string;
|
|
25
|
+
/** Ids the composer holds. A marker for anything else stays text. */
|
|
26
|
+
known: ReadonlySet<string>;
|
|
27
|
+
/** `caret` is the caret's offset into `value`, or `value.length` if it is elsewhere. */
|
|
28
|
+
onChange(value: string, caret: number): void;
|
|
29
|
+
onSubmit(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Which key sends. `"enter"` (default): Enter sends, Shift+Enter is a newline.
|
|
32
|
+
* `"mod-enter"`: Ctrl/⌘+Enter sends and a plain Enter is a newline.
|
|
33
|
+
*/
|
|
34
|
+
submitKey?: "enter" | "mod-enter";
|
|
35
|
+
/** Runs BEFORE the editor's own key handling; `preventDefault()` to claim the key. */
|
|
36
|
+
onKeyDown?(e: React.KeyboardEvent<HTMLDivElement>): void;
|
|
37
|
+
placeholderClassName?: string;
|
|
38
|
+
/** Height in px past which the input scrolls. Defaults to the class's max height. */
|
|
39
|
+
maxHeight?: number;
|
|
40
|
+
/** Return the pills a paste becomes, or null to insert its text as typed. */
|
|
41
|
+
onPaste(data: DataTransfer | null): PillToInsert[] | null;
|
|
42
|
+
/** Every pill placeholder currently in the editor, for the composer to portal into. */
|
|
43
|
+
onHostsChange(hosts: ReadonlyMap<string, HTMLElement>): void;
|
|
44
|
+
placeholder: string;
|
|
45
|
+
disabled: boolean;
|
|
46
|
+
className?: string;
|
|
47
|
+
/**
|
|
48
|
+
* `false` makes it a single-line field, as `Input` uses it: Enter always
|
|
49
|
+
* submits, and a newline in pasted text becomes a space, as a browser does for
|
|
50
|
+
* an `<input>`. A held paste keeps its newlines; it is only the field that has
|
|
51
|
+
* one line. Default `true`.
|
|
52
|
+
*/
|
|
53
|
+
multiline?: boolean;
|
|
54
|
+
/** The editable surface's id, so a host can find it. */
|
|
55
|
+
id?: string;
|
|
56
|
+
/** Names the textbox for assistive tech; a `<div>` cannot be the target of a `<label>`. */
|
|
57
|
+
ariaLabelledBy?: string;
|
|
58
|
+
ariaInvalid?: boolean;
|
|
59
|
+
style?: React.CSSProperties;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The plain-text-plus-pills surface behind `Composer`'s inline placement.
|
|
63
|
+
*
|
|
64
|
+
* The DOM is the source of truth WHILE TYPING and `value` is its serialisation;
|
|
65
|
+
* the DOM is rebuilt from `value` only when `value` changes from outside (a send,
|
|
66
|
+
* a host, an agent). Rebuilding on every keystroke would throw the caret away.
|
|
67
|
+
*
|
|
68
|
+
* Every insertion is done here by hand, never by the browser: a paste is always
|
|
69
|
+
* intercepted, so no foreign HTML ever lands in the editor, and Enter is handled
|
|
70
|
+
* so a browser never invents a `<div>` per line.
|
|
71
|
+
*/
|
|
72
|
+
declare const InlinePasteEditor: react.ForwardRefExoticComponent<InlinePasteEditorProps & react.RefAttributes<InlinePasteEditorHandle>>;
|
|
73
|
+
|
|
74
|
+
interface UseInlinePasteOptions {
|
|
75
|
+
pasteThreshold?: number;
|
|
76
|
+
pasteImages?: boolean;
|
|
77
|
+
previewChars: number;
|
|
78
|
+
renderPastePreview?: (preview: ComposerPastePreview) => ReactNode;
|
|
79
|
+
classNames?: ComposerPasteClassNames;
|
|
80
|
+
disabled: boolean;
|
|
81
|
+
heldPastes?: HeldPaste[];
|
|
82
|
+
onHeldPastesChange?: (pastes: HeldPaste[]) => void;
|
|
83
|
+
heldImages?: HeldImage[];
|
|
84
|
+
onHeldImagesChange?: (images: HeldImage[]) => void;
|
|
85
|
+
editorRef: RefObject<InlinePasteEditorHandle | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Give a text pill a line to itself. Default `true`; a single-line field
|
|
88
|
+
* (`Input`) has no lines to give, so its pills sit in the text.
|
|
89
|
+
*/
|
|
90
|
+
blockText?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Everything inline paste pills need besides the editor itself: the held pastes
|
|
94
|
+
* and images, deciding what a paste becomes, dropping a pill's hold when it is
|
|
95
|
+
* deleted from the message, rendering the pills into the editor, and expanding
|
|
96
|
+
* them on send.
|
|
97
|
+
*
|
|
98
|
+
* Shared by `Composer`, `PromptInput` and `Input` so they cannot disagree about
|
|
99
|
+
* what a pill means. The rule that must not drift is the one `expand` and `imagesIn`
|
|
100
|
+
* carry: a send with pills still closed carries everything they hold.
|
|
101
|
+
*/
|
|
102
|
+
declare function useInlinePaste({ pasteThreshold, pasteImages, previewChars, renderPastePreview, classNames, disabled, heldPastes: controlledHeldPastes, onHeldPastesChange, heldImages: controlledHeldImages, onHeldImagesChange, editorRef, blockText, }: UseInlinePasteOptions): {
|
|
103
|
+
pills: UsePastePillsResult;
|
|
104
|
+
heldPastes: HeldPaste[];
|
|
105
|
+
heldImages: HeldImage[];
|
|
106
|
+
known: Set<string>;
|
|
107
|
+
decidePaste: (data: DataTransfer | null) => PillToInsert[] | null;
|
|
108
|
+
prune: (next: string) => void;
|
|
109
|
+
onHostsChange: (next: ReadonlyMap<string, HTMLElement>) => void;
|
|
110
|
+
expand: (value: string) => string;
|
|
111
|
+
imagesIn: (value: string) => HeldImage[];
|
|
112
|
+
clear: () => void;
|
|
113
|
+
portals: (react.ReactPortal | null)[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface PastedFilesInputProps {
|
|
117
|
+
/** The form field name the files are submitted under. */
|
|
118
|
+
name: string;
|
|
119
|
+
/** The files to submit, in the order the message holds them. */
|
|
120
|
+
files: readonly File[];
|
|
121
|
+
/** Associate with a `<form id>` elsewhere in the document, as `<input form>` does. */
|
|
122
|
+
form?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A hidden `<input type="file">` carrying the files pasted into a field, so a
|
|
126
|
+
* plain `<form>` submits them with everything else.
|
|
127
|
+
*
|
|
128
|
+
* A pasted file is not in any form field: the clipboard hands it to the paste
|
|
129
|
+
* handler and nowhere else. Held as a pill, it is attached as far as the person can
|
|
130
|
+
* see, and a native submit would then post the text without it. A file input is
|
|
131
|
+
* the one element a form posts files from, and its `files` can be set from a
|
|
132
|
+
* `DataTransfer`, so the pills keep it in step: delete a pill and its file leaves
|
|
133
|
+
* the input too.
|
|
134
|
+
*
|
|
135
|
+
* Hidden rather than visually hidden: `hidden` keeps it out of the tab order and
|
|
136
|
+
* the accessibility tree, where the pills already represent it, and does not stop
|
|
137
|
+
* a form submitting it.
|
|
138
|
+
*/
|
|
139
|
+
declare function PastedFilesInput({ name, files, form }: PastedFilesInputProps): react.JSX.Element;
|
|
140
|
+
|
|
141
|
+
export { HOST_ATTR as H, InlinePasteEditor as I, PastedFilesInput as P, type UseInlinePasteOptions as U, type InlinePasteEditorHandle as a, type PastedFilesInputProps as b, type PillToInsert as c, useInlinePaste as u };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
|
+
import { C as ComposerPastePreview, a as ComposerPasteClassNames, b as HeldPaste, H as HeldImage, f as UsePastePillsResult } from './PastePills.types-CDyotkUb.cjs';
|
|
4
|
+
|
|
5
|
+
/** The attribute on the placeholder span a pill is portalled into. */
|
|
6
|
+
declare const HOST_ATTR = "data-react-fancy-composer-paste-host";
|
|
7
|
+
/** What a paste becomes, as decided by `Composer`. `block` pills get their own line. */
|
|
8
|
+
interface PillToInsert {
|
|
9
|
+
id: string;
|
|
10
|
+
block: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface InlinePasteEditorHandle {
|
|
13
|
+
focus(): void;
|
|
14
|
+
/** Swap a pill for plain text, where it sits. */
|
|
15
|
+
replacePill(id: string, text: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Replace the whole value and put the caret at `caret` (an offset into the
|
|
18
|
+
* value). For a host that edits text itself — `PromptInput`'s `/` and `@`
|
|
19
|
+
* pickers — where rebuilding from `value` alone would lose the caret.
|
|
20
|
+
*/
|
|
21
|
+
setValue(value: string, caret: number): void;
|
|
22
|
+
}
|
|
23
|
+
interface InlinePasteEditorProps {
|
|
24
|
+
value: string;
|
|
25
|
+
/** Ids the composer holds. A marker for anything else stays text. */
|
|
26
|
+
known: ReadonlySet<string>;
|
|
27
|
+
/** `caret` is the caret's offset into `value`, or `value.length` if it is elsewhere. */
|
|
28
|
+
onChange(value: string, caret: number): void;
|
|
29
|
+
onSubmit(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Which key sends. `"enter"` (default): Enter sends, Shift+Enter is a newline.
|
|
32
|
+
* `"mod-enter"`: Ctrl/⌘+Enter sends and a plain Enter is a newline.
|
|
33
|
+
*/
|
|
34
|
+
submitKey?: "enter" | "mod-enter";
|
|
35
|
+
/** Runs BEFORE the editor's own key handling; `preventDefault()` to claim the key. */
|
|
36
|
+
onKeyDown?(e: React.KeyboardEvent<HTMLDivElement>): void;
|
|
37
|
+
placeholderClassName?: string;
|
|
38
|
+
/** Height in px past which the input scrolls. Defaults to the class's max height. */
|
|
39
|
+
maxHeight?: number;
|
|
40
|
+
/** Return the pills a paste becomes, or null to insert its text as typed. */
|
|
41
|
+
onPaste(data: DataTransfer | null): PillToInsert[] | null;
|
|
42
|
+
/** Every pill placeholder currently in the editor, for the composer to portal into. */
|
|
43
|
+
onHostsChange(hosts: ReadonlyMap<string, HTMLElement>): void;
|
|
44
|
+
placeholder: string;
|
|
45
|
+
disabled: boolean;
|
|
46
|
+
className?: string;
|
|
47
|
+
/**
|
|
48
|
+
* `false` makes it a single-line field, as `Input` uses it: Enter always
|
|
49
|
+
* submits, and a newline in pasted text becomes a space, as a browser does for
|
|
50
|
+
* an `<input>`. A held paste keeps its newlines; it is only the field that has
|
|
51
|
+
* one line. Default `true`.
|
|
52
|
+
*/
|
|
53
|
+
multiline?: boolean;
|
|
54
|
+
/** The editable surface's id, so a host can find it. */
|
|
55
|
+
id?: string;
|
|
56
|
+
/** Names the textbox for assistive tech; a `<div>` cannot be the target of a `<label>`. */
|
|
57
|
+
ariaLabelledBy?: string;
|
|
58
|
+
ariaInvalid?: boolean;
|
|
59
|
+
style?: React.CSSProperties;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The plain-text-plus-pills surface behind `Composer`'s inline placement.
|
|
63
|
+
*
|
|
64
|
+
* The DOM is the source of truth WHILE TYPING and `value` is its serialisation;
|
|
65
|
+
* the DOM is rebuilt from `value` only when `value` changes from outside (a send,
|
|
66
|
+
* a host, an agent). Rebuilding on every keystroke would throw the caret away.
|
|
67
|
+
*
|
|
68
|
+
* Every insertion is done here by hand, never by the browser: a paste is always
|
|
69
|
+
* intercepted, so no foreign HTML ever lands in the editor, and Enter is handled
|
|
70
|
+
* so a browser never invents a `<div>` per line.
|
|
71
|
+
*/
|
|
72
|
+
declare const InlinePasteEditor: react.ForwardRefExoticComponent<InlinePasteEditorProps & react.RefAttributes<InlinePasteEditorHandle>>;
|
|
73
|
+
|
|
74
|
+
interface UseInlinePasteOptions {
|
|
75
|
+
pasteThreshold?: number;
|
|
76
|
+
pasteImages?: boolean;
|
|
77
|
+
previewChars: number;
|
|
78
|
+
renderPastePreview?: (preview: ComposerPastePreview) => ReactNode;
|
|
79
|
+
classNames?: ComposerPasteClassNames;
|
|
80
|
+
disabled: boolean;
|
|
81
|
+
heldPastes?: HeldPaste[];
|
|
82
|
+
onHeldPastesChange?: (pastes: HeldPaste[]) => void;
|
|
83
|
+
heldImages?: HeldImage[];
|
|
84
|
+
onHeldImagesChange?: (images: HeldImage[]) => void;
|
|
85
|
+
editorRef: RefObject<InlinePasteEditorHandle | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Give a text pill a line to itself. Default `true`; a single-line field
|
|
88
|
+
* (`Input`) has no lines to give, so its pills sit in the text.
|
|
89
|
+
*/
|
|
90
|
+
blockText?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Everything inline paste pills need besides the editor itself: the held pastes
|
|
94
|
+
* and images, deciding what a paste becomes, dropping a pill's hold when it is
|
|
95
|
+
* deleted from the message, rendering the pills into the editor, and expanding
|
|
96
|
+
* them on send.
|
|
97
|
+
*
|
|
98
|
+
* Shared by `Composer`, `PromptInput` and `Input` so they cannot disagree about
|
|
99
|
+
* what a pill means. The rule that must not drift is the one `expand` and `imagesIn`
|
|
100
|
+
* carry: a send with pills still closed carries everything they hold.
|
|
101
|
+
*/
|
|
102
|
+
declare function useInlinePaste({ pasteThreshold, pasteImages, previewChars, renderPastePreview, classNames, disabled, heldPastes: controlledHeldPastes, onHeldPastesChange, heldImages: controlledHeldImages, onHeldImagesChange, editorRef, blockText, }: UseInlinePasteOptions): {
|
|
103
|
+
pills: UsePastePillsResult;
|
|
104
|
+
heldPastes: HeldPaste[];
|
|
105
|
+
heldImages: HeldImage[];
|
|
106
|
+
known: Set<string>;
|
|
107
|
+
decidePaste: (data: DataTransfer | null) => PillToInsert[] | null;
|
|
108
|
+
prune: (next: string) => void;
|
|
109
|
+
onHostsChange: (next: ReadonlyMap<string, HTMLElement>) => void;
|
|
110
|
+
expand: (value: string) => string;
|
|
111
|
+
imagesIn: (value: string) => HeldImage[];
|
|
112
|
+
clear: () => void;
|
|
113
|
+
portals: (react.ReactPortal | null)[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface PastedFilesInputProps {
|
|
117
|
+
/** The form field name the files are submitted under. */
|
|
118
|
+
name: string;
|
|
119
|
+
/** The files to submit, in the order the message holds them. */
|
|
120
|
+
files: readonly File[];
|
|
121
|
+
/** Associate with a `<form id>` elsewhere in the document, as `<input form>` does. */
|
|
122
|
+
form?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A hidden `<input type="file">` carrying the files pasted into a field, so a
|
|
126
|
+
* plain `<form>` submits them with everything else.
|
|
127
|
+
*
|
|
128
|
+
* A pasted file is not in any form field: the clipboard hands it to the paste
|
|
129
|
+
* handler and nowhere else. Held as a pill, it is attached as far as the person can
|
|
130
|
+
* see, and a native submit would then post the text without it. A file input is
|
|
131
|
+
* the one element a form posts files from, and its `files` can be set from a
|
|
132
|
+
* `DataTransfer`, so the pills keep it in step: delete a pill and its file leaves
|
|
133
|
+
* the input too.
|
|
134
|
+
*
|
|
135
|
+
* Hidden rather than visually hidden: `hidden` keeps it out of the tab order and
|
|
136
|
+
* the accessibility tree, where the pills already represent it, and does not stop
|
|
137
|
+
* a form submitting it.
|
|
138
|
+
*/
|
|
139
|
+
declare function PastedFilesInput({ name, files, form }: PastedFilesInputProps): react.JSX.Element;
|
|
140
|
+
|
|
141
|
+
export { HOST_ATTR as H, InlinePasteEditor as I, PastedFilesInput as P, type UseInlinePasteOptions as U, type InlinePasteEditorHandle as a, type PastedFilesInputProps as b, type PillToInsert as c, useInlinePaste as u };
|