@particle-academy/react-fancy 5.36.2 → 5.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/autocomplete.cjs +3 -3
  3. package/dist/autocomplete.js +2 -2
  4. package/dist/{chunk-FBXYZC4Z.cjs → chunk-AE53RKWR.cjs} +2 -2
  5. package/dist/{chunk-FBXYZC4Z.cjs.map → chunk-AE53RKWR.cjs.map} +1 -1
  6. package/dist/{chunk-FXP4JLGU.cjs → chunk-AR3JOPGT.cjs} +3 -3
  7. package/dist/{chunk-FXP4JLGU.cjs.map → chunk-AR3JOPGT.cjs.map} +1 -1
  8. package/dist/{chunk-35EN5AYE.js → chunk-CF5KRDN4.js} +6 -3
  9. package/dist/chunk-CF5KRDN4.js.map +1 -0
  10. package/dist/{chunk-6ASZUE4T.js → chunk-FQLL2JI7.js} +3 -3
  11. package/dist/{chunk-6ASZUE4T.js.map → chunk-FQLL2JI7.js.map} +1 -1
  12. package/dist/{chunk-ZH3SPBVB.js → chunk-IECPLOSZ.js} +3 -3
  13. package/dist/{chunk-ZH3SPBVB.js.map → chunk-IECPLOSZ.js.map} +1 -1
  14. package/dist/{chunk-E2SZBIAV.cjs → chunk-IWQ3ZFLZ.cjs} +5 -5
  15. package/dist/{chunk-E2SZBIAV.cjs.map → chunk-IWQ3ZFLZ.cjs.map} +1 -1
  16. package/dist/{chunk-QBNR5IVM.cjs → chunk-MFBUK4RV.cjs} +3 -3
  17. package/dist/{chunk-QBNR5IVM.cjs.map → chunk-MFBUK4RV.cjs.map} +1 -1
  18. package/dist/{chunk-DNV7MUEE.cjs → chunk-QLL7MWAU.cjs} +4 -4
  19. package/dist/{chunk-DNV7MUEE.cjs.map → chunk-QLL7MWAU.cjs.map} +1 -1
  20. package/dist/{chunk-ZDDWAVVZ.js → chunk-RLE5WYY6.js} +2 -2
  21. package/dist/{chunk-ZDDWAVVZ.js.map → chunk-RLE5WYY6.js.map} +1 -1
  22. package/dist/chunk-RYKPYILS.cjs +699 -0
  23. package/dist/chunk-RYKPYILS.cjs.map +1 -0
  24. package/dist/{chunk-O3WEV562.js → chunk-WUV3XDJH.js} +3 -3
  25. package/dist/{chunk-O3WEV562.js.map → chunk-WUV3XDJH.js.map} +1 -1
  26. package/dist/{chunk-ISPCXIJZ.cjs → chunk-X7H75WVP.cjs} +6 -3
  27. package/dist/chunk-X7H75WVP.cjs.map +1 -0
  28. package/dist/chunk-Z4GO6N6N.js +695 -0
  29. package/dist/chunk-Z4GO6N6N.js.map +1 -0
  30. package/dist/{chunk-O2ZHORSC.js → chunk-Z6RVBZQ4.js} +3 -3
  31. package/dist/{chunk-O2ZHORSC.js.map → chunk-Z6RVBZQ4.js.map} +1 -1
  32. package/dist/composer.cjs +9 -2
  33. package/dist/composer.d.cts +1 -1
  34. package/dist/composer.d.ts +1 -1
  35. package/dist/composer.js +4 -1
  36. package/dist/editor.cjs +1 -1
  37. package/dist/editor.js +1 -1
  38. package/dist/{index-C8PKWA_5.d.ts → index-CazpqSsk.d.cts} +103 -4
  39. package/dist/{index-C8PKWA_5.d.cts → index-CazpqSsk.d.ts} +103 -4
  40. package/dist/index.cjs +43 -39
  41. package/dist/index.d.cts +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +8 -8
  44. package/dist/inputs.cjs +3 -3
  45. package/dist/inputs.js +2 -2
  46. package/dist/json-editor.cjs +14 -14
  47. package/dist/json-editor.js +3 -3
  48. package/dist/prompt-input.cjs +3 -3
  49. package/dist/prompt-input.js +2 -2
  50. package/dist/qr-code.cjs +4 -4
  51. package/dist/qr-code.d.cts +38 -16
  52. package/dist/qr-code.d.ts +38 -16
  53. package/dist/qr-code.js +1 -1
  54. package/dist/select.cjs +3 -3
  55. package/dist/select.js +2 -2
  56. package/dist/tooltip.cjs +2 -2
  57. package/dist/tooltip.js +1 -1
  58. package/docs/QrCode.md +68 -29
  59. package/package.json +1 -1
  60. package/dist/chunk-23FTDEGR.cjs +0 -218
  61. package/dist/chunk-23FTDEGR.cjs.map +0 -1
  62. package/dist/chunk-2XAVAPDA.js +0 -215
  63. package/dist/chunk-2XAVAPDA.js.map +0 -1
  64. package/dist/chunk-35EN5AYE.js.map +0 -1
  65. package/dist/chunk-ISPCXIJZ.cjs.map +0 -1
package/dist/qr-code.d.ts CHANGED
@@ -91,7 +91,17 @@ declare function encodeQr(value: string, options?: EncodeOptions): QrSymbol;
91
91
  */
92
92
  interface QrCodeProps extends Omit<SVGAttributes<SVGSVGElement>, "children" | "viewBox" | "version"> {
93
93
  /**
94
- * What the code carries. Any string — a URL, a PIN, a token.
94
+ * What the code carries. **Any string** — this is not a link component.
95
+ *
96
+ * A QR code is a byte container, and most of what goes in one is not a web
97
+ * address: a serial or asset number, a batch or lot code, a ticket or order
98
+ * id, a WiFi credential block (`WIFI:T:WPA;S:ssid;P:pass;;`), a contact card
99
+ * (vCard / MeCard), a `tel:` / `mailto:` / `geo:` URI, a shipment code, a
100
+ * bare numeric reference, or arbitrary text. Several of those are the cases
101
+ * where a QR code earns its place, because they work with no network at all.
102
+ *
103
+ * The encoder picks the narrowest mode the value allows, so a digits-only
104
+ * value is a third of the size it would be as text — see {@link mode}.
95
105
  *
96
106
  * An empty string renders a valid (empty) symbol rather than throwing, so a
97
107
  * controlled surface can render before its value arrives.
@@ -111,15 +121,15 @@ interface QrCodeProps extends Omit<SVGAttributes<SVGSVGElement>, "children" | "v
111
121
  */
112
122
  errorCorrection?: QrErrorCorrection;
113
123
  /**
114
- * Accessible name. **Required**, and it must describe the DESTINATION rather
115
- * than the artefact: "Pair this device" tells a screen-reader user what the
116
- * code is for, where "QR code" tells them only that an image they cannot use
117
- * is present.
124
+ * Accessible name. **Required**, and it must describe WHAT THE CODE IS FOR
125
+ * rather than the artefact: "Asset tag for pump 14" or "Join the guest WiFi"
126
+ * tells a screen-reader user something useful, where "QR code" tells them
127
+ * only that an image they cannot use is present.
118
128
  *
119
- * It is not a substitute for the value being reachable another way. A QR code
129
+ * It is not a substitute for the value being available another way. A QR code
120
130
  * is unusable by anyone who cannot point a second device at this screen, so
121
- * the value should also appear as text or a link nearby — the `aria-label`
122
- * cannot carry a URL a user is expected to type.
131
+ * the value should also appear as selectable text nearby — the `aria-label`
132
+ * cannot carry a serial number or a password someone is expected to read off.
123
133
  */
124
134
  label: string;
125
135
  /**
@@ -175,25 +185,37 @@ interface QrCodeProps extends Omit<SVGAttributes<SVGSVGElement>, "children" | "v
175
185
  * scanner reading a blurred edge guesses. Vector output cannot have that
176
186
  * problem.
177
187
  *
188
+ * ## `value` is any string — this is not a link component
189
+ *
190
+ * A QR code is a byte container, and most of what belongs in one is not a web
191
+ * address: a serial or asset number, a lot code, a ticket id, a WiFi
192
+ * credential block, a contact card, a `tel:` or `geo:` URI, plain text. Those
193
+ * are the cases where the format earns its place, because they need no network
194
+ * at all — the scanner reads the value and that IS the payload.
195
+ *
178
196
  * ## It is an image of a value, not the value
179
197
  *
180
198
  * A QR code is unreadable to anyone without a second device pointed at the
181
199
  * screen — which includes every screen-reader user and anyone whose phone is
182
200
  * the device showing it. So `label` is required, and the value should ALSO be
183
- * reachable as text or a link beside it. A pairing panel that shows only a
184
- * code is a panel some people cannot use at all.
201
+ * selectable text beside it. A panel that shows only a code is one some people
202
+ * cannot use at all.
185
203
  *
186
204
  * @example
187
205
  * ```tsx
188
- * <QrCode
189
- * value="https://device.example.com/pair#482913"
190
- * size={192}
191
- * label="Pair this device"
192
- * />
206
+ * // Any string. A digits-only value is numeric mode and a third the size.
207
+ * <QrCode value="SN-4829-13" size={192} label="Asset tag for pump 14" />
208
+ * <QrCode value="482913" size={192} label="Order reference" />
209
+ *
210
+ * // A payload with no network in it at all.
211
+ * <QrCode value="WIFI:T:WPA;S:Guest;P:hunter2;;" label="Join the guest WiFi" />
212
+ *
213
+ * // A link is one payload among many, not the default case.
214
+ * <QrCode value="https://example.com/menu" label="Today's menu" />
193
215
  *
194
216
  * // On a dark surface, set both colours: a transparent background renders
195
217
  * // dark-on-dark and will not scan.
196
- * <QrCode value={url} label="Pair" foreground="#0b0b0c" background="#ffffff" />
218
+ * <QrCode value={serial} label="Asset tag" foreground="#0b0b0c" background="#ffffff" />
197
219
  * ```
198
220
  */
199
221
  declare const QrCode: react.ForwardRefExoticComponent<QrCodeProps & react.RefAttributes<SVGSVGElement>>;
package/dist/qr-code.js CHANGED
@@ -1,4 +1,4 @@
1
- export { QrCode, encodeQr, selectMode } from './chunk-ZDDWAVVZ.js';
1
+ export { QrCode, encodeQr, selectMode } from './chunk-RLE5WYY6.js';
2
2
  import './chunk-MMW2JGKZ.js';
3
3
  //# sourceMappingURL=qr-code.js.map
4
4
  //# sourceMappingURL=qr-code.js.map
package/dist/select.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  require('./chunk-HXLSEBAL.cjs');
4
- var chunkFXP4JLGU_cjs = require('./chunk-FXP4JLGU.cjs');
4
+ var chunkAR3JOPGT_cjs = require('./chunk-AR3JOPGT.cjs');
5
5
  require('./chunk-7EZBMGEE.cjs');
6
6
  require('./chunk-XKDW6B7X.cjs');
7
7
  require('./chunk-BBOIFV27.cjs');
8
8
  require('./chunk-YLGTDBB2.cjs');
9
- require('./chunk-J2DI6RMR.cjs');
10
9
  require('./chunk-KJ22KE56.cjs');
11
10
  require('./chunk-L3V3BYVS.cjs');
11
+ require('./chunk-J2DI6RMR.cjs');
12
12
  require('./chunk-NBZC6LOB.cjs');
13
13
  require('./chunk-57FQGQJK.cjs');
14
14
  require('./chunk-RXDRYWVD.cjs');
@@ -18,7 +18,7 @@ require('./chunk-ZU5Q77OT.cjs');
18
18
 
19
19
  Object.defineProperty(exports, "Select", {
20
20
  enumerable: true,
21
- get: function () { return chunkFXP4JLGU_cjs.Select; }
21
+ get: function () { return chunkAR3JOPGT_cjs.Select; }
22
22
  });
23
23
  //# sourceMappingURL=select.cjs.map
24
24
  //# sourceMappingURL=select.cjs.map
package/dist/select.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import './chunk-GNHQVPZP.js';
2
- export { Select } from './chunk-ZH3SPBVB.js';
2
+ export { Select } from './chunk-IECPLOSZ.js';
3
3
  import './chunk-PWEZCMIY.js';
4
4
  import './chunk-KKWNKBKR.js';
5
5
  import './chunk-5MU6X3KO.js';
6
6
  import './chunk-GFYSELJF.js';
7
- import './chunk-MQ6VGG62.js';
8
7
  import './chunk-OL53XYHF.js';
9
8
  import './chunk-NRXK42LO.js';
9
+ import './chunk-MQ6VGG62.js';
10
10
  import './chunk-WHEXTDPV.js';
11
11
  import './chunk-RGANGU7X.js';
12
12
  import './chunk-JXE5XID6.js';
package/dist/tooltip.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkISPCXIJZ_cjs = require('./chunk-ISPCXIJZ.cjs');
3
+ var chunkX7H75WVP_cjs = require('./chunk-X7H75WVP.cjs');
4
4
  require('./chunk-J2DI6RMR.cjs');
5
5
  require('./chunk-NBZC6LOB.cjs');
6
6
  require('./chunk-ZU5Q77OT.cjs');
@@ -9,7 +9,7 @@ require('./chunk-ZU5Q77OT.cjs');
9
9
 
10
10
  Object.defineProperty(exports, "Tooltip", {
11
11
  enumerable: true,
12
- get: function () { return chunkISPCXIJZ_cjs.Tooltip; }
12
+ get: function () { return chunkX7H75WVP_cjs.Tooltip; }
13
13
  });
14
14
  //# sourceMappingURL=tooltip.cjs.map
15
15
  //# sourceMappingURL=tooltip.cjs.map
package/dist/tooltip.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Tooltip } from './chunk-35EN5AYE.js';
1
+ export { Tooltip } from './chunk-CF5KRDN4.js';
2
2
  import './chunk-MQ6VGG62.js';
3
3
  import './chunk-WHEXTDPV.js';
4
4
  import './chunk-MMW2JGKZ.js';
package/docs/QrCode.md CHANGED
@@ -3,8 +3,8 @@
3
3
  A QR code rendered as an SVG, with **no dependencies** — the encoder is part of
4
4
  the component.
5
5
 
6
- ISO/IEC 18004, versions 1–40, error correction L/M/Q/H, with numeric,
7
- alphanumeric and byte modes selected automatically.
6
+ ISO/IEC 18004, versions 1–40, error correction L/M/Q/H, and numeric /
7
+ alphanumeric / byte modes selected automatically.
8
8
 
9
9
  ## Import
10
10
 
@@ -15,19 +15,46 @@ import { QrCode } from "@particle-academy/react-fancy";
15
15
  ## Basic Usage
16
16
 
17
17
  ```tsx
18
- <QrCode
19
- value="https://device.example.com/pair#482913"
20
- size={192}
21
- label="Pair this device"
22
- />
18
+ <QrCode value="SN-4829-13" size={192} label="Asset tag for pump 14" />
23
19
  ```
24
20
 
21
+ ## `value` is any string — this is not a link component
22
+
23
+ A QR code is a byte container. Most of what belongs in one is **not** a web
24
+ address, and those are the cases where the format earns its place, because they
25
+ need no network at all — the scanner reads the value and that *is* the payload.
26
+
27
+ ```tsx
28
+ // An identifier, read off a label in a warehouse with no signal.
29
+ <QrCode value="SN-4829-13" label="Asset tag for pump 14" />
30
+
31
+ // Digits only — numeric mode, a third the size of the same value as text.
32
+ <QrCode value="482913" label="Order reference" />
33
+
34
+ // WiFi credentials. Phone cameras offer "Join network" directly.
35
+ // Escape \ ; , : in the SSID and password with a backslash.
36
+ <QrCode value="WIFI:T:WPA;S:Guest;P:hunter2;;" label="Join the guest WiFi" />
37
+
38
+ // A contact, as MeCard — shorter than vCard and widely recognised.
39
+ <QrCode value="MECARD:N:Okafor,Ada;TEL:+15550100;EMAIL:ada@example.com;;" label="Ada Okafor's contact card" />
40
+
41
+ // Other URI schemes are just strings too.
42
+ <QrCode value="tel:+15550100" label="Call the service desk" />
43
+ <QrCode value="geo:51.5033,-0.1196" label="Meet at the south entrance" />
44
+
45
+ // A link is ONE payload among many, not the default.
46
+ <QrCode value="https://example.com/menu" label="Today's menu" />
47
+ ```
48
+
49
+ Nothing in the component inspects or validates `value`. It encodes the bytes
50
+ you give it, which is why it carries any of the above without a per-format API.
51
+
25
52
  ## Props
26
53
 
27
54
  | Prop | Type | Default | Description |
28
55
  |------|------|---------|-------------|
29
- | value | `string` | — | What the code carries. An empty string renders a valid empty symbol rather than throwing. |
30
- | label | `string` | — | **Required.** Accessible name. Describe the destination ("Pair this device"), not the artefact ("QR code"). |
56
+ | value | `string` | — | Any string. An empty string renders a valid empty symbol rather than throwing. |
57
+ | label | `string` | — | **Required.** Accessible name. Describe what the code is *for*, not the artefact. |
31
58
  | size | `number` | `192` | Rendered width and height in pixels. |
32
59
  | errorCorrection | `"L" \| "M" \| "Q" \| "H"` | `"M"` | Damage tolerance. See the note below — higher is not automatically better. |
33
60
  | quietZone | `number` | `4` | Modules of clear margin. The standard's minimum is 4. |
@@ -40,6 +67,23 @@ Every other SVG attribute passes through. `viewBox` and SVG's own `version` are
40
67
  not accepted — the component owns the first, and the second collides with the QR
41
68
  symbol version.
42
69
 
70
+ ## Mode selection is why a short value is a big symbol
71
+
72
+ Automatic, and worth understanding because it decides how scannable the result
73
+ is at a given pixel size:
74
+
75
+ | Value | Mode | Bits per character | Version at `M` |
76
+ |---|---|---|---|
77
+ | `482913` | numeric | 3⅓ | 1 (21 modules) |
78
+ | `SN-4829-13` | alphanumeric | 5½ | 1 (21 modules) |
79
+ | `sn-4829-13` | byte | 8 | 1 (21 modules) |
80
+ | a 47-character URL | byte | 8 | 4 (33 modules) |
81
+
82
+ Uppercase matters: alphanumeric mode covers `0-9 A-Z` and `$%*+-./: ` only, so
83
+ **lower-casing an identifier pushes it into byte mode** and makes the symbol
84
+ bigger for the same information. If you control the format of a code you are
85
+ printing, uppercase it.
86
+
43
87
  ## Three ways a perfect-looking code fails to scan
44
88
 
45
89
  None of these are visible in a screenshot review, which is why they are here
@@ -57,40 +101,30 @@ most scanners refuse it. Contrast is what gets scanned, not the dark modules on
57
101
  their own:
58
102
 
59
103
  ```tsx
60
- <QrCode value={url} label="Pair" foreground="#0b0b0c" background="#ffffff" />
104
+ <QrCode value={serial} label="Asset tag" foreground="#0b0b0c" background="#ffffff" />
61
105
  ```
62
106
 
63
107
  **3. Reaching for `"H"` because it sounds safer.** `"H"` survives about 30%
64
108
  damage against `"M"`'s 15%, but it holds a third less — so the same value needs
65
109
  a larger symbol, and at the same pixel width that means *smaller modules*. On a
66
110
  phone screen `"H"` can read worse than `"M"`. Raise it for print, for a label
67
- that will be scuffed, or when a logo covers part of the symbol.
111
+ that will be scuffed or oiled, or when a logo covers part of the symbol.
68
112
 
69
113
  ## It is an image of a value, not the value
70
114
 
71
115
  A QR code is unusable to anyone who cannot point a second device at the screen —
72
116
  every screen-reader user, and anyone whose phone is the device displaying it. So
73
- `label` is required, **and the value should also be reachable as text or a
74
- link** beside it. An `aria-label` cannot carry a URL someone is expected to
75
- type.
117
+ `label` is required, **and the value should also be selectable text** beside it.
118
+ An `aria-label` cannot carry a serial number or a WiFi password someone is
119
+ expected to read off.
76
120
 
77
121
  ```tsx
78
122
  <div className="flex flex-col items-center gap-3">
79
- <QrCode value={pairUrl} label="Pair this device" background="#ffffff" />
80
- <code className="text-sm">{pairUrl}</code>
123
+ <QrCode value={serial} label="Asset tag for pump 14" background="#ffffff" />
124
+ <code className="select-all text-sm">{serial}</code>
81
125
  </div>
82
126
  ```
83
127
 
84
- ## A PIN is denser than a URL
85
-
86
- Mode selection is automatic and it matters. Numeric mode packs three digits into
87
- ten bits where byte mode spends twenty-four, so a six-digit PIN is a version 1
88
- symbol — the largest modules and the easiest read:
89
-
90
- ```tsx
91
- <QrCode value="482913" label="Pairing PIN" /> {/* 21 modules + quiet zone */}
92
- ```
93
-
94
128
  ## Why SVG, and one path
95
129
 
96
130
  The symbol is one `<path>` of unit squares on a module-unit `viewBox`, not a
@@ -102,20 +136,22 @@ The module-unit viewBox is what makes `size` free: the symbol is resolution
102
136
  independent, so it stays crisp at any size and prints at the printer's
103
137
  resolution. That is not only cosmetic — a raster code scaled to a non-integer
104
138
  multiple of its module size has blurred module edges, and a scanner reading a
105
- blurred edge guesses.
139
+ blurred edge guesses. For a printed asset label this is the difference between
140
+ a code that reads first time and one a warehouse worker swears at.
106
141
 
107
142
  ## The encoder on its own
108
143
 
109
144
  Exported alongside the component, for a consumer that needs modules rather than
110
- an SVG — a canvas, a PDF, a thermal printer, a terminal:
145
+ an SVG — a canvas, a PDF, a thermal label printer, a terminal:
111
146
 
112
147
  ```tsx
113
148
  import { encodeQr, selectMode } from "@particle-academy/react-fancy";
114
149
 
115
- const symbol = encodeQr("https://example.com", { errorCorrection: "Q" });
150
+ const symbol = encodeQr("SN-4829-13", { errorCorrection: "Q" });
116
151
  symbol.modules; // boolean[][], true is dark
117
152
  symbol.size; // version * 4 + 17
118
153
  symbol.version; // 1-40
154
+ symbol.mode; // the mode chosen
119
155
  symbol.mask; // the mask chosen by penalty scoring
120
156
  selectMode("482913"); // "numeric"
121
157
  ```
@@ -125,6 +161,9 @@ fit. That is deliberate: a truncated symbol scans cleanly and yields the wrong
125
161
  string, which is silent corruption on a surface whose entire job is to carry a
126
162
  value accurately.
127
163
 
164
+ Capacity at the largest symbol (version 40, level `L`): **7,089** digits,
165
+ **4,296** alphanumeric characters, or **2,953** bytes.
166
+
128
167
  ## Not supported
129
168
 
130
169
  Kanji mode and structured append. Neither has a caller here, and an untested
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/react-fancy",
3
- "version": "5.36.2",
3
+ "version": "5.38.0",
4
4
  "description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,218 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkSNHU2AI4_cjs = require('./chunk-SNHU2AI4.cjs');
4
- var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
5
- var react = require('react');
6
- var jsxRuntime = require('react/jsx-runtime');
7
-
8
- function usePastePills({
9
- threshold,
10
- heldPastes: controlledHeldPastes,
11
- onHeldPastesChange
12
- } = {}) {
13
- const [heldPastes, setHeldPastes] = chunkSNHU2AI4_cjs.useControllableState(
14
- controlledHeldPastes,
15
- [],
16
- onHeldPastesChange
17
- );
18
- const nextId = react.useRef(0);
19
- const handlePaste = react.useCallback(
20
- (event) => {
21
- if (!threshold || threshold <= 0) return false;
22
- const text = event.clipboardData?.getData("text/plain") ?? "";
23
- if (text.length < threshold) return false;
24
- event.preventDefault();
25
- nextId.current += 1;
26
- const id = `paste-${nextId.current}`;
27
- setHeldPastes((current) => [...current, { id, text }]);
28
- return true;
29
- },
30
- [threshold, setHeldPastes]
31
- );
32
- const expand = react.useCallback(
33
- (id) => {
34
- const found = heldPastes.find((p) => p.id === id);
35
- if (found === void 0) return void 0;
36
- setHeldPastes((current) => current.filter((p) => p.id !== id));
37
- return found.text;
38
- },
39
- [heldPastes, setHeldPastes]
40
- );
41
- const remove = react.useCallback(
42
- (id) => setHeldPastes((current) => current.filter((p) => p.id !== id)),
43
- [setHeldPastes]
44
- );
45
- const clear = react.useCallback(() => setHeldPastes([]), [setHeldPastes]);
46
- return { heldPastes, handlePaste, expand, remove, clear };
47
- }
48
- var PREVIEW_CHARS = 20;
49
- function withSeparators(n) {
50
- return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
51
- }
52
- function endsOf(text) {
53
- if (text.length <= PREVIEW_CHARS * 2) return text;
54
- return `${text.slice(0, PREVIEW_CHARS)} \u2026 ${text.slice(-PREVIEW_CHARS)}`;
55
- }
56
- var Composer = react.forwardRef(
57
- function Composer2({
58
- value: controlledValue,
59
- defaultValue = "",
60
- onChange,
61
- onSubmit,
62
- placeholder = "Type a message...",
63
- actions,
64
- disabled = false,
65
- className,
66
- pasteThreshold,
67
- heldPastes: controlledHeldPastes,
68
- onHeldPastesChange
69
- }, ref) {
70
- const [value, setValue] = chunkSNHU2AI4_cjs.useControllableState(
71
- controlledValue,
72
- defaultValue,
73
- onChange
74
- );
75
- const pills = usePastePills({
76
- threshold: pasteThreshold,
77
- heldPastes: controlledHeldPastes,
78
- onHeldPastesChange
79
- });
80
- const heldPastes = pills.heldPastes;
81
- const textareaRef = react.useRef(null);
82
- const handlePaste = react.useCallback(
83
- (e) => {
84
- pills.handlePaste(e);
85
- },
86
- [pills]
87
- );
88
- const composeOutgoing = react.useCallback(
89
- () => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join("\n\n"),
90
- [value, heldPastes]
91
- );
92
- const hasContent = value.trim().length > 0 || heldPastes.length > 0;
93
- const handleSubmit = react.useCallback(() => {
94
- if (!hasContent || disabled) return;
95
- onSubmit?.(composeOutgoing());
96
- setValue("");
97
- pills.clear();
98
- textareaRef.current?.focus();
99
- }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, pills]);
100
- const handleKeyDown = (e) => {
101
- if (e.key === "Enter" && !e.shiftKey) {
102
- e.preventDefault();
103
- handleSubmit();
104
- }
105
- };
106
- const insertPaste = react.useCallback(
107
- (paste) => {
108
- const text = pills.expand(paste.id);
109
- if (text === void 0) return;
110
- setValue((current) => current ? `${current}
111
-
112
- ${text}` : text);
113
- textareaRef.current?.focus();
114
- },
115
- [pills, setValue]
116
- );
117
- return /* @__PURE__ */ jsxRuntime.jsxs(
118
- "div",
119
- {
120
- "data-react-fancy-composer": "",
121
- ref,
122
- className: chunkZU5Q77OT_cjs.cn(
123
- "rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
124
- disabled && "opacity-50",
125
- className
126
- ),
127
- children: [
128
- heldPastes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
129
- "div",
130
- {
131
- "data-react-fancy-composer-paste-pills": "",
132
- className: "flex flex-wrap gap-1.5 px-3 pt-3",
133
- children: heldPastes.map((paste) => (
134
- // `group` + `group-hover` rather than hover state: the preview is
135
- // presentation, and a useState for it would be one more thing an
136
- // agent could desynchronise.
137
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "group relative inline-flex", children: [
138
- /* @__PURE__ */ jsxRuntime.jsx(
139
- "button",
140
- {
141
- type: "button",
142
- "data-react-fancy-composer-paste-pill": paste.id,
143
- onClick: () => insertPaste(paste),
144
- disabled,
145
- "aria-label": `Insert this pasted text, ${withSeparators(paste.text.length)} characters`,
146
- "aria-describedby": `${paste.id}-preview`,
147
- className: "inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700",
148
- children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
149
- "Pasted text \xB7 ",
150
- withSeparators(paste.text.length),
151
- " characters"
152
- ] })
153
- }
154
- ),
155
- /* @__PURE__ */ jsxRuntime.jsx(
156
- "button",
157
- {
158
- type: "button",
159
- "data-react-fancy-composer-paste-pill-remove": paste.id,
160
- onClick: () => pills.remove(paste.id),
161
- disabled,
162
- "aria-label": "Discard this pasted text",
163
- className: "absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100",
164
- children: "\xD7"
165
- }
166
- ),
167
- /* @__PURE__ */ jsxRuntime.jsx(
168
- "span",
169
- {
170
- id: `${paste.id}-preview`,
171
- "data-react-fancy-composer-paste-pill-preview": paste.id,
172
- role: "tooltip",
173
- className: "pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900",
174
- children: endsOf(paste.text)
175
- }
176
- )
177
- ] }, paste.id)
178
- ))
179
- }
180
- ),
181
- /* @__PURE__ */ jsxRuntime.jsx(
182
- "textarea",
183
- {
184
- ref: textareaRef,
185
- value,
186
- onChange: (e) => setValue(e.target.value),
187
- onKeyDown: handleKeyDown,
188
- onPaste: handlePaste,
189
- placeholder,
190
- disabled,
191
- rows: 3,
192
- className: "w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400"
193
- }
194
- ),
195
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800", children: [
196
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: actions }),
197
- /* @__PURE__ */ jsxRuntime.jsx(
198
- "button",
199
- {
200
- type: "button",
201
- onClick: handleSubmit,
202
- disabled: disabled || !hasContent,
203
- className: "rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100",
204
- children: "Send"
205
- }
206
- )
207
- ] })
208
- ]
209
- }
210
- );
211
- }
212
- );
213
- Composer.displayName = "Composer";
214
-
215
- exports.Composer = Composer;
216
- exports.usePastePills = usePastePills;
217
- //# sourceMappingURL=chunk-23FTDEGR.cjs.map
218
- //# sourceMappingURL=chunk-23FTDEGR.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-paste-pills.ts","../src/components/Composer/Composer.tsx"],"names":["useControllableState","useRef","useCallback","forwardRef","Composer","jsxs","cn","jsx"],"mappings":"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","file":"chunk-23FTDEGR.cjs","sourcesContent":["import { useCallback, useRef } from \"react\";\nimport { useControllableState } from \"./use-controllable-state\";\n\n/**\n * A paste held beside a composer rather than inserted into it.\n *\n * There is deliberately **no `length` field.** It would be `text.length` and\n * nothing else, and a derived value stored beside its source is a value that can\n * disagree with it — the two drift the moment anything edits one. Read\n * `paste.text.length`.\n */\nexport interface HeldPaste {\n id: string;\n text: string;\n}\n\n/**\n * The minimum of a clipboard event this hook touches.\n *\n * Structural rather than `React.ClipboardEvent` so a host can call it from a\n * native listener, a synthetic one, or a test double. React's own event satisfies\n * it.\n */\nexport interface PasteEventLike {\n clipboardData?: { getData(type: string): string } | null;\n preventDefault(): void;\n}\n\nexport interface UsePastePillsOptions {\n /**\n * Hold a paste of at least this many characters. Try 500 — about a long\n * paragraph.\n *\n * **No default, on purpose.** Paste is a fundamental interaction, so holding one\n * is something a host asks for rather than something it discovers. Omit this and\n * `handlePaste` always returns `false` and touches nothing.\n */\n threshold?: number;\n /** Own the list yourself. Omit to let the hook keep it. */\n heldPastes?: HeldPaste[];\n onHeldPastesChange?: (pastes: HeldPaste[]) => void;\n}\n\nexport interface UsePastePillsResult {\n heldPastes: HeldPaste[];\n /**\n * Call this from your OWN paste handler. Returns `true` if it took the paste —\n * in which case it has already called `preventDefault()` — and `false` if it\n * declined, having touched nothing.\n *\n * Composable on purpose: a host whose handler already intercepts paste for its\n * own reasons needs to ask \"did you want this?\" rather than hand over the event.\n *\n * function onPaste(e) {\n * if (pills.handlePaste(e)) return;\n * // ...your own handling\n * }\n */\n handlePaste: (event: PasteEventLike) => boolean;\n /**\n * Removes the paste and returns its text, for the host to insert into its own\n * surface. `undefined` if there is no such id.\n *\n * The hook cannot insert it — it owns no DOM and does not know what a host's\n * surface is. That is the point.\n */\n expand: (id: string) => string | undefined;\n /** Discard one without returning it. */\n remove: (id: string) => void;\n /** Empty the list, e.g. after a send. */\n clear: () => void;\n}\n\n/**\n * Hold a large paste as state, so a composer can show it as a pill instead of\n * turning itself into a scrolling wall.\n *\n * ---------------------------------------------------------------------------\n * Why this is a hook and not just a feature of `Composer`\n * ---------------------------------------------------------------------------\n *\n * `Composer` shipped the behaviour in 5.31.0 and the MOIC estate, who specified it,\n * then established something I had missed: their chat input is a contenteditable\n * rendering inline `[deal:42|Acme]` badges, so they **cannot** adopt `Composer` (a\n * plain textarea) however much they want its behaviour. Their plan to delete their\n * own composer when ours landed could never have worked.\n *\n * So the logic lives here and **`Composer` is its first caller.** One\n * implementation rather than two, which matters more than the convenience: two\n * copies of a rule drift, and the copy nobody reads is the one that rots. The rule\n * that must not drift is the one in `Composer`'s own tests — a send with pills\n * still closed carries the text.\n *\n * It owns no DOM. The host renders the pills.\n *\n * ---------------------------------------------------------------------------\n * Who actually calls this (as of 5.32.0): `Composer`, and nothing else\n * ---------------------------------------------------------------------------\n *\n * The estate that specified it **withdrew before adopting it**, hours after it\n * shipped — their design moved the pill INTO the message as a `[paste:id|label]`\n * marker in the same grammar as their other object tags, which makes the message\n * itself the record of what is attached. A parallel `heldPastes` array would then be\n * a second place answering \"what am I sending?\", and the two would disagree the\n * moment someone backspaced a badge. Their reasoning was better than the spec.\n *\n * Recorded because a reader should not infer an extension point under active\n * external use. It is kept rather than withdrawn for two reasons that are about\n * cost, not optimism: it is published API on a 5.x package, so removing it needs a\n * major; and `Composer` calls it, so it is exercised by that component's tests on\n * every run and cannot rot unnoticed.\n *\n * **It gets no further surface without a real caller.** No extra options, no second\n * hook. An API with one hypothetical consumer is how a package accumulates surface\n * nobody uses.\n */\nexport function usePastePills({\n threshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n}: UsePastePillsOptions = {}): UsePastePillsResult {\n const [heldPastes, setHeldPastes] = useControllableState<HeldPaste[]>(\n controlledHeldPastes,\n [],\n onHeldPastesChange,\n );\n\n /*\n * A counter rather than `crypto.randomUUID()` or an index.\n *\n * An index is not an id — removing the first pill renumbers the rest, and a host\n * keying its DOM on that remounts every pill and loses hover state. A counter in\n * a ref is stable for the life of the component and needs no platform API that a\n * server renderer might lack.\n */\n const nextId = useRef(0);\n\n const handlePaste = useCallback(\n (event: PasteEventLike): boolean => {\n if (!threshold || threshold <= 0) return false;\n\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n\n // Below the threshold, decline WITHOUT touching the event. A host calling\n // this from inside its own handler has to be able to carry on as if the hook\n // were not there.\n if (text.length < threshold) return false;\n\n event.preventDefault();\n nextId.current += 1;\n const id = `paste-${nextId.current}`;\n setHeldPastes((current) => [...current, { id, text }]);\n\n return true;\n },\n [threshold, setHeldPastes],\n );\n\n const expand = useCallback(\n (id: string): string | undefined => {\n const found = heldPastes.find((p) => p.id === id);\n\n if (found === undefined) return undefined;\n\n setHeldPastes((current) => current.filter((p) => p.id !== id));\n\n return found.text;\n },\n [heldPastes, setHeldPastes],\n );\n\n const remove = useCallback(\n (id: string) => setHeldPastes((current) => current.filter((p) => p.id !== id)),\n [setHeldPastes],\n );\n\n const clear = useCallback(() => setHeldPastes([]), [setHeldPastes]);\n\n return { heldPastes, handlePaste, expand, remove, clear };\n}\n","import { forwardRef, useCallback, useRef } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useControllableState } from \"../../hooks/use-controllable-state\";\nimport { usePastePills, type HeldPaste } from \"../../hooks/use-paste-pills\";\nimport type { ComposerProps } from \"./Composer.types\";\n\n/** Characters of a held paste shown at each end of the hover preview. */\nconst PREVIEW_CHARS = 20;\n\n/**\n * Thousands separators, written out rather than left to `toLocaleString()`.\n *\n * The number exists so someone can size the paste at a glance, and `2,036` sizes\n * faster than `2036`. It is not left to the runtime locale because the label beside\n * it (\"Pasted text\") is not localised either, so a locale-formatted number next to\n * an English noun would be half a translation.\n */\nfunction withSeparators(n: number): string {\n return String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n}\n\n/** First and last N characters, middle elided. The ends say WHICH paste this is. */\nfunction endsOf(text: string): string {\n if (text.length <= PREVIEW_CHARS * 2) return text;\n\n return `${text.slice(0, PREVIEW_CHARS)} … ${text.slice(-PREVIEW_CHARS)}`;\n}\n\nexport const Composer = forwardRef<HTMLDivElement, ComposerProps>(\n function Composer(\n {\n value: controlledValue,\n defaultValue = \"\",\n onChange,\n onSubmit,\n placeholder = \"Type a message...\",\n actions,\n disabled = false,\n className,\n pasteThreshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n },\n ref,\n ) {\n const [value, setValue] = useControllableState(\n controlledValue,\n defaultValue,\n onChange,\n );\n /*\n * The holding logic lives in `usePastePills` and this is its first caller.\n *\n * It moved out in 5.32.0 because the estate that specified the feature cannot\n * use this component at all — their chat input is a contenteditable rendering\n * inline tag badges, which a textarea cannot do. One implementation rather than\n * two: two copies of a rule drift, and the copy nobody reads is the one that\n * rots. The rule that must not drift is the one below — a send with pills still\n * closed carries the text.\n */\n const pills = usePastePills({\n threshold: pasteThreshold,\n heldPastes: controlledHeldPastes,\n onHeldPastesChange,\n });\n const heldPastes = pills.heldPastes;\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handlePaste = useCallback(\n (e: React.ClipboardEvent<HTMLTextAreaElement>) => {\n // Composable by design, so this reads as a question rather than a handover.\n // Nothing else here wants the paste, so the answer is simply ignored.\n pills.handlePaste(e);\n },\n [pills],\n );\n\n /** Everything the person believes they are sending, in the order they built it. */\n const composeOutgoing = useCallback(\n () => [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join(\"\\n\\n\"),\n [value, heldPastes],\n );\n\n const hasContent = value.trim().length > 0 || heldPastes.length > 0;\n\n const handleSubmit = useCallback(() => {\n if (!hasContent || disabled) return;\n\n /*\n * THE DANGEROUS STATE, and the reason this feature needed a spec rather than\n * a guess. A pill reads as \"your paste is attached\" — so a send that drops it\n * means asking a question about a document without the document, and nobody\n * can tell: the sender believes it went, the receiver sees a question about\n * nothing.\n *\n * Held pastes go out in arrival order, after what was typed, because people\n * type the question BEFORE pasting the thing it is about.\n */\n onSubmit?.(composeOutgoing());\n setValue(\"\");\n pills.clear();\n textareaRef.current?.focus();\n }, [hasContent, disabled, onSubmit, composeOutgoing, setValue, pills]);\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n /** Click a pill: dump it into the input, APPENDED to what is already typed. */\n const insertPaste = useCallback(\n (paste: HeldPaste) => {\n // `expand` removes it and hands back the text; inserting is this\n // component's job because the hook owns no DOM.\n const text = pills.expand(paste.id);\n\n if (text === undefined) return;\n\n setValue((current) => (current ? `${current}\\n\\n${text}` : text));\n textareaRef.current?.focus();\n },\n [pills, setValue],\n );\n\n return (\n <div\n data-react-fancy-composer=\"\"\n ref={ref}\n className={cn(\n \"rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {heldPastes.length > 0 && (\n <div\n data-react-fancy-composer-paste-pills=\"\"\n className=\"flex flex-wrap gap-1.5 px-3 pt-3\"\n >\n {heldPastes.map((paste) => (\n // `group` + `group-hover` rather than hover state: the preview is\n // presentation, and a useState for it would be one more thing an\n // agent could desynchronise.\n <span key={paste.id} className=\"group relative inline-flex\">\n <button\n type=\"button\"\n data-react-fancy-composer-paste-pill={paste.id}\n onClick={() => insertPaste(paste)}\n disabled={disabled}\n // No `title`. A native tooltip plus the hover preview below\n // renders TWO tooltips on the same hover, at different delays.\n // The label says what clicking does; the preview says what the\n // text is, and it is linked rather than merely adjacent so a\n // screen reader reaches it too.\n aria-label={`Insert this pasted text, ${withSeparators(paste.text.length)} characters`}\n aria-describedby={`${paste.id}-preview`}\n // `pr-7` leaves room for the absolutely-positioned × that sits\n // over this button's right edge. With a smaller right padding the\n // × lands on top of the label.\n className=\"inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700\"\n >\n <span>\n Pasted text · {withSeparators(paste.text.length)} characters\n </span>\n </button>\n\n <button\n type=\"button\"\n data-react-fancy-composer-paste-pill-remove={paste.id}\n onClick={() => pills.remove(paste.id)}\n disabled={disabled}\n aria-label=\"Discard this pasted text\"\n className=\"absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100\"\n >\n ×\n </button>\n\n <span\n id={`${paste.id}-preview`}\n data-react-fancy-composer-paste-pill-preview={paste.id}\n role=\"tooltip\"\n className=\"pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900\"\n >\n {endsOf(paste.text)}\n </span>\n </span>\n ))}\n </div>\n )}\n\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n placeholder={placeholder}\n disabled={disabled}\n rows={3}\n className=\"w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400\"\n />\n <div className=\"flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800\">\n <div className=\"flex items-center gap-1\">{actions}</div>\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={disabled || !hasContent}\n className=\"rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100\"\n >\n Send\n </button>\n </div>\n </div>\n );\n },\n);\n\nComposer.displayName = \"Composer\";\n"]}