@particle-academy/react-fancy 5.32.0 → 5.33.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 CHANGED
@@ -11,6 +11,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [5.33.0] - 2026-10-04
15
+
16
+ ### Fixed
17
+
18
+ - **`FileUpload` with `multiple={false}` could never change its file.** Choosing
19
+ a second file kept the first and discarded the new one in silence, so a
20
+ single-file field was write-once; the only way out was removing the file or
21
+ remounting the component. Reported by the YouGene estate, who were working
22
+ around it with a `key` remount.
23
+
24
+ **What changed, precisely:** `multiple={false}` now REPLACES. `maxFiles` is
25
+ unchanged and still refuses the overflow. The two used to share one rule — a
26
+ cap of one, first-wins — on the reasoning that they must not disagree about
27
+ what a single-file upload means. They are different statements and are now
28
+ allowed to differ: `maxFiles={n}` is a quota (drop five files into a
29
+ three-file field and you keep three, not the last three), while
30
+ `multiple={false}` is a field shape whose analogue is a radio group, where
31
+ choosing again moves the selection.
32
+
33
+ **Dropping several files at once onto a single-file field still keeps the
34
+ FIRST**, exactly as before. Only the across-gestures case changed. If you
35
+ depended on the old write-once behaviour — which we do not think anyone could
36
+ have wanted — pass `maxFiles={1}` instead of `multiple={false}`.
37
+
38
+ - **`FileUpload` ignored the same file being chosen twice.** The hidden
39
+ `<input type="file">` was never reset, and an input still holding a path fires
40
+ no `change` event when the user picks that same path again. So "remove it,
41
+ then choose it again" and "retry the import that just failed" were dead
42
+ gestures — the component was never asked, so it never rejected anything. The
43
+ input is now cleared after each change.
44
+
45
+
14
46
  ## [5.32.0] - 2026-10-02
15
47
 
16
48
  ### Added
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Particle Academy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -214,5 +214,5 @@ Composer.displayName = "Composer";
214
214
 
215
215
  exports.Composer = Composer;
216
216
  exports.usePastePills = usePastePills;
217
- //# sourceMappingURL=chunk-WRQBV7GU.cjs.map
218
- //# sourceMappingURL=chunk-WRQBV7GU.cjs.map
217
+ //# sourceMappingURL=chunk-23FTDEGR.cjs.map
218
+ //# sourceMappingURL=chunk-23FTDEGR.cjs.map
@@ -0,0 +1 @@
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":";;;;;;;AAoHO,SAAS,aAAA,CAAc;AAAA,EAC5B,SAAA;AAAA,EACA,UAAA,EAAY,oBAAA;AAAA,EACZ;AACF,CAAA,GAA0B,EAAC,EAAwB;AACjD,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIA,sCAAA;AAAA,IAClC,oBAAA;AAAA,IACA,EAAC;AAAA,IACD;AAAA,GACF;AAUA,EAAA,MAAM,MAAA,GAASC,aAAO,CAAC,CAAA;AAEvB,EAAA,MAAM,WAAA,GAAcC,iBAAA;AAAA,IAClB,CAAC,KAAA,KAAmC;AAClC,MAAA,IAAI,CAAC,SAAA,IAAa,SAAA,IAAa,CAAA,EAAG,OAAO,KAAA;AAEzC,MAAA,MAAM,IAAA,GAAO,KAAA,CAAM,aAAA,EAAe,OAAA,CAAQ,YAAY,CAAA,IAAK,EAAA;AAK3D,MAAA,IAAI,IAAA,CAAK,MAAA,GAAS,SAAA,EAAW,OAAO,KAAA;AAEpC,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,MAAA,CAAO,OAAA,IAAW,CAAA;AAClB,MAAA,MAAM,EAAA,GAAK,CAAA,MAAA,EAAS,MAAA,CAAO,OAAO,CAAA,CAAA;AAClC,MAAA,aAAA,CAAc,CAAC,YAAY,CAAC,GAAG,SAAS,EAAE,EAAA,EAAI,IAAA,EAAM,CAAC,CAAA;AAErD,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAAA,IACA,CAAC,WAAW,aAAa;AAAA,GAC3B;AAEA,EAAA,MAAM,MAAA,GAASA,iBAAA;AAAA,IACb,CAAC,EAAA,KAAmC;AAClC,MAAA,MAAM,QAAQ,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,EAAE,CAAA;AAEhD,MAAA,IAAI,KAAA,KAAU,QAAW,OAAO,MAAA;AAEhC,MAAA,aAAA,CAAc,CAAC,YAAY,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAC,CAAA;AAE7D,MAAA,OAAO,KAAA,CAAM,IAAA;AAAA,IACf,CAAA;AAAA,IACA,CAAC,YAAY,aAAa;AAAA,GAC5B;AAEA,EAAA,MAAM,MAAA,GAASA,iBAAA;AAAA,IACb,CAAC,EAAA,KAAe,aAAA,CAAc,CAAC,OAAA,KAAY,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAC,CAAA;AAAA,IAC7E,CAAC,aAAa;AAAA,GAChB;AAEA,EAAA,MAAM,KAAA,GAAQA,kBAAY,MAAM,aAAA,CAAc,EAAE,CAAA,EAAG,CAAC,aAAa,CAAC,CAAA;AAElE,EAAA,OAAO,EAAE,UAAA,EAAY,WAAA,EAAa,MAAA,EAAQ,QAAQ,KAAA,EAAM;AAC1D;AC5KA,IAAM,aAAA,GAAgB,EAAA;AAUtB,SAAS,eAAe,CAAA,EAAmB;AACzC,EAAA,OAAO,MAAA,CAAO,CAAC,CAAA,CAAE,OAAA,CAAQ,yBAAyB,GAAG,CAAA;AACvD;AAGA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,aAAA,GAAgB,CAAA,EAAG,OAAO,IAAA;AAE7C,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,aAAa,CAAC,CAAA,QAAA,EAAM,IAAA,CAAK,KAAA,CAAM,CAAC,aAAa,CAAC,CAAA,CAAA;AACxE;AAEO,IAAM,QAAA,GAAWC,gBAAA;AAAA,EACtB,SAASC,SAAAA,CACP;AAAA,IACE,KAAA,EAAO,eAAA;AAAA,IACP,YAAA,GAAe,EAAA;AAAA,IACf,QAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA,GAAc,mBAAA;AAAA,IACd,OAAA;AAAA,IACA,QAAA,GAAW,KAAA;AAAA,IACX,SAAA;AAAA,IACA,cAAA;AAAA,IACA,UAAA,EAAY,oBAAA;AAAA,IACZ;AAAA,KAEF,GAAA,EACA;AACA,IAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIJ,sCAAA;AAAA,MACxB,eAAA;AAAA,MACA,YAAA;AAAA,MACA;AAAA,KACF;AAWA,IAAA,MAAM,QAAQ,aAAA,CAAc;AAAA,MAC1B,SAAA,EAAW,cAAA;AAAA,MACX,UAAA,EAAY,oBAAA;AAAA,MACZ;AAAA,KACD,CAAA;AACD,IAAA,MAAM,aAAa,KAAA,CAAM,UAAA;AACzB,IAAA,MAAM,WAAA,GAAcC,aAA4B,IAAI,CAAA;AAEpD,IAAA,MAAM,WAAA,GAAcC,iBAAAA;AAAA,MAClB,CAAC,CAAA,KAAiD;AAGhD,QAAA,KAAA,CAAM,YAAY,CAAC,CAAA;AAAA,MACrB,CAAA;AAAA,MACA,CAAC,KAAK;AAAA,KACR;AAGA,IAAA,MAAM,eAAA,GAAkBA,iBAAAA;AAAA,MACtB,MAAM,CAAC,KAAA,EAAO,GAAG,UAAA,CAAW,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,MAAM,CAAA;AAAA,MAC3E,CAAC,OAAO,UAAU;AAAA,KACpB;AAEA,IAAA,MAAM,aAAa,KAAA,CAAM,IAAA,GAAO,MAAA,GAAS,CAAA,IAAK,WAAW,MAAA,GAAS,CAAA;AAElE,IAAA,MAAM,YAAA,GAAeA,kBAAY,MAAM;AACrC,MAAA,IAAI,CAAC,cAAc,QAAA,EAAU;AAY7B,MAAA,QAAA,GAAW,iBAAiB,CAAA;AAC5B,MAAA,QAAA,CAAS,EAAE,CAAA;AACX,MAAA,KAAA,CAAM,KAAA,EAAM;AACZ,MAAA,WAAA,CAAY,SAAS,KAAA,EAAM;AAAA,IAC7B,CAAA,EAAG,CAAC,UAAA,EAAY,QAAA,EAAU,UAAU,eAAA,EAAiB,QAAA,EAAU,KAAK,CAAC,CAAA;AAErE,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAgD;AACrE,MAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,IAAW,CAAC,EAAE,QAAA,EAAU;AACpC,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,YAAA,EAAa;AAAA,MACf;AAAA,IACF,CAAA;AAGA,IAAA,MAAM,WAAA,GAAcA,iBAAAA;AAAA,MAClB,CAAC,KAAA,KAAqB;AAGpB,QAAA,MAAM,IAAA,GAAO,KAAA,CAAM,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA;AAElC,QAAA,IAAI,SAAS,MAAA,EAAW;AAExB,QAAA,QAAA,CAAS,CAAC,OAAA,KAAa,OAAA,GAAU,CAAA,EAAG,OAAO;;AAAA,EAAO,IAAI,KAAK,IAAK,CAAA;AAChE,QAAA,WAAA,CAAY,SAAS,KAAA,EAAM;AAAA,MAC7B,CAAA;AAAA,MACA,CAAC,OAAO,QAAQ;AAAA,KAClB;AAEA,IAAA,uBACEG,eAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,2BAAA,EAA0B,EAAA;AAAA,QAC1B,GAAA;AAAA,QACA,SAAA,EAAWC,oBAAA;AAAA,UACT,kFAAA;AAAA,UACA,QAAA,IAAY,YAAA;AAAA,UACZ;AAAA,SACF;AAAA,QAEC,QAAA,EAAA;AAAA,UAAA,UAAA,CAAW,SAAS,CAAA,oBACnBC,cAAA;AAAA,YAAC,KAAA;AAAA,YAAA;AAAA,cACC,uCAAA,EAAsC,EAAA;AAAA,cACtC,SAAA,EAAU,kCAAA;AAAA,cAET,QAAA,EAAA,UAAA,CAAW,IAAI,CAAC,KAAA;AAAA;AAAA;AAAA;AAAA,gCAIfF,eAAA,CAAC,MAAA,EAAA,EAAoB,SAAA,EAAU,4BAAA,EAC7B,QAAA,EAAA;AAAA,kCAAAE,cAAA;AAAA,oBAAC,QAAA;AAAA,oBAAA;AAAA,sBACC,IAAA,EAAK,QAAA;AAAA,sBACL,wCAAsC,KAAA,CAAM,EAAA;AAAA,sBAC5C,OAAA,EAAS,MAAM,WAAA,CAAY,KAAK,CAAA;AAAA,sBAChC,QAAA;AAAA,sBAMA,cAAY,CAAA,yBAAA,EAA4B,cAAA,CAAe,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC,CAAA,WAAA,CAAA;AAAA,sBACzE,kBAAA,EAAkB,CAAA,EAAG,KAAA,CAAM,EAAE,CAAA,QAAA,CAAA;AAAA,sBAI7B,SAAA,EAAU,4OAAA;AAAA,sBAEV,0CAAC,MAAA,EAAA,EAAK,QAAA,EAAA;AAAA,wBAAA,mBAAA;AAAA,wBACW,cAAA,CAAe,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAAA,wBAAE;AAAA,uBAAA,EACnD;AAAA;AAAA,mBACF;AAAA,kCAEAA,cAAA;AAAA,oBAAC,QAAA;AAAA,oBAAA;AAAA,sBACC,IAAA,EAAK,QAAA;AAAA,sBACL,+CAA6C,KAAA,CAAM,EAAA;AAAA,sBACnD,OAAA,EAAS,MAAM,KAAA,CAAM,MAAA,CAAO,MAAM,EAAE,CAAA;AAAA,sBACpC,QAAA;AAAA,sBACA,YAAA,EAAW,0BAAA;AAAA,sBACX,SAAA,EAAU,kJAAA;AAAA,sBACX,QAAA,EAAA;AAAA;AAAA,mBAED;AAAA,kCAEAA,cAAA;AAAA,oBAAC,MAAA;AAAA,oBAAA;AAAA,sBACC,EAAA,EAAI,CAAA,EAAG,KAAA,CAAM,EAAE,CAAA,QAAA,CAAA;AAAA,sBACf,gDAA8C,KAAA,CAAM,EAAA;AAAA,sBACpD,IAAA,EAAK,SAAA;AAAA,sBACL,SAAA,EAAU,mNAAA;AAAA,sBAET,QAAA,EAAA,MAAA,CAAO,MAAM,IAAI;AAAA;AAAA;AACpB,iBAAA,EAAA,EAzCS,MAAM,EA0CjB;AAAA,eACD;AAAA;AAAA,WACH;AAAA,0BAGFA,cAAA;AAAA,YAAC,UAAA;AAAA,YAAA;AAAA,cACC,GAAA,EAAK,WAAA;AAAA,cACL,KAAA;AAAA,cACA,UAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,OAAO,KAAK,CAAA;AAAA,cACxC,SAAA,EAAW,aAAA;AAAA,cACX,OAAA,EAAS,WAAA;AAAA,cACT,WAAA;AAAA,cACA,QAAA;AAAA,cACA,IAAA,EAAM,CAAA;AAAA,cACN,SAAA,EAAU;AAAA;AAAA,WACZ;AAAA,0BACAF,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2FAAA,EACb,QAAA,EAAA;AAAA,4BAAAE,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,yBAAA,EAA2B,QAAA,EAAA,OAAA,EAAQ,CAAA;AAAA,4BAClDA,cAAA;AAAA,cAAC,QAAA;AAAA,cAAA;AAAA,gBACC,IAAA,EAAK,QAAA;AAAA,gBACL,OAAA,EAAS,YAAA;AAAA,gBACT,QAAA,EAAU,YAAY,CAAC,UAAA;AAAA,gBACvB,SAAA,EAAU,mLAAA;AAAA,gBACX,QAAA,EAAA;AAAA;AAAA;AAED,WAAA,EACF;AAAA;AAAA;AAAA,KACF;AAAA,EAEJ;AACF;AAEA,QAAA,CAAS,WAAA,GAAc,UAAA","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"]}
@@ -211,5 +211,5 @@ ${text}` : text);
211
211
  Composer.displayName = "Composer";
212
212
 
213
213
  export { Composer, usePastePills };
214
- //# sourceMappingURL=chunk-WHOVPDC5.js.map
215
- //# sourceMappingURL=chunk-WHOVPDC5.js.map
214
+ //# sourceMappingURL=chunk-2XAVAPDA.js.map
215
+ //# sourceMappingURL=chunk-2XAVAPDA.js.map
@@ -0,0 +1 @@
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"]}
@@ -57,7 +57,10 @@ function FileUploadDropzone({
57
57
  ref: inputRef,
58
58
  type: "file",
59
59
  className: "hidden",
60
- onChange: (e) => e.target.files && addFiles(e.target.files),
60
+ onChange: (e) => {
61
+ if (e.target.files) addFiles(e.target.files);
62
+ e.target.value = "";
63
+ },
61
64
  accept,
62
65
  multiple,
63
66
  disabled
@@ -174,9 +177,15 @@ function FileUploadRoot({
174
177
  if (maxSize) {
175
178
  toAdd = toAdd.filter((f) => f.size <= maxSize);
176
179
  }
177
- const cap = multiple === false ? 1 : maxFiles;
180
+ if (multiple === false) {
181
+ const replacement = toAdd.slice(0, 1);
182
+ const next = replacement.length > 0 ? replacement : files;
183
+ if (value === void 0) setInternalFiles(next);
184
+ onChange?.(next);
185
+ return;
186
+ }
178
187
  const updated = [...files, ...toAdd];
179
- const limited = cap ? updated.slice(0, cap) : updated;
188
+ const limited = maxFiles ? updated.slice(0, maxFiles) : updated;
180
189
  if (value === void 0) setInternalFiles(limited);
181
190
  onChange?.(limited);
182
191
  },
@@ -203,5 +212,5 @@ var FileUpload = Object.assign(FileUploadRoot, {
203
212
 
204
213
  exports.FileUpload = FileUpload;
205
214
  exports.useFileUpload = useFileUpload;
206
- //# sourceMappingURL=chunk-LAQGBLIU.cjs.map
207
- //# sourceMappingURL=chunk-LAQGBLIU.cjs.map
215
+ //# sourceMappingURL=chunk-VKXNCYLE.cjs.map
216
+ //# sourceMappingURL=chunk-VKXNCYLE.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/FileUpload/FileUpload.context.ts","../src/components/FileUpload/FileUploadDropzone.tsx","../src/components/FileUpload/FileUploadList.tsx","../src/components/FileUpload/matchesAccept.ts","../src/components/FileUpload/FileUpload.tsx"],"names":["createContext","useContext","useState","useRef","useCallback","jsxs","cn","jsx","Fragment","Upload","useMemo","useEffect","File","X"],"mappings":"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","file":"chunk-VKXNCYLE.cjs","sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { FileUploadContextValue } from \"./FileUpload.types\";\n\nexport const FileUploadContext =\n createContext<FileUploadContextValue | null>(null);\n\nexport function useFileUpload(): FileUploadContextValue {\n const ctx = useContext(FileUploadContext);\n if (!ctx) {\n throw new Error(\n \"FileUpload compound components must be used within <FileUpload>\",\n );\n }\n return ctx;\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { Upload } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadDropzoneProps } from \"./FileUpload.types\";\n\nexport function FileUploadDropzone({\n children,\n className,\n}: FileUploadDropzoneProps) {\n const { addFiles, disabled, accept, multiple } = useFileUpload();\n const [dragOver, setDragOver] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(false);\n if (disabled) return;\n addFiles(e.dataTransfer.files);\n },\n [addFiles, disabled],\n );\n\n const handleDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(true);\n }, []);\n\n const handleDragLeave = useCallback(() => setDragOver(false), []);\n\n return (\n <div\n data-react-fancy-file-upload-dropzone=\"\"\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onClick={() => !disabled && inputRef.current?.click()}\n className={cn(\n \"flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors\",\n dragOver\n ? \"border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-950\"\n : \"border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-500\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n >\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files) addFiles(e.target.files);\n\n // Reset, or choosing the SAME file again fires no `change` event at\n // all and the gesture is simply dead — \"remove it, then re-add it\"\n // and \"retry the import that just failed\" both stop working. The\n // component never sees those files to reject them; it is not asked.\n e.target.value = \"\";\n }}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n />\n {children ?? (\n <>\n <Upload size={32} strokeWidth={1.5} className=\"mb-2 text-zinc-400\" />\n <p className=\"text-sm text-zinc-500\">\n Drop files here or click to browse\n </p>\n </>\n )}\n </div>\n );\n}\n\nFileUploadDropzone.displayName = \"FileUploadDropzone\";\n","import { useEffect, useMemo } from \"react\";\nimport { File, X } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadListProps } from \"./FileUpload.types\";\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction getExtension(name: string): string {\n const parts = name.split(\".\");\n return parts.length > 1 ? parts.pop()!.toUpperCase() : \"\";\n}\n\nfunction ThumbnailItem({ file, index, onRemove }: { file: File; index: number; onRemove: (i: number) => void }) {\n const isImage = file.type.startsWith(\"image/\");\n const objectUrl = useMemo(() => (isImage ? URL.createObjectURL(file) : null), [file, isImage]);\n\n useEffect(() => {\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [objectUrl]);\n\n return (\n <div className=\"group relative flex w-20 flex-col items-center gap-1\">\n <div className=\"relative h-20 w-20 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800\">\n {isImage && objectUrl ? (\n <img src={objectUrl} alt={file.name} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-1\">\n <File size={20} className=\"text-zinc-400\" />\n <span className=\"text-[10px] font-medium text-zinc-500\">{getExtension(file.name)}</span>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => onRemove(index)}\n className=\"absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-black/80 group-hover:opacity-100\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={10} />\n </button>\n </div>\n <span className=\"w-full truncate text-center text-xs text-zinc-500\">{file.name}</span>\n </div>\n );\n}\n\nexport function FileUploadList({ thumbnail, className }: FileUploadListProps) {\n const { files, removeFile } = useFileUpload();\n\n if (files.length === 0) return null;\n\n if (thumbnail) {\n return (\n <div data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 flex flex-wrap gap-3\", className)}>\n {files.map((file, i) => (\n <ThumbnailItem key={`${file.name}-${i}`} file={file} index={i} onRemove={removeFile} />\n ))}\n </div>\n );\n }\n\n return (\n <ul data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 space-y-2\", className)}>\n {files.map((file, i) => (\n <li\n key={`${file.name}-${i}`}\n className=\"flex items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700\"\n >\n <File size={16} className=\"shrink-0 text-zinc-400\" />\n <span className=\"flex-1 truncate\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-zinc-400\">\n {formatSize(file.size)}\n </span>\n <button\n type=\"button\"\n onClick={() => removeFile(i)}\n className=\"shrink-0 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={14} />\n </button>\n </li>\n ))}\n </ul>\n );\n}\n\nFileUploadList.displayName = \"FileUploadList\";\n","/**\n * Does a file satisfy an `accept` string?\n *\n * The browser enforces `accept` in the FILE PICKER only. A drag-and-drop never\n * opens a picker, so on the drop path this is the only thing standing between an\n * image-only dropzone and a dropped executable. Forwarding the attribute to the\n * hidden input is half the job; this is the other half.\n *\n * Follows the HTML `accept` grammar: a comma-separated list of extensions\n * (`.csv`), exact MIME types (`text/plain`) and type wildcards (`image/*`).\n * Matching is case-insensitive, because a drop reports whatever the OS says and\n * `TEXT/PLAIN` is a real thing to receive.\n *\n * A dropped file often carries an EMPTY `type` — the OS may not know it and\n * `File.type` is a hint, never a guarantee. That is why extension rules are\n * checked against the name and not the type: an `accept=\".csv\"` zone has to work\n * for a .csv whose type is `\"\"`, which is the common case rather than the edge.\n */\nexport function matchesAccept(file: File, accept: string): boolean {\n const rules = accept\n .split(\",\")\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean);\n\n // An accept string that lists nothing constrains nothing. Failing closed here\n // would turn `accept=\"\"` into a dropzone that silently swallows every file.\n if (rules.length === 0) return true;\n\n const type = file.type.toLowerCase();\n const name = file.name.toLowerCase();\n\n return rules.some((rule) => {\n if (rule.startsWith(\".\")) return name.endsWith(rule);\n if (rule.endsWith(\"/*\")) return type.startsWith(rule.slice(0, -1));\n return type === rule;\n });\n}\n","import { useCallback, useMemo, useState } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { FileUploadContext } from \"./FileUpload.context\";\nimport { FileUploadDropzone } from \"./FileUploadDropzone\";\nimport { FileUploadList } from \"./FileUploadList\";\nimport { matchesAccept } from \"./matchesAccept\";\nimport type { FileUploadProps } from \"./FileUpload.types\";\n\nfunction FileUploadRoot({\n children,\n value,\n onChange,\n accept,\n multiple = true,\n maxFiles,\n maxSize,\n disabled = false,\n className,\n}: FileUploadProps) {\n const [internalFiles, setInternalFiles] = useState<File[]>([]);\n const files = value ?? internalFiles;\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n let toAdd = Array.from(newFiles);\n\n // `accept` is enforced HERE, not only on the input: the input's attribute\n // governs the picker, and a drop never opens one.\n if (accept) {\n toAdd = toAdd.filter((f) => matchesAccept(f, accept));\n }\n\n if (maxSize) {\n toAdd = toAdd.filter((f) => f.size <= maxSize);\n }\n\n /*\n * `multiple={false}` REPLACES; `maxFiles` refuses the overflow.\n *\n * These used to be the same rule — a cap of one, first-wins — on the\n * reasoning that the two props must not disagree about what a single-file\n * upload means. But they are different statements:\n *\n * - `maxFiles={n}` is a QUOTA. Dropping five files into a three-file\n * field should keep three, not the last three.\n * - `multiple={false}` is a FIELD SHAPE: this input holds one file. Its\n * analogue is a radio group, where choosing again MOVES the selection.\n *\n * Under the old rule a single-file field could never change its mind: the\n * second choice computed `[old, new].slice(0, 1)` and discarded the new\n * file in silence. The YouGene estate worked around it by remounting the\n * component with a `key`, which is the shape of a bug rather than a\n * preference.\n */\n if (multiple === false) {\n /*\n * The FIRST of the incoming batch, not the last.\n *\n * Two separate questions hide here, and only one of them was the bug:\n *\n * - ACROSS gestures — choose a file, then choose another. The second\n * must win, or the field cannot change its mind. That is the fix.\n * - WITHIN one gesture — drop two files at once onto a single-file\n * field. Which one wins is arbitrary, and taking the first is what\n * has always shipped; there is no replacement intent in a single\n * ambiguous drop.\n *\n * Taking `slice(-1)` would have answered both with \"the last\", quietly\n * changing the second. The existing drop test caught it.\n */\n const replacement = toAdd.slice(0, 1);\n // Nothing survived the accept/size filters — leave the field alone\n // rather than clearing a file the user never asked to remove.\n const next = replacement.length > 0 ? replacement : files;\n\n if (value === undefined) setInternalFiles(next);\n onChange?.(next);\n\n return;\n }\n\n const updated = [...files, ...toAdd];\n const limited = maxFiles ? updated.slice(0, maxFiles) : updated;\n\n if (value === undefined) setInternalFiles(limited);\n onChange?.(limited);\n },\n [accept, files, maxFiles, maxSize, multiple, onChange, value],\n );\n\n const removeFile = useCallback(\n (index: number) => {\n const updated = files.filter((_, i) => i !== index);\n if (value === undefined) setInternalFiles(updated);\n onChange?.(updated);\n },\n [files, onChange, value],\n );\n\n const ctx = useMemo(\n () => ({ files, addFiles, removeFile, disabled, accept, multiple }),\n [files, addFiles, removeFile, disabled, accept, multiple],\n );\n\n return (\n <FileUploadContext.Provider value={ctx}>\n <div data-react-fancy-file-upload=\"\" className={cn(className)}>{children}</div>\n </FileUploadContext.Provider>\n );\n}\n\nexport const FileUpload = Object.assign(FileUploadRoot, {\n Dropzone: FileUploadDropzone,\n List: FileUploadList,\n});\n"]}
@@ -55,7 +55,10 @@ function FileUploadDropzone({
55
55
  ref: inputRef,
56
56
  type: "file",
57
57
  className: "hidden",
58
- onChange: (e) => e.target.files && addFiles(e.target.files),
58
+ onChange: (e) => {
59
+ if (e.target.files) addFiles(e.target.files);
60
+ e.target.value = "";
61
+ },
59
62
  accept,
60
63
  multiple,
61
64
  disabled
@@ -172,9 +175,15 @@ function FileUploadRoot({
172
175
  if (maxSize) {
173
176
  toAdd = toAdd.filter((f) => f.size <= maxSize);
174
177
  }
175
- const cap = multiple === false ? 1 : maxFiles;
178
+ if (multiple === false) {
179
+ const replacement = toAdd.slice(0, 1);
180
+ const next = replacement.length > 0 ? replacement : files;
181
+ if (value === void 0) setInternalFiles(next);
182
+ onChange?.(next);
183
+ return;
184
+ }
176
185
  const updated = [...files, ...toAdd];
177
- const limited = cap ? updated.slice(0, cap) : updated;
186
+ const limited = maxFiles ? updated.slice(0, maxFiles) : updated;
178
187
  if (value === void 0) setInternalFiles(limited);
179
188
  onChange?.(limited);
180
189
  },
@@ -200,5 +209,5 @@ var FileUpload = Object.assign(FileUploadRoot, {
200
209
  });
201
210
 
202
211
  export { FileUpload, useFileUpload };
203
- //# sourceMappingURL=chunk-32HCJ5GD.js.map
204
- //# sourceMappingURL=chunk-32HCJ5GD.js.map
212
+ //# sourceMappingURL=chunk-W6RDAZJC.js.map
213
+ //# sourceMappingURL=chunk-W6RDAZJC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/FileUpload/FileUpload.context.ts","../src/components/FileUpload/FileUploadDropzone.tsx","../src/components/FileUpload/FileUploadList.tsx","../src/components/FileUpload/matchesAccept.ts","../src/components/FileUpload/FileUpload.tsx"],"names":["jsxs","jsx","useState","useCallback","useMemo"],"mappings":"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","file":"chunk-W6RDAZJC.js","sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { FileUploadContextValue } from \"./FileUpload.types\";\n\nexport const FileUploadContext =\n createContext<FileUploadContextValue | null>(null);\n\nexport function useFileUpload(): FileUploadContextValue {\n const ctx = useContext(FileUploadContext);\n if (!ctx) {\n throw new Error(\n \"FileUpload compound components must be used within <FileUpload>\",\n );\n }\n return ctx;\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { Upload } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadDropzoneProps } from \"./FileUpload.types\";\n\nexport function FileUploadDropzone({\n children,\n className,\n}: FileUploadDropzoneProps) {\n const { addFiles, disabled, accept, multiple } = useFileUpload();\n const [dragOver, setDragOver] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(false);\n if (disabled) return;\n addFiles(e.dataTransfer.files);\n },\n [addFiles, disabled],\n );\n\n const handleDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(true);\n }, []);\n\n const handleDragLeave = useCallback(() => setDragOver(false), []);\n\n return (\n <div\n data-react-fancy-file-upload-dropzone=\"\"\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onClick={() => !disabled && inputRef.current?.click()}\n className={cn(\n \"flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors\",\n dragOver\n ? \"border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-950\"\n : \"border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-500\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n >\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files) addFiles(e.target.files);\n\n // Reset, or choosing the SAME file again fires no `change` event at\n // all and the gesture is simply dead — \"remove it, then re-add it\"\n // and \"retry the import that just failed\" both stop working. The\n // component never sees those files to reject them; it is not asked.\n e.target.value = \"\";\n }}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n />\n {children ?? (\n <>\n <Upload size={32} strokeWidth={1.5} className=\"mb-2 text-zinc-400\" />\n <p className=\"text-sm text-zinc-500\">\n Drop files here or click to browse\n </p>\n </>\n )}\n </div>\n );\n}\n\nFileUploadDropzone.displayName = \"FileUploadDropzone\";\n","import { useEffect, useMemo } from \"react\";\nimport { File, X } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadListProps } from \"./FileUpload.types\";\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction getExtension(name: string): string {\n const parts = name.split(\".\");\n return parts.length > 1 ? parts.pop()!.toUpperCase() : \"\";\n}\n\nfunction ThumbnailItem({ file, index, onRemove }: { file: File; index: number; onRemove: (i: number) => void }) {\n const isImage = file.type.startsWith(\"image/\");\n const objectUrl = useMemo(() => (isImage ? URL.createObjectURL(file) : null), [file, isImage]);\n\n useEffect(() => {\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [objectUrl]);\n\n return (\n <div className=\"group relative flex w-20 flex-col items-center gap-1\">\n <div className=\"relative h-20 w-20 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800\">\n {isImage && objectUrl ? (\n <img src={objectUrl} alt={file.name} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-1\">\n <File size={20} className=\"text-zinc-400\" />\n <span className=\"text-[10px] font-medium text-zinc-500\">{getExtension(file.name)}</span>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => onRemove(index)}\n className=\"absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-black/80 group-hover:opacity-100\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={10} />\n </button>\n </div>\n <span className=\"w-full truncate text-center text-xs text-zinc-500\">{file.name}</span>\n </div>\n );\n}\n\nexport function FileUploadList({ thumbnail, className }: FileUploadListProps) {\n const { files, removeFile } = useFileUpload();\n\n if (files.length === 0) return null;\n\n if (thumbnail) {\n return (\n <div data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 flex flex-wrap gap-3\", className)}>\n {files.map((file, i) => (\n <ThumbnailItem key={`${file.name}-${i}`} file={file} index={i} onRemove={removeFile} />\n ))}\n </div>\n );\n }\n\n return (\n <ul data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 space-y-2\", className)}>\n {files.map((file, i) => (\n <li\n key={`${file.name}-${i}`}\n className=\"flex items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700\"\n >\n <File size={16} className=\"shrink-0 text-zinc-400\" />\n <span className=\"flex-1 truncate\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-zinc-400\">\n {formatSize(file.size)}\n </span>\n <button\n type=\"button\"\n onClick={() => removeFile(i)}\n className=\"shrink-0 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={14} />\n </button>\n </li>\n ))}\n </ul>\n );\n}\n\nFileUploadList.displayName = \"FileUploadList\";\n","/**\n * Does a file satisfy an `accept` string?\n *\n * The browser enforces `accept` in the FILE PICKER only. A drag-and-drop never\n * opens a picker, so on the drop path this is the only thing standing between an\n * image-only dropzone and a dropped executable. Forwarding the attribute to the\n * hidden input is half the job; this is the other half.\n *\n * Follows the HTML `accept` grammar: a comma-separated list of extensions\n * (`.csv`), exact MIME types (`text/plain`) and type wildcards (`image/*`).\n * Matching is case-insensitive, because a drop reports whatever the OS says and\n * `TEXT/PLAIN` is a real thing to receive.\n *\n * A dropped file often carries an EMPTY `type` — the OS may not know it and\n * `File.type` is a hint, never a guarantee. That is why extension rules are\n * checked against the name and not the type: an `accept=\".csv\"` zone has to work\n * for a .csv whose type is `\"\"`, which is the common case rather than the edge.\n */\nexport function matchesAccept(file: File, accept: string): boolean {\n const rules = accept\n .split(\",\")\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean);\n\n // An accept string that lists nothing constrains nothing. Failing closed here\n // would turn `accept=\"\"` into a dropzone that silently swallows every file.\n if (rules.length === 0) return true;\n\n const type = file.type.toLowerCase();\n const name = file.name.toLowerCase();\n\n return rules.some((rule) => {\n if (rule.startsWith(\".\")) return name.endsWith(rule);\n if (rule.endsWith(\"/*\")) return type.startsWith(rule.slice(0, -1));\n return type === rule;\n });\n}\n","import { useCallback, useMemo, useState } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { FileUploadContext } from \"./FileUpload.context\";\nimport { FileUploadDropzone } from \"./FileUploadDropzone\";\nimport { FileUploadList } from \"./FileUploadList\";\nimport { matchesAccept } from \"./matchesAccept\";\nimport type { FileUploadProps } from \"./FileUpload.types\";\n\nfunction FileUploadRoot({\n children,\n value,\n onChange,\n accept,\n multiple = true,\n maxFiles,\n maxSize,\n disabled = false,\n className,\n}: FileUploadProps) {\n const [internalFiles, setInternalFiles] = useState<File[]>([]);\n const files = value ?? internalFiles;\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n let toAdd = Array.from(newFiles);\n\n // `accept` is enforced HERE, not only on the input: the input's attribute\n // governs the picker, and a drop never opens one.\n if (accept) {\n toAdd = toAdd.filter((f) => matchesAccept(f, accept));\n }\n\n if (maxSize) {\n toAdd = toAdd.filter((f) => f.size <= maxSize);\n }\n\n /*\n * `multiple={false}` REPLACES; `maxFiles` refuses the overflow.\n *\n * These used to be the same rule — a cap of one, first-wins — on the\n * reasoning that the two props must not disagree about what a single-file\n * upload means. But they are different statements:\n *\n * - `maxFiles={n}` is a QUOTA. Dropping five files into a three-file\n * field should keep three, not the last three.\n * - `multiple={false}` is a FIELD SHAPE: this input holds one file. Its\n * analogue is a radio group, where choosing again MOVES the selection.\n *\n * Under the old rule a single-file field could never change its mind: the\n * second choice computed `[old, new].slice(0, 1)` and discarded the new\n * file in silence. The YouGene estate worked around it by remounting the\n * component with a `key`, which is the shape of a bug rather than a\n * preference.\n */\n if (multiple === false) {\n /*\n * The FIRST of the incoming batch, not the last.\n *\n * Two separate questions hide here, and only one of them was the bug:\n *\n * - ACROSS gestures — choose a file, then choose another. The second\n * must win, or the field cannot change its mind. That is the fix.\n * - WITHIN one gesture — drop two files at once onto a single-file\n * field. Which one wins is arbitrary, and taking the first is what\n * has always shipped; there is no replacement intent in a single\n * ambiguous drop.\n *\n * Taking `slice(-1)` would have answered both with \"the last\", quietly\n * changing the second. The existing drop test caught it.\n */\n const replacement = toAdd.slice(0, 1);\n // Nothing survived the accept/size filters — leave the field alone\n // rather than clearing a file the user never asked to remove.\n const next = replacement.length > 0 ? replacement : files;\n\n if (value === undefined) setInternalFiles(next);\n onChange?.(next);\n\n return;\n }\n\n const updated = [...files, ...toAdd];\n const limited = maxFiles ? updated.slice(0, maxFiles) : updated;\n\n if (value === undefined) setInternalFiles(limited);\n onChange?.(limited);\n },\n [accept, files, maxFiles, maxSize, multiple, onChange, value],\n );\n\n const removeFile = useCallback(\n (index: number) => {\n const updated = files.filter((_, i) => i !== index);\n if (value === undefined) setInternalFiles(updated);\n onChange?.(updated);\n },\n [files, onChange, value],\n );\n\n const ctx = useMemo(\n () => ({ files, addFiles, removeFile, disabled, accept, multiple }),\n [files, addFiles, removeFile, disabled, accept, multiple],\n );\n\n return (\n <FileUploadContext.Provider value={ctx}>\n <div data-react-fancy-file-upload=\"\" className={cn(className)}>{children}</div>\n </FileUploadContext.Provider>\n );\n}\n\nexport const FileUpload = Object.assign(FileUploadRoot, {\n Dropzone: FileUploadDropzone,\n List: FileUploadList,\n});\n"]}
package/dist/composer.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWRQBV7GU_cjs = require('./chunk-WRQBV7GU.cjs');
3
+ var chunk23FTDEGR_cjs = require('./chunk-23FTDEGR.cjs');
4
4
  require('./chunk-SNHU2AI4.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 chunkWRQBV7GU_cjs.Composer; }
11
+ get: function () { return chunk23FTDEGR_cjs.Composer; }
12
12
  });
13
13
  //# sourceMappingURL=composer.cjs.map
14
14
  //# sourceMappingURL=composer.cjs.map
@@ -1,2 +1,2 @@
1
- export { C as Composer, a as ComposerProps, H as HeldPaste } from './index-CDo7LyIB.cjs';
1
+ export { C as Composer, a as ComposerProps, H as HeldPaste } from './index-C8PKWA_5.cjs';
2
2
  import 'react';
@@ -1,2 +1,2 @@
1
- export { C as Composer, a as ComposerProps, H as HeldPaste } from './index-CDo7LyIB.js';
1
+ export { C as Composer, a as ComposerProps, H as HeldPaste } from './index-C8PKWA_5.js';
2
2
  import 'react';
package/dist/composer.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Composer } from './chunk-WHOVPDC5.js';
1
+ export { Composer } from './chunk-2XAVAPDA.js';
2
2
  import './chunk-EDYAXGUM.js';
3
3
  import './chunk-MMW2JGKZ.js';
4
4
  //# sourceMappingURL=composer.js.map
@@ -1,17 +1,17 @@
1
1
  'use strict';
2
2
 
3
- var chunkLAQGBLIU_cjs = require('./chunk-LAQGBLIU.cjs');
3
+ var chunkVKXNCYLE_cjs = require('./chunk-VKXNCYLE.cjs');
4
4
  require('./chunk-ZU5Q77OT.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "FileUpload", {
9
9
  enumerable: true,
10
- get: function () { return chunkLAQGBLIU_cjs.FileUpload; }
10
+ get: function () { return chunkVKXNCYLE_cjs.FileUpload; }
11
11
  });
12
12
  Object.defineProperty(exports, "useFileUpload", {
13
13
  enumerable: true,
14
- get: function () { return chunkLAQGBLIU_cjs.useFileUpload; }
14
+ get: function () { return chunkVKXNCYLE_cjs.useFileUpload; }
15
15
  });
16
16
  //# sourceMappingURL=file-upload.cjs.map
17
17
  //# sourceMappingURL=file-upload.cjs.map
@@ -1,4 +1,4 @@
1
- export { FileUpload, useFileUpload } from './chunk-32HCJ5GD.js';
1
+ export { FileUpload, useFileUpload } from './chunk-W6RDAZJC.js';
2
2
  import './chunk-MMW2JGKZ.js';
3
3
  //# sourceMappingURL=file-upload.js.map
4
4
  //# sourceMappingURL=file-upload.js.map
@@ -90,6 +90,27 @@ interface UsePastePillsResult {
90
90
  * still closed carries the text.
91
91
  *
92
92
  * It owns no DOM. The host renders the pills.
93
+ *
94
+ * ---------------------------------------------------------------------------
95
+ * Who actually calls this (as of 5.32.0): `Composer`, and nothing else
96
+ * ---------------------------------------------------------------------------
97
+ *
98
+ * The estate that specified it **withdrew before adopting it**, hours after it
99
+ * shipped — their design moved the pill INTO the message as a `[paste:id|label]`
100
+ * marker in the same grammar as their other object tags, which makes the message
101
+ * itself the record of what is attached. A parallel `heldPastes` array would then be
102
+ * a second place answering "what am I sending?", and the two would disagree the
103
+ * moment someone backspaced a badge. Their reasoning was better than the spec.
104
+ *
105
+ * Recorded because a reader should not infer an extension point under active
106
+ * external use. It is kept rather than withdrawn for two reasons that are about
107
+ * cost, not optimism: it is published API on a 5.x package, so removing it needs a
108
+ * major; and `Composer` calls it, so it is exercised by that component's tests on
109
+ * every run and cannot rot unnoticed.
110
+ *
111
+ * **It gets no further surface without a real caller.** No extra options, no second
112
+ * hook. An API with one hypothetical consumer is how a package accumulates surface
113
+ * nobody uses.
93
114
  */
94
115
  declare function usePastePills({ threshold, heldPastes: controlledHeldPastes, onHeldPastesChange, }?: UsePastePillsOptions): UsePastePillsResult;
95
116
 
@@ -90,6 +90,27 @@ interface UsePastePillsResult {
90
90
  * still closed carries the text.
91
91
  *
92
92
  * It owns no DOM. The host renders the pills.
93
+ *
94
+ * ---------------------------------------------------------------------------
95
+ * Who actually calls this (as of 5.32.0): `Composer`, and nothing else
96
+ * ---------------------------------------------------------------------------
97
+ *
98
+ * The estate that specified it **withdrew before adopting it**, hours after it
99
+ * shipped — their design moved the pill INTO the message as a `[paste:id|label]`
100
+ * marker in the same grammar as their other object tags, which makes the message
101
+ * itself the record of what is attached. A parallel `heldPastes` array would then be
102
+ * a second place answering "what am I sending?", and the two would disagree the
103
+ * moment someone backspaced a badge. Their reasoning was better than the spec.
104
+ *
105
+ * Recorded because a reader should not infer an extension point under active
106
+ * external use. It is kept rather than withdrawn for two reasons that are about
107
+ * cost, not optimism: it is published API on a 5.x package, so removing it needs a
108
+ * major; and `Composer` calls it, so it is exercised by that component's tests on
109
+ * every run and cannot rot unnoticed.
110
+ *
111
+ * **It gets no further surface without a real caller.** No extra options, no second
112
+ * hook. An API with one hypothetical consumer is how a package accumulates surface
113
+ * nobody uses.
93
114
  */
94
115
  declare function usePastePills({ threshold, heldPastes: controlledHeldPastes, onHeldPastesChange, }?: UsePastePillsOptions): UsePastePillsResult;
95
116
 
package/dist/index.cjs CHANGED
@@ -60,7 +60,7 @@ var chunkV35A6B7A_cjs = require('./chunk-V35A6B7A.cjs');
60
60
  var chunkRPVCMW64_cjs = require('./chunk-RPVCMW64.cjs');
61
61
  var chunkGBGJWAXZ_cjs = require('./chunk-GBGJWAXZ.cjs');
62
62
  var chunkT45XK5GN_cjs = require('./chunk-T45XK5GN.cjs');
63
- var chunkLAQGBLIU_cjs = require('./chunk-LAQGBLIU.cjs');
63
+ var chunkVKXNCYLE_cjs = require('./chunk-VKXNCYLE.cjs');
64
64
  var chunkOOZCJLEF_cjs = require('./chunk-OOZCJLEF.cjs');
65
65
  require('./chunk-FJKZ5NOA.cjs');
66
66
  var chunkQDVDBUJM_cjs = require('./chunk-QDVDBUJM.cjs');
@@ -85,7 +85,7 @@ var chunkBVRPDMB7_cjs = require('./chunk-BVRPDMB7.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 chunkWRQBV7GU_cjs = require('./chunk-WRQBV7GU.cjs');
88
+ var chunk23FTDEGR_cjs = require('./chunk-23FTDEGR.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');
@@ -435,11 +435,11 @@ Object.defineProperty(exports, "validateFolderName", {
435
435
  });
436
436
  Object.defineProperty(exports, "FileUpload", {
437
437
  enumerable: true,
438
- get: function () { return chunkLAQGBLIU_cjs.FileUpload; }
438
+ get: function () { return chunkVKXNCYLE_cjs.FileUpload; }
439
439
  });
440
440
  Object.defineProperty(exports, "useFileUpload", {
441
441
  enumerable: true,
442
- get: function () { return chunkLAQGBLIU_cjs.useFileUpload; }
442
+ get: function () { return chunkVKXNCYLE_cjs.useFileUpload; }
443
443
  });
444
444
  Object.defineProperty(exports, "Grid", {
445
445
  enumerable: true,
@@ -595,11 +595,11 @@ Object.defineProperty(exports, "useAnimation", {
595
595
  });
596
596
  Object.defineProperty(exports, "Composer", {
597
597
  enumerable: true,
598
- get: function () { return chunkWRQBV7GU_cjs.Composer; }
598
+ get: function () { return chunk23FTDEGR_cjs.Composer; }
599
599
  });
600
600
  Object.defineProperty(exports, "usePastePills", {
601
601
  enumerable: true,
602
- get: function () { return chunkWRQBV7GU_cjs.usePastePills; }
602
+ get: function () { return chunk23FTDEGR_cjs.usePastePills; }
603
603
  });
604
604
  Object.defineProperty(exports, "Container", {
605
605
  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 { C as Composer, a as ComposerProps, H as HeldPaste, P as PasteEventLike, U as UsePastePillsOptions, b as UsePastePillsResult, u as usePastePills } from './index-CDo7LyIB.cjs';
67
+ export { C as Composer, a as ComposerProps, H as HeldPaste, P as PasteEventLike, U as UsePastePillsOptions, b as UsePastePillsResult, u as usePastePills } from './index-C8PKWA_5.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 { C as Composer, a as ComposerProps, H as HeldPaste, P as PasteEventLike, U as UsePastePillsOptions, b as UsePastePillsResult, u as usePastePills } from './index-CDo7LyIB.js';
67
+ export { C as Composer, a as ComposerProps, H as HeldPaste, P as PasteEventLike, U as UsePastePillsOptions, b as UsePastePillsResult, u as usePastePills } from './index-C8PKWA_5.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
@@ -58,7 +58,7 @@ export { PdfViewer } from './chunk-SOYORSLZ.js';
58
58
  export { Menu, useMenu } from './chunk-RA2VOQ46.js';
59
59
  export { MobileMenu, useMobileMenu } from './chunk-P7F32HIH.js';
60
60
  export { FileBrowser, FileBrowserNewFolder, useFileBrowser, validateFolderName } from './chunk-2KAJ4L7C.js';
61
- export { FileUpload, useFileUpload } from './chunk-32HCJ5GD.js';
61
+ export { FileUpload, useFileUpload } from './chunk-W6RDAZJC.js';
62
62
  export { Grid } from './chunk-TM3RTWKI.js';
63
63
  import './chunk-CFH6CJQG.js';
64
64
  export { ImageViewer } from './chunk-MUCXXYDN.js';
@@ -83,7 +83,7 @@ export { ColorPicker } from './chunk-F2GAZZVS.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, usePastePills } from './chunk-WHOVPDC5.js';
86
+ export { Composer, usePastePills } from './chunk-2XAVAPDA.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.32.0",
3
+ "version": "5.33.0",
4
4
  "description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/FileUpload/FileUpload.context.ts","../src/components/FileUpload/FileUploadDropzone.tsx","../src/components/FileUpload/FileUploadList.tsx","../src/components/FileUpload/matchesAccept.ts","../src/components/FileUpload/FileUpload.tsx"],"names":["jsxs","jsx","useState","useCallback","useMemo"],"mappings":"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","file":"chunk-32HCJ5GD.js","sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { FileUploadContextValue } from \"./FileUpload.types\";\n\nexport const FileUploadContext =\n createContext<FileUploadContextValue | null>(null);\n\nexport function useFileUpload(): FileUploadContextValue {\n const ctx = useContext(FileUploadContext);\n if (!ctx) {\n throw new Error(\n \"FileUpload compound components must be used within <FileUpload>\",\n );\n }\n return ctx;\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { Upload } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadDropzoneProps } from \"./FileUpload.types\";\n\nexport function FileUploadDropzone({\n children,\n className,\n}: FileUploadDropzoneProps) {\n const { addFiles, disabled, accept, multiple } = useFileUpload();\n const [dragOver, setDragOver] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(false);\n if (disabled) return;\n addFiles(e.dataTransfer.files);\n },\n [addFiles, disabled],\n );\n\n const handleDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(true);\n }, []);\n\n const handleDragLeave = useCallback(() => setDragOver(false), []);\n\n return (\n <div\n data-react-fancy-file-upload-dropzone=\"\"\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onClick={() => !disabled && inputRef.current?.click()}\n className={cn(\n \"flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors\",\n dragOver\n ? \"border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-950\"\n : \"border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-500\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n >\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(e) => e.target.files && addFiles(e.target.files)}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n />\n {children ?? (\n <>\n <Upload size={32} strokeWidth={1.5} className=\"mb-2 text-zinc-400\" />\n <p className=\"text-sm text-zinc-500\">\n Drop files here or click to browse\n </p>\n </>\n )}\n </div>\n );\n}\n\nFileUploadDropzone.displayName = \"FileUploadDropzone\";\n","import { useEffect, useMemo } from \"react\";\nimport { File, X } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadListProps } from \"./FileUpload.types\";\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction getExtension(name: string): string {\n const parts = name.split(\".\");\n return parts.length > 1 ? parts.pop()!.toUpperCase() : \"\";\n}\n\nfunction ThumbnailItem({ file, index, onRemove }: { file: File; index: number; onRemove: (i: number) => void }) {\n const isImage = file.type.startsWith(\"image/\");\n const objectUrl = useMemo(() => (isImage ? URL.createObjectURL(file) : null), [file, isImage]);\n\n useEffect(() => {\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [objectUrl]);\n\n return (\n <div className=\"group relative flex w-20 flex-col items-center gap-1\">\n <div className=\"relative h-20 w-20 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800\">\n {isImage && objectUrl ? (\n <img src={objectUrl} alt={file.name} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-1\">\n <File size={20} className=\"text-zinc-400\" />\n <span className=\"text-[10px] font-medium text-zinc-500\">{getExtension(file.name)}</span>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => onRemove(index)}\n className=\"absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-black/80 group-hover:opacity-100\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={10} />\n </button>\n </div>\n <span className=\"w-full truncate text-center text-xs text-zinc-500\">{file.name}</span>\n </div>\n );\n}\n\nexport function FileUploadList({ thumbnail, className }: FileUploadListProps) {\n const { files, removeFile } = useFileUpload();\n\n if (files.length === 0) return null;\n\n if (thumbnail) {\n return (\n <div data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 flex flex-wrap gap-3\", className)}>\n {files.map((file, i) => (\n <ThumbnailItem key={`${file.name}-${i}`} file={file} index={i} onRemove={removeFile} />\n ))}\n </div>\n );\n }\n\n return (\n <ul data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 space-y-2\", className)}>\n {files.map((file, i) => (\n <li\n key={`${file.name}-${i}`}\n className=\"flex items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700\"\n >\n <File size={16} className=\"shrink-0 text-zinc-400\" />\n <span className=\"flex-1 truncate\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-zinc-400\">\n {formatSize(file.size)}\n </span>\n <button\n type=\"button\"\n onClick={() => removeFile(i)}\n className=\"shrink-0 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={14} />\n </button>\n </li>\n ))}\n </ul>\n );\n}\n\nFileUploadList.displayName = \"FileUploadList\";\n","/**\n * Does a file satisfy an `accept` string?\n *\n * The browser enforces `accept` in the FILE PICKER only. A drag-and-drop never\n * opens a picker, so on the drop path this is the only thing standing between an\n * image-only dropzone and a dropped executable. Forwarding the attribute to the\n * hidden input is half the job; this is the other half.\n *\n * Follows the HTML `accept` grammar: a comma-separated list of extensions\n * (`.csv`), exact MIME types (`text/plain`) and type wildcards (`image/*`).\n * Matching is case-insensitive, because a drop reports whatever the OS says and\n * `TEXT/PLAIN` is a real thing to receive.\n *\n * A dropped file often carries an EMPTY `type` — the OS may not know it and\n * `File.type` is a hint, never a guarantee. That is why extension rules are\n * checked against the name and not the type: an `accept=\".csv\"` zone has to work\n * for a .csv whose type is `\"\"`, which is the common case rather than the edge.\n */\nexport function matchesAccept(file: File, accept: string): boolean {\n const rules = accept\n .split(\",\")\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean);\n\n // An accept string that lists nothing constrains nothing. Failing closed here\n // would turn `accept=\"\"` into a dropzone that silently swallows every file.\n if (rules.length === 0) return true;\n\n const type = file.type.toLowerCase();\n const name = file.name.toLowerCase();\n\n return rules.some((rule) => {\n if (rule.startsWith(\".\")) return name.endsWith(rule);\n if (rule.endsWith(\"/*\")) return type.startsWith(rule.slice(0, -1));\n return type === rule;\n });\n}\n","import { useCallback, useMemo, useState } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { FileUploadContext } from \"./FileUpload.context\";\nimport { FileUploadDropzone } from \"./FileUploadDropzone\";\nimport { FileUploadList } from \"./FileUploadList\";\nimport { matchesAccept } from \"./matchesAccept\";\nimport type { FileUploadProps } from \"./FileUpload.types\";\n\nfunction FileUploadRoot({\n children,\n value,\n onChange,\n accept,\n multiple = true,\n maxFiles,\n maxSize,\n disabled = false,\n className,\n}: FileUploadProps) {\n const [internalFiles, setInternalFiles] = useState<File[]>([]);\n const files = value ?? internalFiles;\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n let toAdd = Array.from(newFiles);\n\n // `accept` is enforced HERE, not only on the input: the input's attribute\n // governs the picker, and a drop never opens one.\n if (accept) {\n toAdd = toAdd.filter((f) => matchesAccept(f, accept));\n }\n\n if (maxSize) {\n toAdd = toAdd.filter((f) => f.size <= maxSize);\n }\n\n // `multiple={false}` is a cap of one, applied the same way `maxFiles` is —\n // first-wins rather than replace-last, so the two props cannot disagree\n // about what a single-file upload means.\n const cap = multiple === false ? 1 : maxFiles;\n\n const updated = [...files, ...toAdd];\n const limited = cap ? updated.slice(0, cap) : updated;\n\n if (value === undefined) setInternalFiles(limited);\n onChange?.(limited);\n },\n [accept, files, maxFiles, maxSize, multiple, onChange, value],\n );\n\n const removeFile = useCallback(\n (index: number) => {\n const updated = files.filter((_, i) => i !== index);\n if (value === undefined) setInternalFiles(updated);\n onChange?.(updated);\n },\n [files, onChange, value],\n );\n\n const ctx = useMemo(\n () => ({ files, addFiles, removeFile, disabled, accept, multiple }),\n [files, addFiles, removeFile, disabled, accept, multiple],\n );\n\n return (\n <FileUploadContext.Provider value={ctx}>\n <div data-react-fancy-file-upload=\"\" className={cn(className)}>{children}</div>\n </FileUploadContext.Provider>\n );\n}\n\nexport const FileUpload = Object.assign(FileUploadRoot, {\n Dropzone: FileUploadDropzone,\n List: FileUploadList,\n});\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/FileUpload/FileUpload.context.ts","../src/components/FileUpload/FileUploadDropzone.tsx","../src/components/FileUpload/FileUploadList.tsx","../src/components/FileUpload/matchesAccept.ts","../src/components/FileUpload/FileUpload.tsx"],"names":["createContext","useContext","useState","useRef","useCallback","jsxs","cn","jsx","Fragment","Upload","useMemo","useEffect","File","X"],"mappings":"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","file":"chunk-LAQGBLIU.cjs","sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { FileUploadContextValue } from \"./FileUpload.types\";\n\nexport const FileUploadContext =\n createContext<FileUploadContextValue | null>(null);\n\nexport function useFileUpload(): FileUploadContextValue {\n const ctx = useContext(FileUploadContext);\n if (!ctx) {\n throw new Error(\n \"FileUpload compound components must be used within <FileUpload>\",\n );\n }\n return ctx;\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { Upload } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadDropzoneProps } from \"./FileUpload.types\";\n\nexport function FileUploadDropzone({\n children,\n className,\n}: FileUploadDropzoneProps) {\n const { addFiles, disabled, accept, multiple } = useFileUpload();\n const [dragOver, setDragOver] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(false);\n if (disabled) return;\n addFiles(e.dataTransfer.files);\n },\n [addFiles, disabled],\n );\n\n const handleDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n setDragOver(true);\n }, []);\n\n const handleDragLeave = useCallback(() => setDragOver(false), []);\n\n return (\n <div\n data-react-fancy-file-upload-dropzone=\"\"\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onClick={() => !disabled && inputRef.current?.click()}\n className={cn(\n \"flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors\",\n dragOver\n ? \"border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-950\"\n : \"border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-500\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n >\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(e) => e.target.files && addFiles(e.target.files)}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n />\n {children ?? (\n <>\n <Upload size={32} strokeWidth={1.5} className=\"mb-2 text-zinc-400\" />\n <p className=\"text-sm text-zinc-500\">\n Drop files here or click to browse\n </p>\n </>\n )}\n </div>\n );\n}\n\nFileUploadDropzone.displayName = \"FileUploadDropzone\";\n","import { useEffect, useMemo } from \"react\";\nimport { File, X } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport { useFileUpload } from \"./FileUpload.context\";\nimport type { FileUploadListProps } from \"./FileUpload.types\";\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction getExtension(name: string): string {\n const parts = name.split(\".\");\n return parts.length > 1 ? parts.pop()!.toUpperCase() : \"\";\n}\n\nfunction ThumbnailItem({ file, index, onRemove }: { file: File; index: number; onRemove: (i: number) => void }) {\n const isImage = file.type.startsWith(\"image/\");\n const objectUrl = useMemo(() => (isImage ? URL.createObjectURL(file) : null), [file, isImage]);\n\n useEffect(() => {\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [objectUrl]);\n\n return (\n <div className=\"group relative flex w-20 flex-col items-center gap-1\">\n <div className=\"relative h-20 w-20 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800\">\n {isImage && objectUrl ? (\n <img src={objectUrl} alt={file.name} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-1\">\n <File size={20} className=\"text-zinc-400\" />\n <span className=\"text-[10px] font-medium text-zinc-500\">{getExtension(file.name)}</span>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => onRemove(index)}\n className=\"absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-black/80 group-hover:opacity-100\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={10} />\n </button>\n </div>\n <span className=\"w-full truncate text-center text-xs text-zinc-500\">{file.name}</span>\n </div>\n );\n}\n\nexport function FileUploadList({ thumbnail, className }: FileUploadListProps) {\n const { files, removeFile } = useFileUpload();\n\n if (files.length === 0) return null;\n\n if (thumbnail) {\n return (\n <div data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 flex flex-wrap gap-3\", className)}>\n {files.map((file, i) => (\n <ThumbnailItem key={`${file.name}-${i}`} file={file} index={i} onRemove={removeFile} />\n ))}\n </div>\n );\n }\n\n return (\n <ul data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 space-y-2\", className)}>\n {files.map((file, i) => (\n <li\n key={`${file.name}-${i}`}\n className=\"flex items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700\"\n >\n <File size={16} className=\"shrink-0 text-zinc-400\" />\n <span className=\"flex-1 truncate\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-zinc-400\">\n {formatSize(file.size)}\n </span>\n <button\n type=\"button\"\n onClick={() => removeFile(i)}\n className=\"shrink-0 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={14} />\n </button>\n </li>\n ))}\n </ul>\n );\n}\n\nFileUploadList.displayName = \"FileUploadList\";\n","/**\n * Does a file satisfy an `accept` string?\n *\n * The browser enforces `accept` in the FILE PICKER only. A drag-and-drop never\n * opens a picker, so on the drop path this is the only thing standing between an\n * image-only dropzone and a dropped executable. Forwarding the attribute to the\n * hidden input is half the job; this is the other half.\n *\n * Follows the HTML `accept` grammar: a comma-separated list of extensions\n * (`.csv`), exact MIME types (`text/plain`) and type wildcards (`image/*`).\n * Matching is case-insensitive, because a drop reports whatever the OS says and\n * `TEXT/PLAIN` is a real thing to receive.\n *\n * A dropped file often carries an EMPTY `type` — the OS may not know it and\n * `File.type` is a hint, never a guarantee. That is why extension rules are\n * checked against the name and not the type: an `accept=\".csv\"` zone has to work\n * for a .csv whose type is `\"\"`, which is the common case rather than the edge.\n */\nexport function matchesAccept(file: File, accept: string): boolean {\n const rules = accept\n .split(\",\")\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean);\n\n // An accept string that lists nothing constrains nothing. Failing closed here\n // would turn `accept=\"\"` into a dropzone that silently swallows every file.\n if (rules.length === 0) return true;\n\n const type = file.type.toLowerCase();\n const name = file.name.toLowerCase();\n\n return rules.some((rule) => {\n if (rule.startsWith(\".\")) return name.endsWith(rule);\n if (rule.endsWith(\"/*\")) return type.startsWith(rule.slice(0, -1));\n return type === rule;\n });\n}\n","import { useCallback, useMemo, useState } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { FileUploadContext } from \"./FileUpload.context\";\nimport { FileUploadDropzone } from \"./FileUploadDropzone\";\nimport { FileUploadList } from \"./FileUploadList\";\nimport { matchesAccept } from \"./matchesAccept\";\nimport type { FileUploadProps } from \"./FileUpload.types\";\n\nfunction FileUploadRoot({\n children,\n value,\n onChange,\n accept,\n multiple = true,\n maxFiles,\n maxSize,\n disabled = false,\n className,\n}: FileUploadProps) {\n const [internalFiles, setInternalFiles] = useState<File[]>([]);\n const files = value ?? internalFiles;\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n let toAdd = Array.from(newFiles);\n\n // `accept` is enforced HERE, not only on the input: the input's attribute\n // governs the picker, and a drop never opens one.\n if (accept) {\n toAdd = toAdd.filter((f) => matchesAccept(f, accept));\n }\n\n if (maxSize) {\n toAdd = toAdd.filter((f) => f.size <= maxSize);\n }\n\n // `multiple={false}` is a cap of one, applied the same way `maxFiles` is —\n // first-wins rather than replace-last, so the two props cannot disagree\n // about what a single-file upload means.\n const cap = multiple === false ? 1 : maxFiles;\n\n const updated = [...files, ...toAdd];\n const limited = cap ? updated.slice(0, cap) : updated;\n\n if (value === undefined) setInternalFiles(limited);\n onChange?.(limited);\n },\n [accept, files, maxFiles, maxSize, multiple, onChange, value],\n );\n\n const removeFile = useCallback(\n (index: number) => {\n const updated = files.filter((_, i) => i !== index);\n if (value === undefined) setInternalFiles(updated);\n onChange?.(updated);\n },\n [files, onChange, value],\n );\n\n const ctx = useMemo(\n () => ({ files, addFiles, removeFile, disabled, accept, multiple }),\n [files, addFiles, removeFile, disabled, accept, multiple],\n );\n\n return (\n <FileUploadContext.Provider value={ctx}>\n <div data-react-fancy-file-upload=\"\" className={cn(className)}>{children}</div>\n </FileUploadContext.Provider>\n );\n}\n\nexport const FileUpload = Object.assign(FileUploadRoot, {\n Dropzone: FileUploadDropzone,\n List: FileUploadList,\n});\n"]}
@@ -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-WHOVPDC5.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 */\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/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-WRQBV7GU.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 */\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"]}