@particle-academy/react-fancy 5.37.0 → 5.38.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 +76 -0
- package/dist/autocomplete.cjs +3 -3
- package/dist/autocomplete.js +2 -2
- package/dist/{chunk-FXP4JLGU.cjs → chunk-AR3JOPGT.cjs} +3 -3
- package/dist/{chunk-FXP4JLGU.cjs.map → chunk-AR3JOPGT.cjs.map} +1 -1
- package/dist/{chunk-35EN5AYE.js → chunk-CF5KRDN4.js} +6 -3
- package/dist/chunk-CF5KRDN4.js.map +1 -0
- package/dist/{chunk-6ASZUE4T.js → chunk-FQLL2JI7.js} +3 -3
- package/dist/{chunk-6ASZUE4T.js.map → chunk-FQLL2JI7.js.map} +1 -1
- package/dist/{chunk-ZH3SPBVB.js → chunk-IECPLOSZ.js} +3 -3
- package/dist/{chunk-ZH3SPBVB.js.map → chunk-IECPLOSZ.js.map} +1 -1
- package/dist/{chunk-E2SZBIAV.cjs → chunk-IWQ3ZFLZ.cjs} +5 -5
- package/dist/{chunk-E2SZBIAV.cjs.map → chunk-IWQ3ZFLZ.cjs.map} +1 -1
- package/dist/{chunk-QBNR5IVM.cjs → chunk-MFBUK4RV.cjs} +3 -3
- package/dist/{chunk-QBNR5IVM.cjs.map → chunk-MFBUK4RV.cjs.map} +1 -1
- package/dist/{chunk-DNV7MUEE.cjs → chunk-QLL7MWAU.cjs} +4 -4
- package/dist/{chunk-DNV7MUEE.cjs.map → chunk-QLL7MWAU.cjs.map} +1 -1
- package/dist/chunk-RYKPYILS.cjs +699 -0
- package/dist/chunk-RYKPYILS.cjs.map +1 -0
- package/dist/{chunk-O3WEV562.js → chunk-WUV3XDJH.js} +3 -3
- package/dist/{chunk-O3WEV562.js.map → chunk-WUV3XDJH.js.map} +1 -1
- package/dist/{chunk-ISPCXIJZ.cjs → chunk-X7H75WVP.cjs} +6 -3
- package/dist/chunk-X7H75WVP.cjs.map +1 -0
- package/dist/chunk-Z4GO6N6N.js +695 -0
- package/dist/chunk-Z4GO6N6N.js.map +1 -0
- package/dist/{chunk-O2ZHORSC.js → chunk-Z6RVBZQ4.js} +3 -3
- package/dist/{chunk-O2ZHORSC.js.map → chunk-Z6RVBZQ4.js.map} +1 -1
- package/dist/composer.cjs +9 -2
- package/dist/composer.d.cts +1 -1
- package/dist/composer.d.ts +1 -1
- package/dist/composer.js +4 -1
- package/dist/editor.cjs +1 -1
- package/dist/editor.js +1 -1
- package/dist/{index-C8PKWA_5.d.cts → index-CazpqSsk.d.cts} +103 -4
- package/dist/{index-C8PKWA_5.d.ts → index-CazpqSsk.d.ts} +103 -4
- package/dist/index.cjs +39 -35
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/inputs.cjs +3 -3
- package/dist/inputs.js +2 -2
- package/dist/json-editor.cjs +14 -14
- package/dist/json-editor.js +3 -3
- package/dist/prompt-input.cjs +3 -3
- package/dist/prompt-input.js +2 -2
- package/dist/select.cjs +3 -3
- package/dist/select.js +2 -2
- package/dist/tooltip.cjs +2 -2
- package/dist/tooltip.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-23FTDEGR.cjs +0 -218
- package/dist/chunk-23FTDEGR.cjs.map +0 -1
- package/dist/chunk-2XAVAPDA.js +0 -215
- package/dist/chunk-2XAVAPDA.js.map +0 -1
- package/dist/chunk-35EN5AYE.js.map +0 -1
- package/dist/chunk-ISPCXIJZ.cjs.map +0 -1
package/dist/chunk-23FTDEGR.cjs
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkSNHU2AI4_cjs = require('./chunk-SNHU2AI4.cjs');
|
|
4
|
-
var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
|
|
5
|
-
var react = require('react');
|
|
6
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
-
|
|
8
|
-
function usePastePills({
|
|
9
|
-
threshold,
|
|
10
|
-
heldPastes: controlledHeldPastes,
|
|
11
|
-
onHeldPastesChange
|
|
12
|
-
} = {}) {
|
|
13
|
-
const [heldPastes, setHeldPastes] = chunkSNHU2AI4_cjs.useControllableState(
|
|
14
|
-
controlledHeldPastes,
|
|
15
|
-
[],
|
|
16
|
-
onHeldPastesChange
|
|
17
|
-
);
|
|
18
|
-
const nextId = react.useRef(0);
|
|
19
|
-
const handlePaste = react.useCallback(
|
|
20
|
-
(event) => {
|
|
21
|
-
if (!threshold || threshold <= 0) return false;
|
|
22
|
-
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
23
|
-
if (text.length < threshold) return false;
|
|
24
|
-
event.preventDefault();
|
|
25
|
-
nextId.current += 1;
|
|
26
|
-
const id = `paste-${nextId.current}`;
|
|
27
|
-
setHeldPastes((current) => [...current, { id, text }]);
|
|
28
|
-
return true;
|
|
29
|
-
},
|
|
30
|
-
[threshold, setHeldPastes]
|
|
31
|
-
);
|
|
32
|
-
const expand = react.useCallback(
|
|
33
|
-
(id) => {
|
|
34
|
-
const found = heldPastes.find((p) => p.id === id);
|
|
35
|
-
if (found === void 0) return void 0;
|
|
36
|
-
setHeldPastes((current) => current.filter((p) => p.id !== id));
|
|
37
|
-
return found.text;
|
|
38
|
-
},
|
|
39
|
-
[heldPastes, setHeldPastes]
|
|
40
|
-
);
|
|
41
|
-
const remove = react.useCallback(
|
|
42
|
-
(id) => setHeldPastes((current) => current.filter((p) => p.id !== id)),
|
|
43
|
-
[setHeldPastes]
|
|
44
|
-
);
|
|
45
|
-
const clear = react.useCallback(() => setHeldPastes([]), [setHeldPastes]);
|
|
46
|
-
return { heldPastes, handlePaste, expand, remove, clear };
|
|
47
|
-
}
|
|
48
|
-
var PREVIEW_CHARS = 20;
|
|
49
|
-
function withSeparators(n) {
|
|
50
|
-
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
51
|
-
}
|
|
52
|
-
function endsOf(text) {
|
|
53
|
-
if (text.length <= PREVIEW_CHARS * 2) return text;
|
|
54
|
-
return `${text.slice(0, PREVIEW_CHARS)} \u2026 ${text.slice(-PREVIEW_CHARS)}`;
|
|
55
|
-
}
|
|
56
|
-
var Composer = react.forwardRef(
|
|
57
|
-
function Composer2({
|
|
58
|
-
value: controlledValue,
|
|
59
|
-
defaultValue = "",
|
|
60
|
-
onChange,
|
|
61
|
-
onSubmit,
|
|
62
|
-
placeholder = "Type a message...",
|
|
63
|
-
actions,
|
|
64
|
-
disabled = false,
|
|
65
|
-
className,
|
|
66
|
-
pasteThreshold,
|
|
67
|
-
heldPastes: controlledHeldPastes,
|
|
68
|
-
onHeldPastesChange
|
|
69
|
-
}, ref) {
|
|
70
|
-
const [value, setValue] = chunkSNHU2AI4_cjs.useControllableState(
|
|
71
|
-
controlledValue,
|
|
72
|
-
defaultValue,
|
|
73
|
-
onChange
|
|
74
|
-
);
|
|
75
|
-
const pills = usePastePills({
|
|
76
|
-
threshold: pasteThreshold,
|
|
77
|
-
heldPastes: controlledHeldPastes,
|
|
78
|
-
onHeldPastesChange
|
|
79
|
-
});
|
|
80
|
-
const heldPastes = pills.heldPastes;
|
|
81
|
-
const textareaRef = react.useRef(null);
|
|
82
|
-
const handlePaste = react.useCallback(
|
|
83
|
-
(e) => {
|
|
84
|
-
pills.handlePaste(e);
|
|
85
|
-
},
|
|
86
|
-
[pills]
|
|
87
|
-
);
|
|
88
|
-
const composeOutgoing = react.useCallback(
|
|
89
|
-
() => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join("\n\n"),
|
|
90
|
-
[value, heldPastes]
|
|
91
|
-
);
|
|
92
|
-
const hasContent = value.trim().length > 0 || heldPastes.length > 0;
|
|
93
|
-
const handleSubmit = react.useCallback(() => {
|
|
94
|
-
if (!hasContent || disabled) return;
|
|
95
|
-
onSubmit?.(composeOutgoing());
|
|
96
|
-
setValue("");
|
|
97
|
-
pills.clear();
|
|
98
|
-
textareaRef.current?.focus();
|
|
99
|
-
}, [hasContent, disabled, onSubmit, composeOutgoing, setValue, pills]);
|
|
100
|
-
const handleKeyDown = (e) => {
|
|
101
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
102
|
-
e.preventDefault();
|
|
103
|
-
handleSubmit();
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
const insertPaste = react.useCallback(
|
|
107
|
-
(paste) => {
|
|
108
|
-
const text = pills.expand(paste.id);
|
|
109
|
-
if (text === void 0) return;
|
|
110
|
-
setValue((current) => current ? `${current}
|
|
111
|
-
|
|
112
|
-
${text}` : text);
|
|
113
|
-
textareaRef.current?.focus();
|
|
114
|
-
},
|
|
115
|
-
[pills, setValue]
|
|
116
|
-
);
|
|
117
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
118
|
-
"div",
|
|
119
|
-
{
|
|
120
|
-
"data-react-fancy-composer": "",
|
|
121
|
-
ref,
|
|
122
|
-
className: chunkZU5Q77OT_cjs.cn(
|
|
123
|
-
"rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
|
|
124
|
-
disabled && "opacity-50",
|
|
125
|
-
className
|
|
126
|
-
),
|
|
127
|
-
children: [
|
|
128
|
-
heldPastes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
129
|
-
"div",
|
|
130
|
-
{
|
|
131
|
-
"data-react-fancy-composer-paste-pills": "",
|
|
132
|
-
className: "flex flex-wrap gap-1.5 px-3 pt-3",
|
|
133
|
-
children: heldPastes.map((paste) => (
|
|
134
|
-
// `group` + `group-hover` rather than hover state: the preview is
|
|
135
|
-
// presentation, and a useState for it would be one more thing an
|
|
136
|
-
// agent could desynchronise.
|
|
137
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "group relative inline-flex", children: [
|
|
138
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
139
|
-
"button",
|
|
140
|
-
{
|
|
141
|
-
type: "button",
|
|
142
|
-
"data-react-fancy-composer-paste-pill": paste.id,
|
|
143
|
-
onClick: () => insertPaste(paste),
|
|
144
|
-
disabled,
|
|
145
|
-
"aria-label": `Insert this pasted text, ${withSeparators(paste.text.length)} characters`,
|
|
146
|
-
"aria-describedby": `${paste.id}-preview`,
|
|
147
|
-
className: "inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700",
|
|
148
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
149
|
-
"Pasted text \xB7 ",
|
|
150
|
-
withSeparators(paste.text.length),
|
|
151
|
-
" characters"
|
|
152
|
-
] })
|
|
153
|
-
}
|
|
154
|
-
),
|
|
155
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
156
|
-
"button",
|
|
157
|
-
{
|
|
158
|
-
type: "button",
|
|
159
|
-
"data-react-fancy-composer-paste-pill-remove": paste.id,
|
|
160
|
-
onClick: () => pills.remove(paste.id),
|
|
161
|
-
disabled,
|
|
162
|
-
"aria-label": "Discard this pasted text",
|
|
163
|
-
className: "absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100",
|
|
164
|
-
children: "\xD7"
|
|
165
|
-
}
|
|
166
|
-
),
|
|
167
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
168
|
-
"span",
|
|
169
|
-
{
|
|
170
|
-
id: `${paste.id}-preview`,
|
|
171
|
-
"data-react-fancy-composer-paste-pill-preview": paste.id,
|
|
172
|
-
role: "tooltip",
|
|
173
|
-
className: "pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900",
|
|
174
|
-
children: endsOf(paste.text)
|
|
175
|
-
}
|
|
176
|
-
)
|
|
177
|
-
] }, paste.id)
|
|
178
|
-
))
|
|
179
|
-
}
|
|
180
|
-
),
|
|
181
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
182
|
-
"textarea",
|
|
183
|
-
{
|
|
184
|
-
ref: textareaRef,
|
|
185
|
-
value,
|
|
186
|
-
onChange: (e) => setValue(e.target.value),
|
|
187
|
-
onKeyDown: handleKeyDown,
|
|
188
|
-
onPaste: handlePaste,
|
|
189
|
-
placeholder,
|
|
190
|
-
disabled,
|
|
191
|
-
rows: 3,
|
|
192
|
-
className: "w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400"
|
|
193
|
-
}
|
|
194
|
-
),
|
|
195
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800", children: [
|
|
196
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: actions }),
|
|
197
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
198
|
-
"button",
|
|
199
|
-
{
|
|
200
|
-
type: "button",
|
|
201
|
-
onClick: handleSubmit,
|
|
202
|
-
disabled: disabled || !hasContent,
|
|
203
|
-
className: "rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100",
|
|
204
|
-
children: "Send"
|
|
205
|
-
}
|
|
206
|
-
)
|
|
207
|
-
] })
|
|
208
|
-
]
|
|
209
|
-
}
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
);
|
|
213
|
-
Composer.displayName = "Composer";
|
|
214
|
-
|
|
215
|
-
exports.Composer = Composer;
|
|
216
|
-
exports.usePastePills = usePastePills;
|
|
217
|
-
//# sourceMappingURL=chunk-23FTDEGR.cjs.map
|
|
218
|
-
//# sourceMappingURL=chunk-23FTDEGR.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-paste-pills.ts","../src/components/Composer/Composer.tsx"],"names":["useControllableState","useRef","useCallback","forwardRef","Composer","jsxs","cn","jsx"],"mappings":"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","file":"chunk-23FTDEGR.cjs","sourcesContent":["import { useCallback, useRef } from \"react\";\nimport { useControllableState } from \"./use-controllable-state\";\n\n/**\n * A paste held beside a composer rather than inserted into it.\n *\n * There is deliberately **no `length` field.** It would be `text.length` and\n * nothing else, and a derived value stored beside its source is a value that can\n * disagree with it — the two drift the moment anything edits one. Read\n * `paste.text.length`.\n */\nexport interface HeldPaste {\n id: string;\n text: string;\n}\n\n/**\n * The minimum of a clipboard event this hook touches.\n *\n * Structural rather than `React.ClipboardEvent` so a host can call it from a\n * native listener, a synthetic one, or a test double. React's own event satisfies\n * it.\n */\nexport interface PasteEventLike {\n clipboardData?: { getData(type: string): string } | null;\n preventDefault(): void;\n}\n\nexport interface UsePastePillsOptions {\n /**\n * Hold a paste of at least this many characters. Try 500 — about a long\n * paragraph.\n *\n * **No default, on purpose.** Paste is a fundamental interaction, so holding one\n * is something a host asks for rather than something it discovers. Omit this and\n * `handlePaste` always returns `false` and touches nothing.\n */\n threshold?: number;\n /** Own the list yourself. Omit to let the hook keep it. */\n heldPastes?: HeldPaste[];\n onHeldPastesChange?: (pastes: HeldPaste[]) => void;\n}\n\nexport interface UsePastePillsResult {\n heldPastes: HeldPaste[];\n /**\n * Call this from your OWN paste handler. Returns `true` if it took the paste —\n * in which case it has already called `preventDefault()` — and `false` if it\n * declined, having touched nothing.\n *\n * Composable on purpose: a host whose handler already intercepts paste for its\n * own reasons needs to ask \"did you want this?\" rather than hand over the event.\n *\n * function onPaste(e) {\n * if (pills.handlePaste(e)) return;\n * // ...your own handling\n * }\n */\n handlePaste: (event: PasteEventLike) => boolean;\n /**\n * Removes the paste and returns its text, for the host to insert into its own\n * surface. `undefined` if there is no such id.\n *\n * The hook cannot insert it — it owns no DOM and does not know what a host's\n * surface is. That is the point.\n */\n expand: (id: string) => string | undefined;\n /** Discard one without returning it. */\n remove: (id: string) => void;\n /** Empty the list, e.g. after a send. */\n clear: () => void;\n}\n\n/**\n * Hold a large paste as state, so a composer can show it as a pill instead of\n * turning itself into a scrolling wall.\n *\n * ---------------------------------------------------------------------------\n * Why this is a hook and not just a feature of `Composer`\n * ---------------------------------------------------------------------------\n *\n * `Composer` shipped the behaviour in 5.31.0 and the MOIC estate, who specified it,\n * then established something I had missed: their chat input is a contenteditable\n * rendering inline `[deal:42|Acme]` badges, so they **cannot** adopt `Composer` (a\n * plain textarea) however much they want its behaviour. Their plan to delete their\n * own composer when ours landed could never have worked.\n *\n * So the logic lives here and **`Composer` is its first caller.** One\n * implementation rather than two, which matters more than the convenience: two\n * copies of a rule drift, and the copy nobody reads is the one that rots. The rule\n * that must not drift is the one in `Composer`'s own tests — a send with pills\n * still closed carries the text.\n *\n * It owns no DOM. The host renders the pills.\n *\n * ---------------------------------------------------------------------------\n * Who actually calls this (as of 5.32.0): `Composer`, and nothing else\n * ---------------------------------------------------------------------------\n *\n * The estate that specified it **withdrew before adopting it**, hours after it\n * shipped — their design moved the pill INTO the message as a `[paste:id|label]`\n * marker in the same grammar as their other object tags, which makes the message\n * itself the record of what is attached. A parallel `heldPastes` array would then be\n * a second place answering \"what am I sending?\", and the two would disagree the\n * moment someone backspaced a badge. Their reasoning was better than the spec.\n *\n * Recorded because a reader should not infer an extension point under active\n * external use. It is kept rather than withdrawn for two reasons that are about\n * cost, not optimism: it is published API on a 5.x package, so removing it needs a\n * major; and `Composer` calls it, so it is exercised by that component's tests on\n * every run and cannot rot unnoticed.\n *\n * **It gets no further surface without a real caller.** No extra options, no second\n * hook. An API with one hypothetical consumer is how a package accumulates surface\n * nobody uses.\n */\nexport function usePastePills({\n threshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n}: UsePastePillsOptions = {}): UsePastePillsResult {\n const [heldPastes, setHeldPastes] = useControllableState<HeldPaste[]>(\n controlledHeldPastes,\n [],\n onHeldPastesChange,\n );\n\n /*\n * A counter rather than `crypto.randomUUID()` or an index.\n *\n * An index is not an id — removing the first pill renumbers the rest, and a host\n * keying its DOM on that remounts every pill and loses hover state. A counter in\n * a ref is stable for the life of the component and needs no platform API that a\n * server renderer might lack.\n */\n const nextId = useRef(0);\n\n const handlePaste = useCallback(\n (event: PasteEventLike): boolean => {\n if (!threshold || threshold <= 0) return false;\n\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n\n // Below the threshold, decline WITHOUT touching the event. A host calling\n // this from inside its own handler has to be able to carry on as if the hook\n // were not there.\n if (text.length < threshold) return false;\n\n event.preventDefault();\n nextId.current += 1;\n const id = `paste-${nextId.current}`;\n setHeldPastes((current) => [...current, { id, text }]);\n\n return true;\n },\n [threshold, setHeldPastes],\n );\n\n const expand = useCallback(\n (id: string): string | undefined => {\n const found = heldPastes.find((p) => p.id === id);\n\n if (found === undefined) return undefined;\n\n setHeldPastes((current) => current.filter((p) => p.id !== id));\n\n return found.text;\n },\n [heldPastes, setHeldPastes],\n );\n\n const remove = useCallback(\n (id: string) => setHeldPastes((current) => current.filter((p) => p.id !== id)),\n [setHeldPastes],\n );\n\n const clear = useCallback(() => setHeldPastes([]), [setHeldPastes]);\n\n return { heldPastes, handlePaste, expand, remove, clear };\n}\n","import { forwardRef, useCallback, useRef } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useControllableState } from \"../../hooks/use-controllable-state\";\nimport { usePastePills, type HeldPaste } from \"../../hooks/use-paste-pills\";\nimport type { ComposerProps } from \"./Composer.types\";\n\n/** Characters of a held paste shown at each end of the hover preview. */\nconst PREVIEW_CHARS = 20;\n\n/**\n * Thousands separators, written out rather than left to `toLocaleString()`.\n *\n * The number exists so someone can size the paste at a glance, and `2,036` sizes\n * faster than `2036`. It is not left to the runtime locale because the label beside\n * it (\"Pasted text\") is not localised either, so a locale-formatted number next to\n * an English noun would be half a translation.\n */\nfunction withSeparators(n: number): string {\n return String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n}\n\n/** First and last N characters, middle elided. The ends say WHICH paste this is. */\nfunction endsOf(text: string): string {\n if (text.length <= PREVIEW_CHARS * 2) return text;\n\n return `${text.slice(0, PREVIEW_CHARS)} … ${text.slice(-PREVIEW_CHARS)}`;\n}\n\nexport const Composer = forwardRef<HTMLDivElement, ComposerProps>(\n function Composer(\n {\n value: controlledValue,\n defaultValue = \"\",\n onChange,\n onSubmit,\n placeholder = \"Type a message...\",\n actions,\n disabled = false,\n className,\n pasteThreshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n },\n ref,\n ) {\n const [value, setValue] = useControllableState(\n controlledValue,\n defaultValue,\n onChange,\n );\n /*\n * The holding logic lives in `usePastePills` and this is its first caller.\n *\n * It moved out in 5.32.0 because the estate that specified the feature cannot\n * use this component at all — their chat input is a contenteditable rendering\n * inline tag badges, which a textarea cannot do. One implementation rather than\n * two: two copies of a rule drift, and the copy nobody reads is the one that\n * rots. The rule that must not drift is the one below — a send with pills still\n * closed carries the text.\n */\n const pills = usePastePills({\n threshold: pasteThreshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n });\n const heldPastes = pills.heldPastes;\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handlePaste = useCallback(\n (e: React.ClipboardEvent<HTMLTextAreaElement>) => {\n // Composable by design, so this reads as a question rather than a handover.\n // Nothing else here wants the paste, so the answer is simply ignored.\n pills.handlePaste(e);\n },\n [pills],\n );\n\n /** Everything the person believes they are sending, in the order they built it. */\n const composeOutgoing = useCallback(\n () => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join(\"\\n\\n\"),\n [value, heldPastes],\n );\n\n const hasContent = value.trim().length > 0 || heldPastes.length > 0;\n\n const handleSubmit = useCallback(() => {\n if (!hasContent || disabled) return;\n\n /*\n * THE DANGEROUS STATE, and the reason this feature needed a spec rather than\n * a guess. A pill reads as \"your paste is attached\" — so a send that drops it\n * means asking a question about a document without the document, and nobody\n * can tell: the sender believes it went, the receiver sees a question about\n * nothing.\n *\n * Held pastes go out in arrival order, after what was typed, because people\n * type the question BEFORE pasting the thing it is about.\n */\n onSubmit?.(composeOutgoing());\n setValue(\"\");\n pills.clear();\n textareaRef.current?.focus();\n }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, pills]);\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n /** Click a pill: dump it into the input, APPENDED to what is already typed. */\n const insertPaste = useCallback(\n (paste: HeldPaste) => {\n // `expand` removes it and hands back the text; inserting is this\n // component's job because the hook owns no DOM.\n const text = pills.expand(paste.id);\n\n if (text === undefined) return;\n\n setValue((current) => (current ? `${current}\\n\\n${text}` : text));\n textareaRef.current?.focus();\n },\n [pills, setValue],\n );\n\n return (\n <div\n data-react-fancy-composer=\"\"\n ref={ref}\n className={cn(\n \"rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {heldPastes.length > 0 && (\n <div\n data-react-fancy-composer-paste-pills=\"\"\n className=\"flex flex-wrap gap-1.5 px-3 pt-3\"\n >\n {heldPastes.map((paste) => (\n // `group` + `group-hover` rather than hover state: the preview is\n // presentation, and a useState for it would be one more thing an\n // agent could desynchronise.\n <span key={paste.id} className=\"group relative inline-flex\">\n <button\n type=\"button\"\n data-react-fancy-composer-paste-pill={paste.id}\n onClick={() => insertPaste(paste)}\n disabled={disabled}\n // No `title`. A native tooltip plus the hover preview below\n // renders TWO tooltips on the same hover, at different delays.\n // The label says what clicking does; the preview says what the\n // text is, and it is linked rather than merely adjacent so a\n // screen reader reaches it too.\n aria-label={`Insert this pasted text, ${withSeparators(paste.text.length)} characters`}\n aria-describedby={`${paste.id}-preview`}\n // `pr-7` leaves room for the absolutely-positioned × that sits\n // over this button's right edge. With a smaller right padding the\n // × lands on top of the label.\n className=\"inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700\"\n >\n <span>\n Pasted text · {withSeparators(paste.text.length)} characters\n </span>\n </button>\n\n <button\n type=\"button\"\n data-react-fancy-composer-paste-pill-remove={paste.id}\n onClick={() => pills.remove(paste.id)}\n disabled={disabled}\n aria-label=\"Discard this pasted text\"\n className=\"absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100\"\n >\n ×\n </button>\n\n <span\n id={`${paste.id}-preview`}\n data-react-fancy-composer-paste-pill-preview={paste.id}\n role=\"tooltip\"\n className=\"pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900\"\n >\n {endsOf(paste.text)}\n </span>\n </span>\n ))}\n </div>\n )}\n\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n placeholder={placeholder}\n disabled={disabled}\n rows={3}\n className=\"w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400\"\n />\n <div className=\"flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800\">\n <div className=\"flex items-center gap-1\">{actions}</div>\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={disabled || !hasContent}\n className=\"rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100\"\n >\n Send\n </button>\n </div>\n </div>\n );\n },\n);\n\nComposer.displayName = \"Composer\";\n"]}
|
package/dist/chunk-2XAVAPDA.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { useControllableState } from './chunk-EDYAXGUM.js';
|
|
2
|
-
import { cn } from './chunk-MMW2JGKZ.js';
|
|
3
|
-
import { forwardRef, useRef, useCallback } from 'react';
|
|
4
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
-
|
|
6
|
-
function usePastePills({
|
|
7
|
-
threshold,
|
|
8
|
-
heldPastes: controlledHeldPastes,
|
|
9
|
-
onHeldPastesChange
|
|
10
|
-
} = {}) {
|
|
11
|
-
const [heldPastes, setHeldPastes] = useControllableState(
|
|
12
|
-
controlledHeldPastes,
|
|
13
|
-
[],
|
|
14
|
-
onHeldPastesChange
|
|
15
|
-
);
|
|
16
|
-
const nextId = useRef(0);
|
|
17
|
-
const handlePaste = useCallback(
|
|
18
|
-
(event) => {
|
|
19
|
-
if (!threshold || threshold <= 0) return false;
|
|
20
|
-
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
21
|
-
if (text.length < threshold) return false;
|
|
22
|
-
event.preventDefault();
|
|
23
|
-
nextId.current += 1;
|
|
24
|
-
const id = `paste-${nextId.current}`;
|
|
25
|
-
setHeldPastes((current) => [...current, { id, text }]);
|
|
26
|
-
return true;
|
|
27
|
-
},
|
|
28
|
-
[threshold, setHeldPastes]
|
|
29
|
-
);
|
|
30
|
-
const expand = useCallback(
|
|
31
|
-
(id) => {
|
|
32
|
-
const found = heldPastes.find((p) => p.id === id);
|
|
33
|
-
if (found === void 0) return void 0;
|
|
34
|
-
setHeldPastes((current) => current.filter((p) => p.id !== id));
|
|
35
|
-
return found.text;
|
|
36
|
-
},
|
|
37
|
-
[heldPastes, setHeldPastes]
|
|
38
|
-
);
|
|
39
|
-
const remove = useCallback(
|
|
40
|
-
(id) => setHeldPastes((current) => current.filter((p) => p.id !== id)),
|
|
41
|
-
[setHeldPastes]
|
|
42
|
-
);
|
|
43
|
-
const clear = useCallback(() => setHeldPastes([]), [setHeldPastes]);
|
|
44
|
-
return { heldPastes, handlePaste, expand, remove, clear };
|
|
45
|
-
}
|
|
46
|
-
var PREVIEW_CHARS = 20;
|
|
47
|
-
function withSeparators(n) {
|
|
48
|
-
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
49
|
-
}
|
|
50
|
-
function endsOf(text) {
|
|
51
|
-
if (text.length <= PREVIEW_CHARS * 2) return text;
|
|
52
|
-
return `${text.slice(0, PREVIEW_CHARS)} \u2026 ${text.slice(-PREVIEW_CHARS)}`;
|
|
53
|
-
}
|
|
54
|
-
var Composer = forwardRef(
|
|
55
|
-
function Composer2({
|
|
56
|
-
value: controlledValue,
|
|
57
|
-
defaultValue = "",
|
|
58
|
-
onChange,
|
|
59
|
-
onSubmit,
|
|
60
|
-
placeholder = "Type a message...",
|
|
61
|
-
actions,
|
|
62
|
-
disabled = false,
|
|
63
|
-
className,
|
|
64
|
-
pasteThreshold,
|
|
65
|
-
heldPastes: controlledHeldPastes,
|
|
66
|
-
onHeldPastesChange
|
|
67
|
-
}, ref) {
|
|
68
|
-
const [value, setValue] = useControllableState(
|
|
69
|
-
controlledValue,
|
|
70
|
-
defaultValue,
|
|
71
|
-
onChange
|
|
72
|
-
);
|
|
73
|
-
const pills = usePastePills({
|
|
74
|
-
threshold: pasteThreshold,
|
|
75
|
-
heldPastes: controlledHeldPastes,
|
|
76
|
-
onHeldPastesChange
|
|
77
|
-
});
|
|
78
|
-
const heldPastes = pills.heldPastes;
|
|
79
|
-
const textareaRef = useRef(null);
|
|
80
|
-
const handlePaste = useCallback(
|
|
81
|
-
(e) => {
|
|
82
|
-
pills.handlePaste(e);
|
|
83
|
-
},
|
|
84
|
-
[pills]
|
|
85
|
-
);
|
|
86
|
-
const composeOutgoing = useCallback(
|
|
87
|
-
() => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join("\n\n"),
|
|
88
|
-
[value, heldPastes]
|
|
89
|
-
);
|
|
90
|
-
const hasContent = value.trim().length > 0 || heldPastes.length > 0;
|
|
91
|
-
const handleSubmit = useCallback(() => {
|
|
92
|
-
if (!hasContent || disabled) return;
|
|
93
|
-
onSubmit?.(composeOutgoing());
|
|
94
|
-
setValue("");
|
|
95
|
-
pills.clear();
|
|
96
|
-
textareaRef.current?.focus();
|
|
97
|
-
}, [hasContent, disabled, onSubmit, composeOutgoing, setValue, pills]);
|
|
98
|
-
const handleKeyDown = (e) => {
|
|
99
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
handleSubmit();
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const insertPaste = useCallback(
|
|
105
|
-
(paste) => {
|
|
106
|
-
const text = pills.expand(paste.id);
|
|
107
|
-
if (text === void 0) return;
|
|
108
|
-
setValue((current) => current ? `${current}
|
|
109
|
-
|
|
110
|
-
${text}` : text);
|
|
111
|
-
textareaRef.current?.focus();
|
|
112
|
-
},
|
|
113
|
-
[pills, setValue]
|
|
114
|
-
);
|
|
115
|
-
return /* @__PURE__ */ jsxs(
|
|
116
|
-
"div",
|
|
117
|
-
{
|
|
118
|
-
"data-react-fancy-composer": "",
|
|
119
|
-
ref,
|
|
120
|
-
className: cn(
|
|
121
|
-
"rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
|
|
122
|
-
disabled && "opacity-50",
|
|
123
|
-
className
|
|
124
|
-
),
|
|
125
|
-
children: [
|
|
126
|
-
heldPastes.length > 0 && /* @__PURE__ */ jsx(
|
|
127
|
-
"div",
|
|
128
|
-
{
|
|
129
|
-
"data-react-fancy-composer-paste-pills": "",
|
|
130
|
-
className: "flex flex-wrap gap-1.5 px-3 pt-3",
|
|
131
|
-
children: heldPastes.map((paste) => (
|
|
132
|
-
// `group` + `group-hover` rather than hover state: the preview is
|
|
133
|
-
// presentation, and a useState for it would be one more thing an
|
|
134
|
-
// agent could desynchronise.
|
|
135
|
-
/* @__PURE__ */ jsxs("span", { className: "group relative inline-flex", children: [
|
|
136
|
-
/* @__PURE__ */ jsx(
|
|
137
|
-
"button",
|
|
138
|
-
{
|
|
139
|
-
type: "button",
|
|
140
|
-
"data-react-fancy-composer-paste-pill": paste.id,
|
|
141
|
-
onClick: () => insertPaste(paste),
|
|
142
|
-
disabled,
|
|
143
|
-
"aria-label": `Insert this pasted text, ${withSeparators(paste.text.length)} characters`,
|
|
144
|
-
"aria-describedby": `${paste.id}-preview`,
|
|
145
|
-
className: "inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700",
|
|
146
|
-
children: /* @__PURE__ */ jsxs("span", { children: [
|
|
147
|
-
"Pasted text \xB7 ",
|
|
148
|
-
withSeparators(paste.text.length),
|
|
149
|
-
" characters"
|
|
150
|
-
] })
|
|
151
|
-
}
|
|
152
|
-
),
|
|
153
|
-
/* @__PURE__ */ jsx(
|
|
154
|
-
"button",
|
|
155
|
-
{
|
|
156
|
-
type: "button",
|
|
157
|
-
"data-react-fancy-composer-paste-pill-remove": paste.id,
|
|
158
|
-
onClick: () => pills.remove(paste.id),
|
|
159
|
-
disabled,
|
|
160
|
-
"aria-label": "Discard this pasted text",
|
|
161
|
-
className: "absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100",
|
|
162
|
-
children: "\xD7"
|
|
163
|
-
}
|
|
164
|
-
),
|
|
165
|
-
/* @__PURE__ */ jsx(
|
|
166
|
-
"span",
|
|
167
|
-
{
|
|
168
|
-
id: `${paste.id}-preview`,
|
|
169
|
-
"data-react-fancy-composer-paste-pill-preview": paste.id,
|
|
170
|
-
role: "tooltip",
|
|
171
|
-
className: "pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900",
|
|
172
|
-
children: endsOf(paste.text)
|
|
173
|
-
}
|
|
174
|
-
)
|
|
175
|
-
] }, paste.id)
|
|
176
|
-
))
|
|
177
|
-
}
|
|
178
|
-
),
|
|
179
|
-
/* @__PURE__ */ jsx(
|
|
180
|
-
"textarea",
|
|
181
|
-
{
|
|
182
|
-
ref: textareaRef,
|
|
183
|
-
value,
|
|
184
|
-
onChange: (e) => setValue(e.target.value),
|
|
185
|
-
onKeyDown: handleKeyDown,
|
|
186
|
-
onPaste: handlePaste,
|
|
187
|
-
placeholder,
|
|
188
|
-
disabled,
|
|
189
|
-
rows: 3,
|
|
190
|
-
className: "w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400"
|
|
191
|
-
}
|
|
192
|
-
),
|
|
193
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800", children: [
|
|
194
|
-
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: actions }),
|
|
195
|
-
/* @__PURE__ */ jsx(
|
|
196
|
-
"button",
|
|
197
|
-
{
|
|
198
|
-
type: "button",
|
|
199
|
-
onClick: handleSubmit,
|
|
200
|
-
disabled: disabled || !hasContent,
|
|
201
|
-
className: "rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100",
|
|
202
|
-
children: "Send"
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
] })
|
|
206
|
-
]
|
|
207
|
-
}
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
);
|
|
211
|
-
Composer.displayName = "Composer";
|
|
212
|
-
|
|
213
|
-
export { Composer, usePastePills };
|
|
214
|
-
//# sourceMappingURL=chunk-2XAVAPDA.js.map
|
|
215
|
-
//# sourceMappingURL=chunk-2XAVAPDA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-paste-pills.ts","../src/components/Composer/Composer.tsx"],"names":["Composer","useRef","useCallback"],"mappings":"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","file":"chunk-2XAVAPDA.js","sourcesContent":["import { useCallback, useRef } from \"react\";\nimport { useControllableState } from \"./use-controllable-state\";\n\n/**\n * A paste held beside a composer rather than inserted into it.\n *\n * There is deliberately **no `length` field.** It would be `text.length` and\n * nothing else, and a derived value stored beside its source is a value that can\n * disagree with it — the two drift the moment anything edits one. Read\n * `paste.text.length`.\n */\nexport interface HeldPaste {\n id: string;\n text: string;\n}\n\n/**\n * The minimum of a clipboard event this hook touches.\n *\n * Structural rather than `React.ClipboardEvent` so a host can call it from a\n * native listener, a synthetic one, or a test double. React's own event satisfies\n * it.\n */\nexport interface PasteEventLike {\n clipboardData?: { getData(type: string): string } | null;\n preventDefault(): void;\n}\n\nexport interface UsePastePillsOptions {\n /**\n * Hold a paste of at least this many characters. Try 500 — about a long\n * paragraph.\n *\n * **No default, on purpose.** Paste is a fundamental interaction, so holding one\n * is something a host asks for rather than something it discovers. Omit this and\n * `handlePaste` always returns `false` and touches nothing.\n */\n threshold?: number;\n /** Own the list yourself. Omit to let the hook keep it. */\n heldPastes?: HeldPaste[];\n onHeldPastesChange?: (pastes: HeldPaste[]) => void;\n}\n\nexport interface UsePastePillsResult {\n heldPastes: HeldPaste[];\n /**\n * Call this from your OWN paste handler. Returns `true` if it took the paste —\n * in which case it has already called `preventDefault()` — and `false` if it\n * declined, having touched nothing.\n *\n * Composable on purpose: a host whose handler already intercepts paste for its\n * own reasons needs to ask \"did you want this?\" rather than hand over the event.\n *\n * function onPaste(e) {\n * if (pills.handlePaste(e)) return;\n * // ...your own handling\n * }\n */\n handlePaste: (event: PasteEventLike) => boolean;\n /**\n * Removes the paste and returns its text, for the host to insert into its own\n * surface. `undefined` if there is no such id.\n *\n * The hook cannot insert it — it owns no DOM and does not know what a host's\n * surface is. That is the point.\n */\n expand: (id: string) => string | undefined;\n /** Discard one without returning it. */\n remove: (id: string) => void;\n /** Empty the list, e.g. after a send. */\n clear: () => void;\n}\n\n/**\n * Hold a large paste as state, so a composer can show it as a pill instead of\n * turning itself into a scrolling wall.\n *\n * ---------------------------------------------------------------------------\n * Why this is a hook and not just a feature of `Composer`\n * ---------------------------------------------------------------------------\n *\n * `Composer` shipped the behaviour in 5.31.0 and the MOIC estate, who specified it,\n * then established something I had missed: their chat input is a contenteditable\n * rendering inline `[deal:42|Acme]` badges, so they **cannot** adopt `Composer` (a\n * plain textarea) however much they want its behaviour. Their plan to delete their\n * own composer when ours landed could never have worked.\n *\n * So the logic lives here and **`Composer` is its first caller.** One\n * implementation rather than two, which matters more than the convenience: two\n * copies of a rule drift, and the copy nobody reads is the one that rots. The rule\n * that must not drift is the one in `Composer`'s own tests — a send with pills\n * still closed carries the text.\n *\n * It owns no DOM. The host renders the pills.\n *\n * ---------------------------------------------------------------------------\n * Who actually calls this (as of 5.32.0): `Composer`, and nothing else\n * ---------------------------------------------------------------------------\n *\n * The estate that specified it **withdrew before adopting it**, hours after it\n * shipped — their design moved the pill INTO the message as a `[paste:id|label]`\n * marker in the same grammar as their other object tags, which makes the message\n * itself the record of what is attached. A parallel `heldPastes` array would then be\n * a second place answering \"what am I sending?\", and the two would disagree the\n * moment someone backspaced a badge. Their reasoning was better than the spec.\n *\n * Recorded because a reader should not infer an extension point under active\n * external use. It is kept rather than withdrawn for two reasons that are about\n * cost, not optimism: it is published API on a 5.x package, so removing it needs a\n * major; and `Composer` calls it, so it is exercised by that component's tests on\n * every run and cannot rot unnoticed.\n *\n * **It gets no further surface without a real caller.** No extra options, no second\n * hook. An API with one hypothetical consumer is how a package accumulates surface\n * nobody uses.\n */\nexport function usePastePills({\n threshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n}: UsePastePillsOptions = {}): UsePastePillsResult {\n const [heldPastes, setHeldPastes] = useControllableState<HeldPaste[]>(\n controlledHeldPastes,\n [],\n onHeldPastesChange,\n );\n\n /*\n * A counter rather than `crypto.randomUUID()` or an index.\n *\n * An index is not an id — removing the first pill renumbers the rest, and a host\n * keying its DOM on that remounts every pill and loses hover state. A counter in\n * a ref is stable for the life of the component and needs no platform API that a\n * server renderer might lack.\n */\n const nextId = useRef(0);\n\n const handlePaste = useCallback(\n (event: PasteEventLike): boolean => {\n if (!threshold || threshold <= 0) return false;\n\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n\n // Below the threshold, decline WITHOUT touching the event. A host calling\n // this from inside its own handler has to be able to carry on as if the hook\n // were not there.\n if (text.length < threshold) return false;\n\n event.preventDefault();\n nextId.current += 1;\n const id = `paste-${nextId.current}`;\n setHeldPastes((current) => [...current, { id, text }]);\n\n return true;\n },\n [threshold, setHeldPastes],\n );\n\n const expand = useCallback(\n (id: string): string | undefined => {\n const found = heldPastes.find((p) => p.id === id);\n\n if (found === undefined) return undefined;\n\n setHeldPastes((current) => current.filter((p) => p.id !== id));\n\n return found.text;\n },\n [heldPastes, setHeldPastes],\n );\n\n const remove = useCallback(\n (id: string) => setHeldPastes((current) => current.filter((p) => p.id !== id)),\n [setHeldPastes],\n );\n\n const clear = useCallback(() => setHeldPastes([]), [setHeldPastes]);\n\n return { heldPastes, handlePaste, expand, remove, clear };\n}\n","import { forwardRef, useCallback, useRef } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useControllableState } from \"../../hooks/use-controllable-state\";\nimport { usePastePills, type HeldPaste } from \"../../hooks/use-paste-pills\";\nimport type { ComposerProps } from \"./Composer.types\";\n\n/** Characters of a held paste shown at each end of the hover preview. */\nconst PREVIEW_CHARS = 20;\n\n/**\n * Thousands separators, written out rather than left to `toLocaleString()`.\n *\n * The number exists so someone can size the paste at a glance, and `2,036` sizes\n * faster than `2036`. It is not left to the runtime locale because the label beside\n * it (\"Pasted text\") is not localised either, so a locale-formatted number next to\n * an English noun would be half a translation.\n */\nfunction withSeparators(n: number): string {\n return String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n}\n\n/** First and last N characters, middle elided. The ends say WHICH paste this is. */\nfunction endsOf(text: string): string {\n if (text.length <= PREVIEW_CHARS * 2) return text;\n\n return `${text.slice(0, PREVIEW_CHARS)} … ${text.slice(-PREVIEW_CHARS)}`;\n}\n\nexport const Composer = forwardRef<HTMLDivElement, ComposerProps>(\n function Composer(\n {\n value: controlledValue,\n defaultValue = \"\",\n onChange,\n onSubmit,\n placeholder = \"Type a message...\",\n actions,\n disabled = false,\n className,\n pasteThreshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n },\n ref,\n ) {\n const [value, setValue] = useControllableState(\n controlledValue,\n defaultValue,\n onChange,\n );\n /*\n * The holding logic lives in `usePastePills` and this is its first caller.\n *\n * It moved out in 5.32.0 because the estate that specified the feature cannot\n * use this component at all — their chat input is a contenteditable rendering\n * inline tag badges, which a textarea cannot do. One implementation rather than\n * two: two copies of a rule drift, and the copy nobody reads is the one that\n * rots. The rule that must not drift is the one below — a send with pills still\n * closed carries the text.\n */\n const pills = usePastePills({\n threshold: pasteThreshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n });\n const heldPastes = pills.heldPastes;\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handlePaste = useCallback(\n (e: React.ClipboardEvent<HTMLTextAreaElement>) => {\n // Composable by design, so this reads as a question rather than a handover.\n // Nothing else here wants the paste, so the answer is simply ignored.\n pills.handlePaste(e);\n },\n [pills],\n );\n\n /** Everything the person believes they are sending, in the order they built it. */\n const composeOutgoing = useCallback(\n () => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join(\"\\n\\n\"),\n [value, heldPastes],\n );\n\n const hasContent = value.trim().length > 0 || heldPastes.length > 0;\n\n const handleSubmit = useCallback(() => {\n if (!hasContent || disabled) return;\n\n /*\n * THE DANGEROUS STATE, and the reason this feature needed a spec rather than\n * a guess. A pill reads as \"your paste is attached\" — so a send that drops it\n * means asking a question about a document without the document, and nobody\n * can tell: the sender believes it went, the receiver sees a question about\n * nothing.\n *\n * Held pastes go out in arrival order, after what was typed, because people\n * type the question BEFORE pasting the thing it is about.\n */\n onSubmit?.(composeOutgoing());\n setValue(\"\");\n pills.clear();\n textareaRef.current?.focus();\n }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, pills]);\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n /** Click a pill: dump it into the input, APPENDED to what is already typed. */\n const insertPaste = useCallback(\n (paste: HeldPaste) => {\n // `expand` removes it and hands back the text; inserting is this\n // component's job because the hook owns no DOM.\n const text = pills.expand(paste.id);\n\n if (text === undefined) return;\n\n setValue((current) => (current ? `${current}\\n\\n${text}` : text));\n textareaRef.current?.focus();\n },\n [pills, setValue],\n );\n\n return (\n <div\n data-react-fancy-composer=\"\"\n ref={ref}\n className={cn(\n \"rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {heldPastes.length > 0 && (\n <div\n data-react-fancy-composer-paste-pills=\"\"\n className=\"flex flex-wrap gap-1.5 px-3 pt-3\"\n >\n {heldPastes.map((paste) => (\n // `group` + `group-hover` rather than hover state: the preview is\n // presentation, and a useState for it would be one more thing an\n // agent could desynchronise.\n <span key={paste.id} className=\"group relative inline-flex\">\n <button\n type=\"button\"\n data-react-fancy-composer-paste-pill={paste.id}\n onClick={() => insertPaste(paste)}\n disabled={disabled}\n // No `title`. A native tooltip plus the hover preview below\n // renders TWO tooltips on the same hover, at different delays.\n // The label says what clicking does; the preview says what the\n // text is, and it is linked rather than merely adjacent so a\n // screen reader reaches it too.\n aria-label={`Insert this pasted text, ${withSeparators(paste.text.length)} characters`}\n aria-describedby={`${paste.id}-preview`}\n // `pr-7` leaves room for the absolutely-positioned × that sits\n // over this button's right edge. With a smaller right padding the\n // × lands on top of the label.\n className=\"inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700\"\n >\n <span>\n Pasted text · {withSeparators(paste.text.length)} characters\n </span>\n </button>\n\n <button\n type=\"button\"\n data-react-fancy-composer-paste-pill-remove={paste.id}\n onClick={() => pills.remove(paste.id)}\n disabled={disabled}\n aria-label=\"Discard this pasted text\"\n className=\"absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100\"\n >\n ×\n </button>\n\n <span\n id={`${paste.id}-preview`}\n data-react-fancy-composer-paste-pill-preview={paste.id}\n role=\"tooltip\"\n className=\"pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900\"\n >\n {endsOf(paste.text)}\n </span>\n </span>\n ))}\n </div>\n )}\n\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n placeholder={placeholder}\n disabled={disabled}\n rows={3}\n className=\"w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400\"\n />\n <div className=\"flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800\">\n <div className=\"flex items-center gap-1\">{actions}</div>\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={disabled || !hasContent}\n className=\"rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100\"\n >\n Send\n </button>\n </div>\n </div>\n );\n },\n);\n\nComposer.displayName = \"Composer\";\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/Tooltip/Tooltip.tsx"],"names":["Tooltip"],"mappings":";;;;;;AAaO,IAAM,OAAA,GAAU,UAAA;AAAA,EACrB,SAASA,QAAAA,CACP,EAAE,QAAA,EAAU,OAAA,EAAS,SAAA,GAAY,KAAA,EAAO,KAAA,GAAQ,GAAA,EAAK,MAAA,GAAS,CAAA,EAAG,SAAA,IACjE,IAAA,EACA;AACA,IAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,KAAK,CAAA;AACtC,IAAA,MAAM,SAAA,GAAY,OAAoB,IAAI,CAAA;AAC1C,IAAA,MAAM,WAAA,GAAc,OAAuB,IAAI,CAAA;AAC/C,IAAA,MAAM,UAAA,GAAa,OAAsC,MAAS,CAAA;AAElE,IAAA,MAAM,QAAA,GAAW,mBAAA,CAAoB,SAAA,EAAW,WAAA,EAAa;AAAA,MAC3D,SAAA;AAAA,MACA,MAAA;AAAA,MACA,OAAA,EAAS;AAAA,KACV,CAAA;AAED,IAAA,MAAM,IAAA,GAAO,YAAY,MAAM;AAC7B,MAAA,UAAA,CAAW,UAAU,UAAA,CAAW,MAAM,OAAA,CAAQ,IAAI,GAAG,KAAK,CAAA;AAAA,IAC5D,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,IAAA,MAAM,IAAA,GAAO,YAAY,MAAM;AAC7B,MAAA,YAAA,CAAa,WAAW,OAAO,CAAA;AAC/B,MAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,IACf,CAAA,EAAG,EAAE,CAAA;AAEL,IAAA,MAAM,OAAA,GAAU,aAAa,QAAA,EAAmD;AAAA,MAC9E,GAAA,EAAK,SAAA;AAAA,MACL,YAAA,EAAc,IAAA;AAAA,MACd,YAAA,EAAc,IAAA;AAAA,MACd,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ;AAAA,KACT,CAAA;AAED,IAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,MAAA,OAAA;AAAA,MACA,IAAA,wBACE,MAAA,EAAA,EACC,QAAA,kBAAA,IAAA;AAAA,QAAC,KAAA;AAAA,QAAA;AAAA,UACC,GAAA,EAAK,WAAA;AAAA,UACL,0BAAA,EAAyB,EAAA;AAAA,UACzB,IAAA,EAAK,SAAA;AAAA,UACL,SAAA,EAAW,EAAA;AAAA,YACT,2JAAA;AAAA,YACA;AAAA,WACF;AAAA,UACA,OAAO,EAAE,IAAA,EAAM,SAAS,CAAA,EAAG,GAAA,EAAK,SAAS,CAAA,EAAE;AAAA,UAE1C,QAAA,EAAA;AAAA,YAAA,OAAA;AAAA,4BACD,GAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBACC,SAAA,EAAW,EAAA;AAAA,kBACT,yDAAA;AAAA,kBACA,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,KAAK,CAAA,IACjC,yCAAA;AAAA,kBACF,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,QAAQ,CAAA,IACpC,sCAAA;AAAA,kBACF,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,MAAM,CAAA,IAClC,uCAAA;AAAA,kBACF,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,OAAO,CAAA,IACnC;AAAA;AACJ;AAAA;AACF;AAAA;AAAA,OACF,EACF;AAAA,KAAA,EAEJ,CAAA;AAAA,EAEJ;AACF;AAEA,OAAA,CAAQ,WAAA,GAAc,SAAA","file":"chunk-35EN5AYE.js","sourcesContent":["import {\n cloneElement,\n forwardRef,\n useCallback,\n useRef,\n useState,\n type ReactElement,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Portal } from \"../Portal\";\nimport { useFloatingPosition } from \"../../hooks/use-floating-position\";\nimport type { TooltipProps } from \"./Tooltip.types\";\n\nexport const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(\n function Tooltip(\n { children, content, placement = \"top\", delay = 200, offset = 8, className },\n _ref,\n ) {\n const [open, setOpen] = useState(false);\n const anchorRef = useRef<HTMLElement>(null);\n const floatingRef = useRef<HTMLDivElement>(null);\n const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n const position = useFloatingPosition(anchorRef, floatingRef, {\n placement,\n offset,\n enabled: open,\n });\n\n const show = useCallback(() => {\n timeoutRef.current = setTimeout(() => setOpen(true), delay);\n }, [delay]);\n\n const hide = useCallback(() => {\n clearTimeout(timeoutRef.current);\n setOpen(false);\n }, []);\n\n const trigger = cloneElement(children as ReactElement<Record<string, unknown>>, {\n ref: anchorRef,\n onMouseEnter: show,\n onMouseLeave: hide,\n onFocus: show,\n onBlur: hide,\n });\n\n return (\n <>\n {trigger}\n {open && (\n <Portal>\n <div\n ref={floatingRef}\n data-react-fancy-tooltip=\"\"\n role=\"tooltip\"\n className={cn(\n \"fancy-fade-in pointer-events-none fixed z-50 max-w-xs rounded-lg bg-zinc-900 px-3 py-1.5 text-sm text-white shadow-lg dark:bg-zinc-700 dark:text-zinc-100\",\n className,\n )}\n style={{ left: position.x, top: position.y }}\n >\n {content}\n <div\n className={cn(\n \"absolute h-2 w-2 rotate-45 bg-zinc-900 dark:bg-zinc-700\",\n position.placement.startsWith(\"top\") &&\n \"bottom-[-4px] left-1/2 -translate-x-1/2\",\n position.placement.startsWith(\"bottom\") &&\n \"top-[-4px] left-1/2 -translate-x-1/2\",\n position.placement.startsWith(\"left\") &&\n \"right-[-4px] top-1/2 -translate-y-1/2\",\n position.placement.startsWith(\"right\") &&\n \"left-[-4px] top-1/2 -translate-y-1/2\",\n )}\n />\n </div>\n </Portal>\n )}\n </>\n );\n },\n);\n\nTooltip.displayName = \"Tooltip\";\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/Tooltip/Tooltip.tsx"],"names":["forwardRef","Tooltip","useState","useRef","useFloatingPosition","useCallback","cloneElement","jsxs","Fragment","Portal","cn","jsx"],"mappings":";;;;;;;;AAaO,IAAM,OAAA,GAAUA,gBAAA;AAAA,EACrB,SAASC,QAAAA,CACP,EAAE,QAAA,EAAU,OAAA,EAAS,SAAA,GAAY,KAAA,EAAO,KAAA,GAAQ,GAAA,EAAK,MAAA,GAAS,CAAA,EAAG,SAAA,IACjE,IAAA,EACA;AACA,IAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIC,eAAS,KAAK,CAAA;AACtC,IAAA,MAAM,SAAA,GAAYC,aAAoB,IAAI,CAAA;AAC1C,IAAA,MAAM,WAAA,GAAcA,aAAuB,IAAI,CAAA;AAC/C,IAAA,MAAM,UAAA,GAAaA,aAAsC,MAAS,CAAA;AAElE,IAAA,MAAM,QAAA,GAAWC,qCAAA,CAAoB,SAAA,EAAW,WAAA,EAAa;AAAA,MAC3D,SAAA;AAAA,MACA,MAAA;AAAA,MACA,OAAA,EAAS;AAAA,KACV,CAAA;AAED,IAAA,MAAM,IAAA,GAAOC,kBAAY,MAAM;AAC7B,MAAA,UAAA,CAAW,UAAU,UAAA,CAAW,MAAM,OAAA,CAAQ,IAAI,GAAG,KAAK,CAAA;AAAA,IAC5D,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,IAAA,MAAM,IAAA,GAAOA,kBAAY,MAAM;AAC7B,MAAA,YAAA,CAAa,WAAW,OAAO,CAAA;AAC/B,MAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,IACf,CAAA,EAAG,EAAE,CAAA;AAEL,IAAA,MAAM,OAAA,GAAUC,mBAAa,QAAA,EAAmD;AAAA,MAC9E,GAAA,EAAK,SAAA;AAAA,MACL,YAAA,EAAc,IAAA;AAAA,MACd,YAAA,EAAc,IAAA;AAAA,MACd,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ;AAAA,KACT,CAAA;AAED,IAAA,uBACEC,eAAA,CAAAC,mBAAA,EAAA,EACG,QAAA,EAAA;AAAA,MAAA,OAAA;AAAA,MACA,IAAA,mCACEC,wBAAA,EAAA,EACC,QAAA,kBAAAF,eAAA;AAAA,QAAC,KAAA;AAAA,QAAA;AAAA,UACC,GAAA,EAAK,WAAA;AAAA,UACL,0BAAA,EAAyB,EAAA;AAAA,UACzB,IAAA,EAAK,SAAA;AAAA,UACL,SAAA,EAAWG,oBAAA;AAAA,YACT,2JAAA;AAAA,YACA;AAAA,WACF;AAAA,UACA,OAAO,EAAE,IAAA,EAAM,SAAS,CAAA,EAAG,GAAA,EAAK,SAAS,CAAA,EAAE;AAAA,UAE1C,QAAA,EAAA;AAAA,YAAA,OAAA;AAAA,4BACDC,cAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBACC,SAAA,EAAWD,oBAAA;AAAA,kBACT,yDAAA;AAAA,kBACA,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,KAAK,CAAA,IACjC,yCAAA;AAAA,kBACF,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,QAAQ,CAAA,IACpC,sCAAA;AAAA,kBACF,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,MAAM,CAAA,IAClC,uCAAA;AAAA,kBACF,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,OAAO,CAAA,IACnC;AAAA;AACJ;AAAA;AACF;AAAA;AAAA,OACF,EACF;AAAA,KAAA,EAEJ,CAAA;AAAA,EAEJ;AACF;AAEA,OAAA,CAAQ,WAAA,GAAc,SAAA","file":"chunk-ISPCXIJZ.cjs","sourcesContent":["import {\n cloneElement,\n forwardRef,\n useCallback,\n useRef,\n useState,\n type ReactElement,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Portal } from \"../Portal\";\nimport { useFloatingPosition } from \"../../hooks/use-floating-position\";\nimport type { TooltipProps } from \"./Tooltip.types\";\n\nexport const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(\n function Tooltip(\n { children, content, placement = \"top\", delay = 200, offset = 8, className },\n _ref,\n ) {\n const [open, setOpen] = useState(false);\n const anchorRef = useRef<HTMLElement>(null);\n const floatingRef = useRef<HTMLDivElement>(null);\n const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n const position = useFloatingPosition(anchorRef, floatingRef, {\n placement,\n offset,\n enabled: open,\n });\n\n const show = useCallback(() => {\n timeoutRef.current = setTimeout(() => setOpen(true), delay);\n }, [delay]);\n\n const hide = useCallback(() => {\n clearTimeout(timeoutRef.current);\n setOpen(false);\n }, []);\n\n const trigger = cloneElement(children as ReactElement<Record<string, unknown>>, {\n ref: anchorRef,\n onMouseEnter: show,\n onMouseLeave: hide,\n onFocus: show,\n onBlur: hide,\n });\n\n return (\n <>\n {trigger}\n {open && (\n <Portal>\n <div\n ref={floatingRef}\n data-react-fancy-tooltip=\"\"\n role=\"tooltip\"\n className={cn(\n \"fancy-fade-in pointer-events-none fixed z-50 max-w-xs rounded-lg bg-zinc-900 px-3 py-1.5 text-sm text-white shadow-lg dark:bg-zinc-700 dark:text-zinc-100\",\n className,\n )}\n style={{ left: position.x, top: position.y }}\n >\n {content}\n <div\n className={cn(\n \"absolute h-2 w-2 rotate-45 bg-zinc-900 dark:bg-zinc-700\",\n position.placement.startsWith(\"top\") &&\n \"bottom-[-4px] left-1/2 -translate-x-1/2\",\n position.placement.startsWith(\"bottom\") &&\n \"top-[-4px] left-1/2 -translate-x-1/2\",\n position.placement.startsWith(\"left\") &&\n \"right-[-4px] top-1/2 -translate-y-1/2\",\n position.placement.startsWith(\"right\") &&\n \"left-[-4px] top-1/2 -translate-y-1/2\",\n )}\n />\n </div>\n </Portal>\n )}\n </>\n );\n },\n);\n\nTooltip.displayName = \"Tooltip\";\n"]}
|