@particle-academy/react-fancy 5.30.0 → 5.31.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.
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ var chunkOUOEQZHY_cjs = require('./chunk-OUOEQZHY.cjs');
4
+ var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ var PREVIEW_CHARS = 20;
9
+ function withSeparators(n) {
10
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
11
+ }
12
+ function endsOf(text) {
13
+ if (text.length <= PREVIEW_CHARS * 2) return text;
14
+ return `${text.slice(0, PREVIEW_CHARS)} \u2026 ${text.slice(-PREVIEW_CHARS)}`;
15
+ }
16
+ var Composer = react.forwardRef(
17
+ function Composer2({
18
+ value: controlledValue,
19
+ defaultValue = "",
20
+ onChange,
21
+ onSubmit,
22
+ placeholder = "Type a message...",
23
+ actions,
24
+ disabled = false,
25
+ className,
26
+ pasteThreshold,
27
+ heldPastes: controlledHeldPastes,
28
+ onHeldPastesChange
29
+ }, ref) {
30
+ const [value, setValue] = chunkOUOEQZHY_cjs.useControllableState(
31
+ controlledValue,
32
+ defaultValue,
33
+ onChange
34
+ );
35
+ const [heldPastes, setHeldPastes] = chunkOUOEQZHY_cjs.useControllableState(
36
+ controlledHeldPastes,
37
+ [],
38
+ onHeldPastesChange
39
+ );
40
+ const textareaRef = react.useRef(null);
41
+ const nextId = react.useRef(0);
42
+ const handlePaste = react.useCallback(
43
+ (e) => {
44
+ if (!pasteThreshold || pasteThreshold <= 0) return;
45
+ const text = e.clipboardData?.getData("text/plain") ?? "";
46
+ if (text.length < pasteThreshold) return;
47
+ e.preventDefault();
48
+ nextId.current += 1;
49
+ const id = `paste-${nextId.current}`;
50
+ setHeldPastes((current) => [...current, { id, text }]);
51
+ },
52
+ [pasteThreshold, setHeldPastes]
53
+ );
54
+ const composeOutgoing = react.useCallback(
55
+ () => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join("\n\n"),
56
+ [value, heldPastes]
57
+ );
58
+ const hasContent = value.trim().length > 0 || heldPastes.length > 0;
59
+ const handleSubmit = react.useCallback(() => {
60
+ if (!hasContent || disabled) return;
61
+ onSubmit?.(composeOutgoing());
62
+ setValue("");
63
+ setHeldPastes([]);
64
+ textareaRef.current?.focus();
65
+ }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, setHeldPastes]);
66
+ const handleKeyDown = (e) => {
67
+ if (e.key === "Enter" && !e.shiftKey) {
68
+ e.preventDefault();
69
+ handleSubmit();
70
+ }
71
+ };
72
+ const insertPaste = react.useCallback(
73
+ (paste) => {
74
+ setValue((current) => current ? `${current}
75
+
76
+ ${paste.text}` : paste.text);
77
+ setHeldPastes((current) => current.filter((p) => p.id !== paste.id));
78
+ textareaRef.current?.focus();
79
+ },
80
+ [setValue, setHeldPastes]
81
+ );
82
+ const discardPaste = react.useCallback(
83
+ (id) => setHeldPastes((current) => current.filter((p) => p.id !== id)),
84
+ [setHeldPastes]
85
+ );
86
+ return /* @__PURE__ */ jsxRuntime.jsxs(
87
+ "div",
88
+ {
89
+ "data-react-fancy-composer": "",
90
+ ref,
91
+ className: chunkZU5Q77OT_cjs.cn(
92
+ "rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
93
+ disabled && "opacity-50",
94
+ className
95
+ ),
96
+ children: [
97
+ heldPastes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
98
+ "div",
99
+ {
100
+ "data-react-fancy-composer-paste-pills": "",
101
+ className: "flex flex-wrap gap-1.5 px-3 pt-3",
102
+ children: heldPastes.map((paste) => (
103
+ // `group` + `group-hover` rather than hover state: the preview is
104
+ // presentation, and a useState for it would be one more thing an
105
+ // agent could desynchronise.
106
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "group relative inline-flex", children: [
107
+ /* @__PURE__ */ jsxRuntime.jsx(
108
+ "button",
109
+ {
110
+ type: "button",
111
+ "data-react-fancy-composer-paste-pill": paste.id,
112
+ onClick: () => insertPaste(paste),
113
+ disabled,
114
+ "aria-label": `Insert this pasted text, ${withSeparators(paste.text.length)} characters`,
115
+ "aria-describedby": `${paste.id}-preview`,
116
+ 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",
117
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
118
+ "Pasted text \xB7 ",
119
+ withSeparators(paste.text.length),
120
+ " characters"
121
+ ] })
122
+ }
123
+ ),
124
+ /* @__PURE__ */ jsxRuntime.jsx(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ "data-react-fancy-composer-paste-pill-remove": paste.id,
129
+ onClick: () => discardPaste(paste.id),
130
+ disabled,
131
+ "aria-label": "Discard this pasted text",
132
+ 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",
133
+ children: "\xD7"
134
+ }
135
+ ),
136
+ /* @__PURE__ */ jsxRuntime.jsx(
137
+ "span",
138
+ {
139
+ id: `${paste.id}-preview`,
140
+ "data-react-fancy-composer-paste-pill-preview": paste.id,
141
+ role: "tooltip",
142
+ 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",
143
+ children: endsOf(paste.text)
144
+ }
145
+ )
146
+ ] }, paste.id)
147
+ ))
148
+ }
149
+ ),
150
+ /* @__PURE__ */ jsxRuntime.jsx(
151
+ "textarea",
152
+ {
153
+ ref: textareaRef,
154
+ value,
155
+ onChange: (e) => setValue(e.target.value),
156
+ onKeyDown: handleKeyDown,
157
+ onPaste: handlePaste,
158
+ placeholder,
159
+ disabled,
160
+ rows: 3,
161
+ className: "w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400"
162
+ }
163
+ ),
164
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800", children: [
165
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: actions }),
166
+ /* @__PURE__ */ jsxRuntime.jsx(
167
+ "button",
168
+ {
169
+ type: "button",
170
+ onClick: handleSubmit,
171
+ disabled: disabled || !hasContent,
172
+ 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",
173
+ children: "Send"
174
+ }
175
+ )
176
+ ] })
177
+ ]
178
+ }
179
+ );
180
+ }
181
+ );
182
+ Composer.displayName = "Composer";
183
+
184
+ exports.Composer = Composer;
185
+ //# sourceMappingURL=chunk-7MSIREUY.cjs.map
186
+ //# sourceMappingURL=chunk-7MSIREUY.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Composer/Composer.tsx"],"names":["forwardRef","Composer","useControllableState","useRef","useCallback","jsxs","cn","jsx"],"mappings":"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","file":"chunk-7MSIREUY.cjs","sourcesContent":["import { forwardRef, useCallback, useRef } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useControllableState } from \"../../hooks/use-controllable-state\";\nimport type { ComposerProps, HeldPaste } 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 const [heldPastes, setHeldPastes] = useControllableState<HeldPaste[]>(\n controlledHeldPastes,\n [],\n onHeldPastesChange,\n );\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const nextId = useRef(0);\n\n /*\n * A large paste is held, not inserted.\n *\n * Requested by a consumer who had built and proven it against their own\n * composer: paste a transcript and the input becomes a scrolling wall, so the\n * message you were half way through writing is above the fold and the only way\n * to see what you are about to send is to scroll your own input.\n */\n const handlePaste = useCallback(\n (e: React.ClipboardEvent<HTMLTextAreaElement>) => {\n if (!pasteThreshold || pasteThreshold <= 0) return;\n\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n\n // Below the threshold, do NOTHING — not even look busy. This is the common\n // case and the easiest thing to regress.\n if (text.length < pasteThreshold) return;\n\n e.preventDefault();\n nextId.current += 1;\n const id = `paste-${nextId.current}`;\n setHeldPastes((current) => [...current, { id, text }]);\n },\n [pasteThreshold, setHeldPastes],\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 setHeldPastes([]);\n textareaRef.current?.focus();\n }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, setHeldPastes]);\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 setValue((current) => (current ? `${current}\\n\\n${paste.text}` : paste.text));\n setHeldPastes((current) => current.filter((p) => p.id !== paste.id));\n textareaRef.current?.focus();\n },\n [setValue, setHeldPastes],\n );\n\n /** Discard without inserting. Without this the only way out is in-then-delete. */\n const discardPaste = useCallback(\n (id: string) => setHeldPastes((current) => current.filter((p) => p.id !== id)),\n [setHeldPastes],\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={() => discardPaste(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"]}
@@ -0,0 +1,184 @@
1
+ import { useControllableState } from './chunk-6FQGMVF2.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
+ var PREVIEW_CHARS = 20;
7
+ function withSeparators(n) {
8
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
9
+ }
10
+ function endsOf(text) {
11
+ if (text.length <= PREVIEW_CHARS * 2) return text;
12
+ return `${text.slice(0, PREVIEW_CHARS)} \u2026 ${text.slice(-PREVIEW_CHARS)}`;
13
+ }
14
+ var Composer = forwardRef(
15
+ function Composer2({
16
+ value: controlledValue,
17
+ defaultValue = "",
18
+ onChange,
19
+ onSubmit,
20
+ placeholder = "Type a message...",
21
+ actions,
22
+ disabled = false,
23
+ className,
24
+ pasteThreshold,
25
+ heldPastes: controlledHeldPastes,
26
+ onHeldPastesChange
27
+ }, ref) {
28
+ const [value, setValue] = useControllableState(
29
+ controlledValue,
30
+ defaultValue,
31
+ onChange
32
+ );
33
+ const [heldPastes, setHeldPastes] = useControllableState(
34
+ controlledHeldPastes,
35
+ [],
36
+ onHeldPastesChange
37
+ );
38
+ const textareaRef = useRef(null);
39
+ const nextId = useRef(0);
40
+ const handlePaste = useCallback(
41
+ (e) => {
42
+ if (!pasteThreshold || pasteThreshold <= 0) return;
43
+ const text = e.clipboardData?.getData("text/plain") ?? "";
44
+ if (text.length < pasteThreshold) return;
45
+ e.preventDefault();
46
+ nextId.current += 1;
47
+ const id = `paste-${nextId.current}`;
48
+ setHeldPastes((current) => [...current, { id, text }]);
49
+ },
50
+ [pasteThreshold, setHeldPastes]
51
+ );
52
+ const composeOutgoing = useCallback(
53
+ () => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join("\n\n"),
54
+ [value, heldPastes]
55
+ );
56
+ const hasContent = value.trim().length > 0 || heldPastes.length > 0;
57
+ const handleSubmit = useCallback(() => {
58
+ if (!hasContent || disabled) return;
59
+ onSubmit?.(composeOutgoing());
60
+ setValue("");
61
+ setHeldPastes([]);
62
+ textareaRef.current?.focus();
63
+ }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, setHeldPastes]);
64
+ const handleKeyDown = (e) => {
65
+ if (e.key === "Enter" && !e.shiftKey) {
66
+ e.preventDefault();
67
+ handleSubmit();
68
+ }
69
+ };
70
+ const insertPaste = useCallback(
71
+ (paste) => {
72
+ setValue((current) => current ? `${current}
73
+
74
+ ${paste.text}` : paste.text);
75
+ setHeldPastes((current) => current.filter((p) => p.id !== paste.id));
76
+ textareaRef.current?.focus();
77
+ },
78
+ [setValue, setHeldPastes]
79
+ );
80
+ const discardPaste = useCallback(
81
+ (id) => setHeldPastes((current) => current.filter((p) => p.id !== id)),
82
+ [setHeldPastes]
83
+ );
84
+ return /* @__PURE__ */ jsxs(
85
+ "div",
86
+ {
87
+ "data-react-fancy-composer": "",
88
+ ref,
89
+ className: cn(
90
+ "rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
91
+ disabled && "opacity-50",
92
+ className
93
+ ),
94
+ children: [
95
+ heldPastes.length > 0 && /* @__PURE__ */ jsx(
96
+ "div",
97
+ {
98
+ "data-react-fancy-composer-paste-pills": "",
99
+ className: "flex flex-wrap gap-1.5 px-3 pt-3",
100
+ children: heldPastes.map((paste) => (
101
+ // `group` + `group-hover` rather than hover state: the preview is
102
+ // presentation, and a useState for it would be one more thing an
103
+ // agent could desynchronise.
104
+ /* @__PURE__ */ jsxs("span", { className: "group relative inline-flex", children: [
105
+ /* @__PURE__ */ jsx(
106
+ "button",
107
+ {
108
+ type: "button",
109
+ "data-react-fancy-composer-paste-pill": paste.id,
110
+ onClick: () => insertPaste(paste),
111
+ disabled,
112
+ "aria-label": `Insert this pasted text, ${withSeparators(paste.text.length)} characters`,
113
+ "aria-describedby": `${paste.id}-preview`,
114
+ 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",
115
+ children: /* @__PURE__ */ jsxs("span", { children: [
116
+ "Pasted text \xB7 ",
117
+ withSeparators(paste.text.length),
118
+ " characters"
119
+ ] })
120
+ }
121
+ ),
122
+ /* @__PURE__ */ jsx(
123
+ "button",
124
+ {
125
+ type: "button",
126
+ "data-react-fancy-composer-paste-pill-remove": paste.id,
127
+ onClick: () => discardPaste(paste.id),
128
+ disabled,
129
+ "aria-label": "Discard this pasted text",
130
+ 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",
131
+ children: "\xD7"
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsx(
135
+ "span",
136
+ {
137
+ id: `${paste.id}-preview`,
138
+ "data-react-fancy-composer-paste-pill-preview": paste.id,
139
+ role: "tooltip",
140
+ 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",
141
+ children: endsOf(paste.text)
142
+ }
143
+ )
144
+ ] }, paste.id)
145
+ ))
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsx(
149
+ "textarea",
150
+ {
151
+ ref: textareaRef,
152
+ value,
153
+ onChange: (e) => setValue(e.target.value),
154
+ onKeyDown: handleKeyDown,
155
+ onPaste: handlePaste,
156
+ placeholder,
157
+ disabled,
158
+ rows: 3,
159
+ className: "w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400"
160
+ }
161
+ ),
162
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800", children: [
163
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: actions }),
164
+ /* @__PURE__ */ jsx(
165
+ "button",
166
+ {
167
+ type: "button",
168
+ onClick: handleSubmit,
169
+ disabled: disabled || !hasContent,
170
+ 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",
171
+ children: "Send"
172
+ }
173
+ )
174
+ ] })
175
+ ]
176
+ }
177
+ );
178
+ }
179
+ );
180
+ Composer.displayName = "Composer";
181
+
182
+ export { Composer };
183
+ //# sourceMappingURL=chunk-NH5XJ6VD.js.map
184
+ //# sourceMappingURL=chunk-NH5XJ6VD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Composer/Composer.tsx"],"names":["Composer"],"mappings":"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","file":"chunk-NH5XJ6VD.js","sourcesContent":["import { forwardRef, useCallback, useRef } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useControllableState } from \"../../hooks/use-controllable-state\";\nimport type { ComposerProps, HeldPaste } 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 const [heldPastes, setHeldPastes] = useControllableState<HeldPaste[]>(\n controlledHeldPastes,\n [],\n onHeldPastesChange,\n );\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const nextId = useRef(0);\n\n /*\n * A large paste is held, not inserted.\n *\n * Requested by a consumer who had built and proven it against their own\n * composer: paste a transcript and the input becomes a scrolling wall, so the\n * message you were half way through writing is above the fold and the only way\n * to see what you are about to send is to scroll your own input.\n */\n const handlePaste = useCallback(\n (e: React.ClipboardEvent<HTMLTextAreaElement>) => {\n if (!pasteThreshold || pasteThreshold <= 0) return;\n\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n\n // Below the threshold, do NOTHING — not even look busy. This is the common\n // case and the easiest thing to regress.\n if (text.length < pasteThreshold) return;\n\n e.preventDefault();\n nextId.current += 1;\n const id = `paste-${nextId.current}`;\n setHeldPastes((current) => [...current, { id, text }]);\n },\n [pasteThreshold, setHeldPastes],\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 setHeldPastes([]);\n textareaRef.current?.focus();\n }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, setHeldPastes]);\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 setValue((current) => (current ? `${current}\\n\\n${paste.text}` : paste.text));\n setHeldPastes((current) => current.filter((p) => p.id !== paste.id));\n textareaRef.current?.focus();\n },\n [setValue, setHeldPastes],\n );\n\n /** Discard without inserting. Without this the only way out is in-then-delete. */\n const discardPaste = useCallback(\n (id: string) => setHeldPastes((current) => current.filter((p) => p.id !== id)),\n [setHeldPastes],\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={() => discardPaste(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/composer.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkOGEE23SP_cjs = require('./chunk-OGEE23SP.cjs');
3
+ var chunk7MSIREUY_cjs = require('./chunk-7MSIREUY.cjs');
4
4
  require('./chunk-OUOEQZHY.cjs');
5
5
  require('./chunk-ZU5Q77OT.cjs');
6
6
 
@@ -8,7 +8,7 @@ require('./chunk-ZU5Q77OT.cjs');
8
8
 
9
9
  Object.defineProperty(exports, "Composer", {
10
10
  enumerable: true,
11
- get: function () { return chunkOGEE23SP_cjs.Composer; }
11
+ get: function () { return chunk7MSIREUY_cjs.Composer; }
12
12
  });
13
13
  //# sourceMappingURL=composer.cjs.map
14
14
  //# sourceMappingURL=composer.cjs.map
@@ -1,6 +1,16 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
+ /**
5
+ * A paste held beside the composer rather than inserted into it.
6
+ *
7
+ * `text` is the whole thing. The pill shows a count and the two ends; nothing
8
+ * truncates the content itself, because the point is to send it.
9
+ */
10
+ interface HeldPaste {
11
+ id: string;
12
+ text: string;
13
+ }
4
14
  interface ComposerProps {
5
15
  value?: string;
6
16
  defaultValue?: string;
@@ -10,8 +20,31 @@ interface ComposerProps {
10
20
  actions?: ReactNode;
11
21
  disabled?: boolean;
12
22
  className?: string;
23
+ /**
24
+ * Hold a paste of at least this many characters beside the composer as a pill,
25
+ * instead of inserting it. Try 500 — about a long paragraph.
26
+ *
27
+ * **Omit it and paste behaves exactly as it always has.** Opt-in on purpose:
28
+ * paste is a fundamental interaction and this library is past 1.0, so a minor
29
+ * release must not quietly change what pasting does for every consumer. There is
30
+ * no default threshold for the same reason.
31
+ *
32
+ * Below the threshold nothing changes at all — no prevented event, no pill, no
33
+ * ceremony for an address or a name.
34
+ */
35
+ pasteThreshold?: number;
36
+ /**
37
+ * The held pastes, if you want to own them.
38
+ *
39
+ * Controllable rather than internal because the component contract forbids
40
+ * internal-only state for anything an agent might need to read or write — an
41
+ * agent driving this surface has to be able to see that a paste is attached, and
42
+ * to attach one itself.
43
+ */
44
+ heldPastes?: HeldPaste[];
45
+ onHeldPastesChange?: (pastes: HeldPaste[]) => void;
13
46
  }
14
47
 
15
48
  declare const Composer: react.ForwardRefExoticComponent<ComposerProps & react.RefAttributes<HTMLDivElement>>;
16
49
 
17
- export { Composer, type ComposerProps };
50
+ export { Composer, type ComposerProps, type HeldPaste };
@@ -1,6 +1,16 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
+ /**
5
+ * A paste held beside the composer rather than inserted into it.
6
+ *
7
+ * `text` is the whole thing. The pill shows a count and the two ends; nothing
8
+ * truncates the content itself, because the point is to send it.
9
+ */
10
+ interface HeldPaste {
11
+ id: string;
12
+ text: string;
13
+ }
4
14
  interface ComposerProps {
5
15
  value?: string;
6
16
  defaultValue?: string;
@@ -10,8 +20,31 @@ interface ComposerProps {
10
20
  actions?: ReactNode;
11
21
  disabled?: boolean;
12
22
  className?: string;
23
+ /**
24
+ * Hold a paste of at least this many characters beside the composer as a pill,
25
+ * instead of inserting it. Try 500 — about a long paragraph.
26
+ *
27
+ * **Omit it and paste behaves exactly as it always has.** Opt-in on purpose:
28
+ * paste is a fundamental interaction and this library is past 1.0, so a minor
29
+ * release must not quietly change what pasting does for every consumer. There is
30
+ * no default threshold for the same reason.
31
+ *
32
+ * Below the threshold nothing changes at all — no prevented event, no pill, no
33
+ * ceremony for an address or a name.
34
+ */
35
+ pasteThreshold?: number;
36
+ /**
37
+ * The held pastes, if you want to own them.
38
+ *
39
+ * Controllable rather than internal because the component contract forbids
40
+ * internal-only state for anything an agent might need to read or write — an
41
+ * agent driving this surface has to be able to see that a paste is attached, and
42
+ * to attach one itself.
43
+ */
44
+ heldPastes?: HeldPaste[];
45
+ onHeldPastesChange?: (pastes: HeldPaste[]) => void;
13
46
  }
14
47
 
15
48
  declare const Composer: react.ForwardRefExoticComponent<ComposerProps & react.RefAttributes<HTMLDivElement>>;
16
49
 
17
- export { Composer, type ComposerProps };
50
+ export { Composer, type ComposerProps, type HeldPaste };
package/dist/composer.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Composer } from './chunk-VVL5LMLW.js';
1
+ export { Composer } from './chunk-NH5XJ6VD.js';
2
2
  import './chunk-6FQGMVF2.js';
3
3
  import './chunk-MMW2JGKZ.js';
4
4
  //# sourceMappingURL=composer.js.map
package/dist/index.cjs CHANGED
@@ -85,7 +85,7 @@ var chunkJM2H642B_cjs = require('./chunk-JM2H642B.cjs');
85
85
  var chunkTZHK3LEW_cjs = require('./chunk-TZHK3LEW.cjs');
86
86
  var chunkTXTRTTG7_cjs = require('./chunk-TXTRTTG7.cjs');
87
87
  var chunk3WGRVDQR_cjs = require('./chunk-3WGRVDQR.cjs');
88
- var chunkOGEE23SP_cjs = require('./chunk-OGEE23SP.cjs');
88
+ var chunk7MSIREUY_cjs = require('./chunk-7MSIREUY.cjs');
89
89
  var chunkFQSSVOVQ_cjs = require('./chunk-FQSSVOVQ.cjs');
90
90
  require('./chunk-S3TRSECD.cjs');
91
91
  var chunkND5D7GJB_cjs = require('./chunk-ND5D7GJB.cjs');
@@ -595,7 +595,7 @@ Object.defineProperty(exports, "useAnimation", {
595
595
  });
596
596
  Object.defineProperty(exports, "Composer", {
597
597
  enumerable: true,
598
- get: function () { return chunkOGEE23SP_cjs.Composer; }
598
+ get: function () { return chunk7MSIREUY_cjs.Composer; }
599
599
  });
600
600
  Object.defineProperty(exports, "Container", {
601
601
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -64,7 +64,7 @@ export { OtpInput, OtpInputProps } from './otp-input.cjs';
64
64
  export { FileUpload, FileUploadContextValue, FileUploadDropzoneProps, FileUploadListProps, FileUploadProps, useFileUpload } from './file-upload.cjs';
65
65
  export { TimePicker, TimePickerProps } from './time-picker.cjs';
66
66
  export { Calendar, CalendarMode, CalendarProps, DateRange } from './calendar.cjs';
67
- export { Composer, ComposerProps } from './composer.cjs';
67
+ export { Composer, ComposerProps, HeldPaste } from './composer.cjs';
68
68
  export { Chart, ChartAreaProps, ChartBarData, ChartBarProps, ChartCommonProps, ChartDonutData, ChartDonutProps, ChartHorizontalBarProps, ChartLineProps, ChartPieData, ChartPieProps, ChartSeries, ChartSparklineProps, ChartStackedBarProps } from './chart.cjs';
69
69
  export { Editor, EditorAction, EditorContentProps, EditorContextValue, EditorProps, EditorSourceToggleProps, EditorToolbarProps, useEditor } from './editor.cjs';
70
70
  export { CodeView, CodeViewProps } from './code-view.cjs';
package/dist/index.d.ts CHANGED
@@ -64,7 +64,7 @@ export { OtpInput, OtpInputProps } from './otp-input.js';
64
64
  export { FileUpload, FileUploadContextValue, FileUploadDropzoneProps, FileUploadListProps, FileUploadProps, useFileUpload } from './file-upload.js';
65
65
  export { TimePicker, TimePickerProps } from './time-picker.js';
66
66
  export { Calendar, CalendarMode, CalendarProps, DateRange } from './calendar.js';
67
- export { Composer, ComposerProps } from './composer.js';
67
+ export { Composer, ComposerProps, HeldPaste } from './composer.js';
68
68
  export { Chart, ChartAreaProps, ChartBarData, ChartBarProps, ChartCommonProps, ChartDonutData, ChartDonutProps, ChartHorizontalBarProps, ChartLineProps, ChartPieData, ChartPieProps, ChartSeries, ChartSparklineProps, ChartStackedBarProps } from './chart.js';
69
69
  export { Editor, EditorAction, EditorContentProps, EditorContextValue, EditorProps, EditorSourceToggleProps, EditorToolbarProps, useEditor } from './editor.js';
70
70
  export { CodeView, CodeViewProps } from './code-view.js';
package/dist/index.js CHANGED
@@ -83,7 +83,7 @@ export { ColorPicker } from './chunk-6PF5ENWM.js';
83
83
  export { Command, useCommand } from './chunk-YCV43A7D.js';
84
84
  export { useFocusTrap } from './chunk-5U2DZ7AV.js';
85
85
  export { useAnimation } from './chunk-CHADHN6M.js';
86
- export { Composer } from './chunk-VVL5LMLW.js';
86
+ export { Composer } from './chunk-NH5XJ6VD.js';
87
87
  export { Container } from './chunk-VKMPVFQK.js';
88
88
  import './chunk-AUTW5O4W.js';
89
89
  export { ContentRenderer, registerExtension, registerExtensions } from './chunk-25ZOOUFG.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/react-fancy",
3
- "version": "5.30.0",
3
+ "version": "5.31.0",
4
4
  "description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
5
5
  "repository": {
6
6
  "type": "git",
@@ -928,7 +928,8 @@
928
928
  "files": [
929
929
  "dist",
930
930
  "docs",
931
- "README.md"
931
+ "README.md",
932
+ "CHANGELOG.md"
932
933
  ],
933
934
  "scripts": {
934
935
  "build": "node --max-old-space-size=8192 ./node_modules/tsup/dist/cli-default.js",
@@ -979,7 +980,9 @@
979
980
  "**/*.css"
980
981
  ],
981
982
  "overrides": {
982
- "esbuild": "^0.28.1"
983
+ "esbuild": "^0.28.1",
984
+ "brace-expansion": "^5.0.12",
985
+ "undici": "^7.29.1"
983
986
  },
984
987
  "engines": {
985
988
  "node": ">=22"