@versini/ui-bubble 10.2.4 → 11.0.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/README.md CHANGED
@@ -13,11 +13,12 @@ The Bubble component provides chat-style message bubbles with support for footer
13
13
  - [Installation](#installation)
14
14
  - [Usage](#usage)
15
15
  - [API](#api)
16
+ - [Migration from v10](#migration-from-v10)
16
17
 
17
18
  ## Features
18
19
 
19
20
  - **🎯 Chat Bubbles**: Left and right-aligned message bubbles with optional tails
20
- - **📋 Copy Functionality**: Built-in copy-to-clipboard with custom copy handling
21
+ - **📋 Flexible Actions**: Customizable action slot with built-in copy-to-clipboard component
21
22
  - **📊 Footer Support**: Structured footer with key-value pairs or raw JSX
22
23
  - **♿ Accessible**: Keyboard navigation and screen reader support
23
24
  - **🎨 Customizable**: Multiple styling options and theme support
@@ -78,40 +79,122 @@ function App() {
78
79
 
79
80
  ### Copy to Clipboard
80
81
 
82
+ The Bubble component uses an `action` prop that accepts a React node. For copy-to-clipboard functionality, use the `BubbleCopy` component:
83
+
81
84
  ```tsx
82
85
  import { Bubble } from "@versini/ui-bubble/bubble";
86
+ import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
83
87
 
84
88
  function App() {
85
89
  return (
86
90
  <div className="space-y-4">
87
- {/* Simple copy - copies the bubble content */}
88
- <Bubble kind="left" copyToClipboard>
91
+ {/* Simple copy - pass the text to copy as children */}
92
+ <Bubble
93
+ kind="left"
94
+ action={
95
+ <BubbleCopy>Click the copy icon to copy this message.</BubbleCopy>
96
+ }
97
+ >
89
98
  Click the copy icon to copy this message.
90
99
  </Bubble>
91
100
 
92
- {/* Custom copy text */}
93
- <Bubble kind="left" copyToClipboard="Custom text to copy">
94
- This will copy custom text instead of the bubble content.
95
- </Bubble>
96
-
97
- {/* Custom copy function */}
101
+ {/* With custom copy button styling */}
98
102
  <Bubble
99
- kind="left"
100
- copyToClipboard={(text) => {
101
- navigator.clipboard.writeText(`Copied: ${text}`);
102
- }}
103
+ kind="right"
104
+ action={
105
+ <BubbleCopy mode="dark" focusMode="light">
106
+ Copy button with custom theme.
107
+ </BubbleCopy>
108
+ }
103
109
  >
104
- This uses a custom copy function.
110
+ Copy button with custom theme.
105
111
  </Bubble>
106
112
  </div>
107
113
  );
108
114
  }
109
115
  ```
110
116
 
117
+ ### Rich Text Copy
118
+
119
+ When you need to preserve formatting (headings, lists, bold text) when pasting into applications like Microsoft Word or Google Docs, use the `richText` prop:
120
+
121
+ ```tsx
122
+ import { Bubble } from "@versini/ui-bubble/bubble";
123
+ import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
124
+
125
+ function App() {
126
+ return (
127
+ <Bubble
128
+ kind="left"
129
+ action={
130
+ <BubbleCopy richText>
131
+ <h2>Recipe</h2>
132
+ <p>
133
+ A delicious <strong>chocolate cake</strong> with{" "}
134
+ <em>vanilla frosting</em>.
135
+ </p>
136
+ <ul>
137
+ <li>2 cups flour</li>
138
+ <li>1 cup sugar</li>
139
+ <li>3 eggs</li>
140
+ </ul>
141
+ </BubbleCopy>
142
+ }
143
+ >
144
+ <h2>Recipe</h2>
145
+ <p>
146
+ A delicious <strong>chocolate cake</strong> with{" "}
147
+ <em>vanilla frosting</em>.
148
+ </p>
149
+ <ul>
150
+ <li>2 cups flour</li>
151
+ <li>1 cup sugar</li>
152
+ <li>3 eggs</li>
153
+ </ul>
154
+ </Bubble>
155
+ );
156
+ }
157
+ ```
158
+
159
+ When `richText` is enabled, the clipboard will contain both HTML and plain text formats. Applications that support rich text (Word, Docs, email clients) will paste the formatted version, while plain text editors (Notepad, terminals) will receive the plain text fallback.
160
+
161
+ ### Custom Actions
162
+
163
+ The `action` prop gives you full control over what appears next to the bubble. You can use it for custom copy behavior, dropdown menus, or any other interactive elements:
164
+
165
+ ```tsx
166
+ import { Bubble } from "@versini/ui-bubble/bubble";
167
+
168
+ function App() {
169
+ const text = "This bubble has custom action buttons.";
170
+ return (
171
+ <Bubble
172
+ kind="left"
173
+ action={
174
+ <div className="flex gap-2">
175
+ <button
176
+ type="button"
177
+ onClick={() => navigator.clipboard.writeText(text)}
178
+ >
179
+ Copy
180
+ </button>
181
+ <button type="button" onClick={() => console.info("Share:", text)}>
182
+ Share
183
+ </button>
184
+ </div>
185
+ }
186
+ >
187
+ {text}
188
+ </Bubble>
189
+ );
190
+ }
191
+ ```
192
+
111
193
  ### Chat Interface
112
194
 
113
195
  ```tsx
114
196
  import { Bubble } from "@versini/ui-bubble/bubble";
197
+ import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
115
198
 
116
199
  function ChatExample() {
117
200
  const messages = [
@@ -138,7 +221,7 @@ function ChatExample() {
138
221
  kind={message.kind}
139
222
  tail
140
223
  footer={[{ key: "Time", value: message.time }]}
141
- copyToClipboard
224
+ action={<BubbleCopy>{message.text}</BubbleCopy>}
142
225
  >
143
226
  {message.text}
144
227
  </Bubble>
@@ -219,67 +302,30 @@ function CustomWidthExample() {
219
302
  }
220
303
  ```
221
304
 
222
- ### Copy Functionality Variations
223
-
224
- ```tsx
225
- import { Bubble } from "@versini/ui-bubble/bubble";
226
-
227
- function CopyFunctionalityExample() {
228
- const handleCustomCopy = (text: any) => {
229
- // Custom copy logic
230
- const copyText = `Shared message: "${text}"`;
231
- navigator.clipboard.writeText(copyText);
232
- console.log("Copied with custom format:", copyText);
233
- };
234
-
235
- return (
236
- <div className="space-y-4">
237
- {/* Boolean - copies bubble content */}
238
- <Bubble kind="left" copyToClipboard={true}>
239
- Basic copy functionality
240
- </Bubble>
241
-
242
- {/* String - copies specific text */}
243
- <Bubble kind="left" copyToClipboard="contact@example.com">
244
- Click to copy: contact@example.com
245
- </Bubble>
246
-
247
- {/* Function - custom copy behavior */}
248
- <Bubble kind="left" copyToClipboard={handleCustomCopy}>
249
- Custom copy behavior with formatting
250
- </Bubble>
251
-
252
- {/* With custom copy button styling */}
253
- <Bubble
254
- kind="right"
255
- copyToClipboard
256
- copyToClipboardMode="dark"
257
- copyToClipboardFocusMode="light"
258
- >
259
- Copy button with custom theme
260
- </Bubble>
261
- </div>
262
- );
263
- }
264
- ```
265
-
266
305
  ## API
267
306
 
268
307
  ### Bubble Props
269
308
 
270
- | Prop | Type | Default | Description |
271
- | ------------------------ | ----------------------------------------------- | ---------- | ------------------------------------------------------- |
272
- | children | `React.ReactNode` | - | The text to render in the bubble |
273
- | kind | `"left" \| "right"` | `"left"` | The type of Bubble (changes color and chevron location) |
274
- | tail | `boolean` | `false` | Whether or not the Bubble should have a tail |
275
- | copyToClipboard | `boolean \| string \| ((text: any) => void)` | - | Copy functionality configuration |
276
- | footer | `BubbleFooter` (see below) | - | Array of footer items for the Bubble |
277
- | rawFooter | `React.ReactNode` | - | Same as "footer" but accepts raw JSX |
278
- | noMaxWidth | `boolean` | `false` | Whether to disable default responsive max-width |
279
- | className | `string` | - | CSS class(es) to add to the main component wrapper |
280
- | contentClassName | `string` | - | CSS class(es) to add to the content wrapper |
281
- | copyToClipboardMode | `"dark" \| "light" \| "system" \| "alt-system"` | `"system"` | The mode of Copy Button |
282
- | copyToClipboardFocusMode | `"dark" \| "light" \| "system" \| "alt-system"` | `"system"` | The focus mode for the Copy Button |
309
+ | Prop | Type | Default | Description |
310
+ | ---------------- | -------------------------- | -------- | ------------------------------------------------------- |
311
+ | children | `React.ReactNode` | - | The text to render in the bubble |
312
+ | kind | `"left" \| "right"` | `"left"` | The type of Bubble (changes color and chevron location) |
313
+ | tail | `boolean` | `false` | Whether or not the Bubble should have a tail |
314
+ | action | `React.ReactNode` | - | Action slot content (e.g., BubbleCopy) |
315
+ | footer | `BubbleFooter` (see below) | - | Array of footer items for the Bubble |
316
+ | rawFooter | `React.ReactNode` | - | Same as "footer" but accepts raw JSX |
317
+ | noMaxWidth | `boolean` | `false` | Whether to disable default responsive max-width |
318
+ | className | `string` | - | CSS class(es) to add to the main component wrapper |
319
+ | contentClassName | `string` | - | CSS class(es) to add to the content wrapper |
320
+
321
+ ### BubbleCopy Props
322
+
323
+ | Prop | Type | Default | Description |
324
+ | --------- | ----------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- |
325
+ | children | `React.ReactNode` | - | The content to copy (string or JSX) |
326
+ | richText | `boolean` | `false` | When true, copies as HTML + plain text. Preserves formatting when pasting into Word, Google Docs, etc |
327
+ | mode | `"dark" \| "light" \| "system" \| "alt-system"` | `"system"` | The mode of the Copy Button |
328
+ | focusMode | `"dark" \| "light" \| "system" \| "alt-system"` | `"system"` | The focus mode for the Button |
283
329
 
284
330
  ### Footer Types
285
331
 
@@ -304,3 +350,71 @@ BUBBLE_FOOTER_EMPTY;
304
350
  ### Special Values
305
351
 
306
352
  - `BUBBLE_FOOTER_EMPTY` - Import from `@versini/ui-bubble/constants` to create an empty footer row that maintains height
353
+
354
+ ## Migration from v10
355
+
356
+ Version 11 introduces a breaking change to the copy-to-clipboard functionality. The `copyToClipboard`, `copyToClipboardMode`, and `copyToClipboardFocusMode` props have been replaced with a more flexible `action` prop and a separate `BubbleCopy` component.
357
+
358
+ ### Before (v10)
359
+
360
+ ```tsx
361
+ import { Bubble } from "@versini/ui-bubble/bubble";
362
+
363
+ // Simple copy
364
+ <Bubble copyToClipboard>Content</Bubble>
365
+
366
+ // With styling
367
+ <Bubble
368
+ copyToClipboard
369
+ copyToClipboardMode="dark"
370
+ copyToClipboardFocusMode="light"
371
+ >
372
+ Content
373
+ </Bubble>
374
+
375
+ // Custom copy text
376
+ <Bubble copyToClipboard="custom text">Content</Bubble>
377
+
378
+ // Custom copy function
379
+ <Bubble copyToClipboard={(text) => customCopy(text)}>Content</Bubble>
380
+ ```
381
+
382
+ ### After (v11)
383
+
384
+ ```tsx
385
+ import { Bubble } from "@versini/ui-bubble/bubble";
386
+ import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
387
+
388
+ // Simple copy - pass text to copy as children
389
+ <Bubble action={<BubbleCopy>Content</BubbleCopy>}>
390
+ Content
391
+ </Bubble>
392
+
393
+ // With styling
394
+ <Bubble action={<BubbleCopy mode="dark" focusMode="light">Content</BubbleCopy>}>
395
+ Content
396
+ </Bubble>
397
+
398
+ // Custom copy text
399
+ <Bubble action={<BubbleCopy>custom text</BubbleCopy>}>
400
+ Content
401
+ </Bubble>
402
+
403
+ // Custom copy function - now you have full control!
404
+ <Bubble
405
+ action={
406
+ <button type="button" onClick={() => customCopy("Content")}>
407
+ Copy
408
+ </button>
409
+ }
410
+ >
411
+ Content
412
+ </Bubble>
413
+ ```
414
+
415
+ ### Key Changes
416
+
417
+ 1. **New import**: Add `import { BubbleCopy } from "@versini/ui-bubble/bubble-copy"`
418
+ 2. **Replace props**: Change `copyToClipboard` to `action={<BubbleCopy>text to copy</BubbleCopy>}`
419
+ 3. **Styling props**: Move `copyToClipboardMode` → `mode` and `copyToClipboardFocusMode` → `focusMode` on `BubbleCopy`
420
+ 4. **Full flexibility**: The `action` prop now accepts any React node, enabling custom dropdown menus, multiple buttons, or any other UI
@@ -1,6 +1,6 @@
1
1
  /*!
2
- @versini/ui-bubble v10.2.4
3
- © 2025 gizmette.com
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
6
 
@@ -1,3 +1,3 @@
1
1
  import type { BubbleProps } from "./BubbleTypes";
2
2
  export type { BubbleFooter, BubbleFooterItem, BubbleFooterKeyValue, BubbleFooterValueOnly, BubbleProps, } from "./BubbleTypes";
3
- export declare const Bubble: ({ children, kind, className, contentClassName, footerClassName, footer, rawFooter, copyToClipboard, copyToClipboardFocusMode, copyToClipboardMode, noMaxWidth, tail, gradient, }: BubbleProps) => import("react/jsx-runtime").JSX.Element;
3
+ export declare const Bubble: ({ children, kind, className, contentClassName, footerClassName, footer, rawFooter, action, noMaxWidth, tail, gradient, }: BubbleProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,30 +1,18 @@
1
1
  /*!
2
- @versini/ui-bubble v10.2.4
3
- © 2025 gizmette.com
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { ButtonIcon } from "@versini/ui-button/button-icon";
8
- import { IconCopied, IconCopy } from "@versini/ui-icons";
9
- import { useEffect, useState } from "react";
10
7
  import { getBubbleClasses } from "./utilities.js";
11
8
 
12
9
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
13
10
 
14
- ;// CONCATENATED MODULE: external "@versini/ui-button/button-icon"
15
-
16
- ;// CONCATENATED MODULE: external "@versini/ui-icons"
17
-
18
- ;// CONCATENATED MODULE: external "react"
19
-
20
11
  ;// CONCATENATED MODULE: external "./utilities.js"
21
12
 
22
13
  ;// CONCATENATED MODULE: ./src/components/Bubble/Bubble.tsx
23
14
 
24
15
 
25
-
26
-
27
-
28
16
  /**
29
17
  * Type guard to check if a footer item is the BUBBLE_FOOTER_EMPTY constant.
30
18
  */ const isFooterEmpty = (item)=>{
@@ -35,8 +23,7 @@ import { getBubbleClasses } from "./utilities.js";
35
23
  */ const isFooterKeyValue = (item)=>{
36
24
  return typeof item === "object" && item !== null && "key" in item && "value" in item;
37
25
  };
38
- const Bubble = ({ children, kind = "left", className, contentClassName, footerClassName, footer, rawFooter, copyToClipboard, copyToClipboardFocusMode = "system", copyToClipboardMode = "system", noMaxWidth = false, tail = false, gradient })=>{
39
- const [copied, setCopied] = useState(false);
26
+ const Bubble = ({ children, kind = "left", className, contentClassName, footerClassName, footer, rawFooter, action, noMaxWidth = false, tail = false, gradient })=>{
40
27
  const bubbleClasses = getBubbleClasses({
41
28
  kind,
42
29
  className,
@@ -46,32 +33,6 @@ const Bubble = ({ children, kind = "left", className, contentClassName, footerCl
46
33
  tail,
47
34
  gradient
48
35
  });
49
- const isCopyToClipboardEnabled = Boolean(copyToClipboard) && (typeof copyToClipboard === "function" || typeof copyToClipboard === "string" || typeof children === "string");
50
- // copy to clipboard function
51
- /* v8 ignore start - clipboard edge cases */ const handleCopyToClipboard = ()=>{
52
- setCopied(true);
53
- if (typeof copyToClipboard === "function") {
54
- copyToClipboard(children);
55
- } else if (typeof copyToClipboard === "string") {
56
- navigator.clipboard.writeText(copyToClipboard);
57
- } else if (typeof children === "string") {
58
- navigator.clipboard.writeText(children);
59
- }
60
- };
61
- /* v8 ignore stop */ // after 3 seconds, reset the copied state
62
- useEffect(()=>{
63
- let timeoutId;
64
- if (copied) {
65
- timeoutId = window.setTimeout(()=>{
66
- setCopied(false);
67
- }, 3000);
68
- }
69
- return ()=>{
70
- clearTimeout(timeoutId);
71
- };
72
- }, [
73
- copied
74
- ]);
75
36
  return /*#__PURE__*/ jsxs("div", {
76
37
  className: bubbleClasses.wrapper,
77
38
  children: [
@@ -128,23 +89,9 @@ const Bubble = ({ children, kind = "left", className, contentClassName, footerCl
128
89
  rawFooter && rawFooter
129
90
  ]
130
91
  }),
131
- isCopyToClipboardEnabled && /*#__PURE__*/ jsx("div", {
132
- className: bubbleClasses.copyButton,
133
- children: /*#__PURE__*/ jsx(ButtonIcon, {
134
- noBorder: true,
135
- noBackground: true,
136
- size: "small",
137
- mode: copyToClipboardMode,
138
- focusMode: copyToClipboardFocusMode,
139
- label: copied ? "Copied to clipboard" : "Copy to clipboard",
140
- onClick: handleCopyToClipboard,
141
- disabled: copied,
142
- children: copied ? /*#__PURE__*/ jsx(IconCopied, {
143
- size: "size-3"
144
- }) : /*#__PURE__*/ jsx(IconCopy, {
145
- size: "size-3"
146
- })
147
- })
92
+ action && /*#__PURE__*/ jsx("div", {
93
+ className: bubbleClasses.action,
94
+ children: action
148
95
  })
149
96
  ]
150
97
  });
@@ -47,22 +47,20 @@ export type BubbleProps = {
47
47
  */
48
48
  footerClassName?: string;
49
49
  /**
50
- * Whether or not to show a "copy/paste" icon next to the Bubble.
51
- * - If a function is passed, it will be called with the text to copy.
52
- * - If a string is passed, that string will be copied.
53
- * - If a boolean is passed, the children will be copied, but only if they
54
- * are of type string.
55
- */
56
- copyToClipboard?: boolean | string | ((text: any) => void);
57
- /**
58
- * The type of focus for the Copy Button. This will change the color
59
- * of the focus ring around the Button.
60
- */
61
- copyToClipboardFocusMode?: "dark" | "light" | "system" | "alt-system";
62
- /**
63
- * The mode of Copy Button. This will change the color of the Button.
50
+ * A React node to be displayed in the action slot (typically next to the bubble).
51
+ * Use this for copy buttons, dropdown menus, or any other interactive elements.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * import { Bubble } from "@versini/ui-bubble/bubble";
56
+ * import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
57
+ *
58
+ * <Bubble action={<BubbleCopy>Hello World</BubbleCopy>}>
59
+ * Hello World
60
+ * </Bubble>
61
+ * ```
64
62
  */
65
- copyToClipboardMode?: "dark" | "light" | "system" | "alt-system";
63
+ action?: React.ReactNode;
66
64
  /**
67
65
  * Array of footer items to display below the bubble content.
68
66
  * @example
@@ -1,6 +1,6 @@
1
1
  /*!
2
- @versini/ui-bubble v10.2.4
3
- © 2025 gizmette.com
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
6
 
@@ -4,6 +4,6 @@ export declare const getBubbleClasses: ({ kind, className, contentClassName, foo
4
4
  wrapper: string;
5
5
  main: string;
6
6
  footer: string;
7
- copyButton: string;
7
+ action: string;
8
8
  };
9
9
  export {};
@@ -1,6 +1,6 @@
1
1
  /*!
2
- @versini/ui-bubble v10.2.4
3
- © 2025 gizmette.com
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import clsx from "clsx";
@@ -82,7 +82,7 @@ const getBubbleClasses = ({ kind, className, contentClassName, footerClassName,
82
82
  tail
83
83
  }), contentClassName);
84
84
  const footer = clsx("pr-2 pt-1 text-end text-xs text-copy-light", footerClassName);
85
- const copyButton = clsx("flex flex-col-reverse sm:flex-row", {
85
+ const action = clsx("flex flex-col-reverse sm:flex-row", {
86
86
  "ml-2": kind === "left" && !tail,
87
87
  "mr-2": kind === "right" && !tail,
88
88
  "ml-1": kind === "left" && tail,
@@ -92,7 +92,7 @@ const getBubbleClasses = ({ kind, className, contentClassName, footerClassName,
92
92
  wrapper,
93
93
  main,
94
94
  footer,
95
- copyButton
95
+ action
96
96
  };
97
97
  };
98
98
 
@@ -1,6 +1,6 @@
1
1
  /*!
2
- @versini/ui-bubble v10.2.4
3
- © 2025 gizmette.com
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
6
 
@@ -0,0 +1,30 @@
1
+ import type { BubbleCopyProps } from "./BubbleCopyTypes";
2
+ export type { BubbleCopyProps } from "./BubbleCopyTypes";
3
+ /**
4
+ * BubbleCopy provides a copy-to-clipboard button for use with the Bubble component.
5
+ * It handles the copy state, icon switching, and automatic reset after copying.
6
+ * Supports both plain text and rich text (HTML) copying.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { Bubble } from "@versini/ui-bubble/bubble";
11
+ * import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
12
+ *
13
+ * // Plain text copy
14
+ * <Bubble action={<BubbleCopy>Hello World</BubbleCopy>}>
15
+ * Hello World
16
+ * </Bubble>
17
+ *
18
+ * // Rich text copy (preserves formatting when pasting into Word, Docs, etc.)
19
+ * <Bubble action={
20
+ * <BubbleCopy richText>
21
+ * <h2>Title</h2>
22
+ * <ul><li>Item 1</li><li>Item 2</li></ul>
23
+ * </BubbleCopy>
24
+ * }>
25
+ * <h2>Title</h2>
26
+ * <ul><li>Item 1</li><li>Item 2</li></ul>
27
+ * </Bubble>
28
+ * ```
29
+ */
30
+ export declare const BubbleCopy: ({ children, richText, mode, focusMode, }: BubbleCopyProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,144 @@
1
+ /*!
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { ButtonIcon } from "@versini/ui-button/button-icon";
8
+ import { IconCopied, IconCopy } from "@versini/ui-icons";
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
+
11
+ ;// CONCATENATED MODULE: external "react/jsx-runtime"
12
+
13
+ ;// CONCATENATED MODULE: external "@versini/ui-button/button-icon"
14
+
15
+ ;// CONCATENATED MODULE: external "@versini/ui-icons"
16
+
17
+ ;// CONCATENATED MODULE: external "react"
18
+
19
+ ;// CONCATENATED MODULE: ./src/components/BubbleCopy/BubbleCopy.tsx
20
+
21
+
22
+
23
+
24
+ /**
25
+ * BubbleCopy provides a copy-to-clipboard button for use with the Bubble component.
26
+ * It handles the copy state, icon switching, and automatic reset after copying.
27
+ * Supports both plain text and rich text (HTML) copying.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import { Bubble } from "@versini/ui-bubble/bubble";
32
+ * import { BubbleCopy } from "@versini/ui-bubble/bubble-copy";
33
+ *
34
+ * // Plain text copy
35
+ * <Bubble action={<BubbleCopy>Hello World</BubbleCopy>}>
36
+ * Hello World
37
+ * </Bubble>
38
+ *
39
+ * // Rich text copy (preserves formatting when pasting into Word, Docs, etc.)
40
+ * <Bubble action={
41
+ * <BubbleCopy richText>
42
+ * <h2>Title</h2>
43
+ * <ul><li>Item 1</li><li>Item 2</li></ul>
44
+ * </BubbleCopy>
45
+ * }>
46
+ * <h2>Title</h2>
47
+ * <ul><li>Item 1</li><li>Item 2</li></ul>
48
+ * </Bubble>
49
+ * ```
50
+ */ const BubbleCopy = ({ children, richText = false, mode = "system", focusMode = "system" })=>{
51
+ const [copied, setCopied] = useState(false);
52
+ const contentRef = useRef(null);
53
+ /* v8 ignore start - clipboard edge cases */ const handleCopyToClipboard = useCallback(async ()=>{
54
+ // Ensure clipboard API is available before attempting to copy
55
+ if (!navigator.clipboard) {
56
+ return;
57
+ }
58
+ try {
59
+ if (richText && contentRef.current && typeof ClipboardItem !== "undefined" && typeof navigator.clipboard.write === "function") {
60
+ // Rich text copy: write both HTML and plain text to clipboard
61
+ const htmlContent = contentRef.current.innerHTML;
62
+ const textContent = contentRef.current.innerText;
63
+ const clipboardItem = new ClipboardItem({
64
+ "text/html": new Blob([
65
+ htmlContent
66
+ ], {
67
+ type: "text/html"
68
+ }),
69
+ "text/plain": new Blob([
70
+ textContent
71
+ ], {
72
+ type: "text/plain"
73
+ })
74
+ });
75
+ await navigator.clipboard.write([
76
+ clipboardItem
77
+ ]);
78
+ } else if (typeof children === "string" && typeof navigator.clipboard.writeText === "function") {
79
+ // Plain text copy (original behavior)
80
+ await navigator.clipboard.writeText(children);
81
+ } else if (contentRef.current && typeof navigator.clipboard.writeText === "function") {
82
+ // ReactNode but not richText: extract innerText for plain text copy
83
+ await navigator.clipboard.writeText(contentRef.current.innerText);
84
+ } else {
85
+ // Clipboard API capabilities are insufficient for the requested operation
86
+ return;
87
+ }
88
+ // Only set copied state after a successful clipboard write
89
+ setCopied(true);
90
+ } catch {
91
+ // Swallow clipboard errors to avoid unhandled rejections
92
+ }
93
+ }, [
94
+ children,
95
+ richText
96
+ ]);
97
+ /* v8 ignore stop */ // Reset copied state after 3 seconds
98
+ useEffect(()=>{
99
+ let timeoutId;
100
+ if (copied) {
101
+ timeoutId = window.setTimeout(()=>{
102
+ setCopied(false);
103
+ }, 3000);
104
+ }
105
+ return ()=>{
106
+ clearTimeout(timeoutId);
107
+ };
108
+ }, [
109
+ copied
110
+ ]);
111
+ // Don't render if there's nothing to copy
112
+ if (!children) {
113
+ return null;
114
+ }
115
+ // Check if we need the hidden content span (for richText or non-string children)
116
+ const needsContentRef = richText || typeof children !== "string";
117
+ return /*#__PURE__*/ jsxs(Fragment, {
118
+ children: [
119
+ needsContentRef && /*#__PURE__*/ jsx("span", {
120
+ ref: contentRef,
121
+ className: "sr-only",
122
+ "aria-hidden": "true",
123
+ children: children
124
+ }),
125
+ /*#__PURE__*/ jsx(ButtonIcon, {
126
+ noBorder: true,
127
+ noBackground: true,
128
+ size: "small",
129
+ mode: mode,
130
+ focusMode: focusMode,
131
+ label: copied ? "Copied to clipboard" : "Copy to clipboard",
132
+ onClick: handleCopyToClipboard,
133
+ disabled: copied,
134
+ children: copied ? /*#__PURE__*/ jsx(IconCopied, {
135
+ size: "size-3"
136
+ }) : /*#__PURE__*/ jsx(IconCopy, {
137
+ size: "size-3"
138
+ })
139
+ })
140
+ ]
141
+ });
142
+ };
143
+
144
+ export { BubbleCopy };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Props for the BubbleCopy component.
3
+ */
4
+ export interface BubbleCopyProps {
5
+ /**
6
+ * The content to copy. Can be a string for plain text, or JSX for rich text
7
+ * (when used with the richText prop).
8
+ */
9
+ children: React.ReactNode;
10
+ /**
11
+ * When true, copies content as rich text (HTML) in addition to plain text.
12
+ * This allows pasting into applications like Word or Google Docs with
13
+ * formatting preserved. When false, only plain text is copied.
14
+ * @default false
15
+ */
16
+ richText?: boolean;
17
+ /**
18
+ * The mode of the Copy Button. This will change the color of the Button.
19
+ * @default "system"
20
+ */
21
+ mode?: "dark" | "light" | "system" | "alt-system";
22
+ /**
23
+ * The type of focus for the Copy Button. This will change the color
24
+ * of the focus ring around the Button.
25
+ * @default "system"
26
+ */
27
+ focusMode?: "dark" | "light" | "system" | "alt-system";
28
+ }
@@ -0,0 +1,11 @@
1
+ /*!
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+
7
+ ;// CONCATENATED MODULE: ./src/components/BubbleCopy/BubbleCopyTypes.ts
8
+ /**
9
+ * Props for the BubbleCopy component.
10
+ */
11
+
@@ -3,3 +3,4 @@ export * from "./Bubble/Bubble";
3
3
  export type * from "./Bubble/BubbleTypes";
4
4
  export type { BubbleFooter, BubbleFooterItem, BubbleFooterKeyValue, BubbleFooterValueOnly, } from "./Bubble/BubbleTypes";
5
5
  export { BUBBLE_FOOTER_EMPTY } from "./BubbleConstants/BubbleConstants";
6
+ export * from "./BubbleCopy/BubbleCopy";
@@ -1,11 +1,12 @@
1
1
  /*!
2
- @versini/ui-bubble v10.2.4
3
- © 2025 gizmette.com
2
+ @versini/ui-bubble v11.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { BUBBLE_FOOTER_EMPTY } from "./BubbleConstants/BubbleConstants.js";
7
7
  export * from "../common/constants.js";
8
8
  export * from "./Bubble/Bubble.js";
9
+ export * from "./BubbleCopy/BubbleCopy.js";
9
10
 
10
11
  ;// CONCATENATED MODULE: external "./BubbleConstants/BubbleConstants.js"
11
12
 
@@ -15,4 +16,5 @@ export * from "./Bubble/Bubble.js";
15
16
  // Export constants through a dedicated module entry for better tree-shaking.
16
17
 
17
18
 
19
+
18
20
  export { BUBBLE_FOOTER_EMPTY };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-bubble",
3
- "version": "10.2.4",
3
+ "version": "11.0.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -17,6 +17,10 @@
17
17
  "types": "./dist/components/Bubble/Bubble.d.ts",
18
18
  "import": "./dist/components/Bubble/Bubble.js"
19
19
  },
20
+ "./bubble-copy": {
21
+ "types": "./dist/components/BubbleCopy/BubbleCopy.d.ts",
22
+ "import": "./dist/components/BubbleCopy/BubbleCopy.js"
23
+ },
20
24
  "./constants": {
21
25
  "types": "./dist/components/BubbleConstants/BubbleConstants.d.ts",
22
26
  "import": "./dist/components/BubbleConstants/BubbleConstants.js"
@@ -58,5 +62,5 @@
58
62
  "sideEffects": [
59
63
  "**/*.css"
60
64
  ],
61
- "gitHead": "fae53f4bd56440f7a72ed63b1a2a02b0c3b5a7e6"
65
+ "gitHead": "bf646259091d0f1fd23afec7efb62587d06a66a1"
62
66
  }