@versini/ui-bubble 10.2.4 → 11.0.1
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 +186 -72
- package/dist/common/constants.js +2 -2
- package/dist/components/Bubble/Bubble.d.ts +1 -1
- package/dist/components/Bubble/Bubble.js +6 -59
- package/dist/components/Bubble/BubbleTypes.d.ts +13 -15
- package/dist/components/Bubble/BubbleTypes.js +2 -2
- package/dist/components/Bubble/utilities.d.ts +1 -1
- package/dist/components/Bubble/utilities.js +4 -4
- package/dist/components/BubbleConstants/BubbleConstants.js +2 -2
- package/dist/components/BubbleCopy/BubbleCopy.d.ts +30 -0
- package/dist/components/BubbleCopy/BubbleCopy.js +144 -0
- package/dist/components/BubbleCopy/BubbleCopyTypes.d.ts +28 -0
- package/dist/components/BubbleCopy/BubbleCopyTypes.js +11 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +4 -2
- package/package.json +8 -4
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
|
-
- **📋
|
|
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 -
|
|
88
|
-
<Bubble
|
|
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
|
-
{/*
|
|
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="
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
kind="right"
|
|
104
|
+
action={
|
|
105
|
+
<BubbleCopy mode="dark" focusMode="light">
|
|
106
|
+
Copy button with custom theme.
|
|
107
|
+
</BubbleCopy>
|
|
108
|
+
}
|
|
103
109
|
>
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
271
|
-
|
|
|
272
|
-
| children
|
|
273
|
-
| kind
|
|
274
|
-
| tail
|
|
275
|
-
|
|
|
276
|
-
| footer
|
|
277
|
-
| rawFooter
|
|
278
|
-
| noMaxWidth
|
|
279
|
-
| className
|
|
280
|
-
| contentClassName
|
|
281
|
-
|
|
282
|
-
|
|
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
|
package/dist/common/constants.js
CHANGED
|
@@ -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,
|
|
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
|
|
3
|
-
©
|
|
2
|
+
@versini/ui-bubble v11.0.1
|
|
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,
|
|
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
|
-
|
|
132
|
-
className: bubbleClasses.
|
|
133
|
-
children:
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
©
|
|
2
|
+
@versini/ui-bubble v11.0.1
|
|
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
|
|
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
|
-
|
|
95
|
+
action
|
|
96
96
|
};
|
|
97
97
|
};
|
|
98
98
|
|
|
@@ -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.1
|
|
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
|
+
}
|
|
@@ -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";
|
package/dist/components/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-bubble
|
|
3
|
-
©
|
|
2
|
+
@versini/ui-bubble v11.0.1
|
|
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": "
|
|
3
|
+
"version": "11.0.1",
|
|
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"
|
|
@@ -50,13 +54,13 @@
|
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|
|
52
56
|
"@tailwindcss/typography": "0.5.19",
|
|
53
|
-
"@versini/ui-button": "11.3.
|
|
54
|
-
"@versini/ui-icons": "4.
|
|
57
|
+
"@versini/ui-button": "11.3.1",
|
|
58
|
+
"@versini/ui-icons": "4.16.0",
|
|
55
59
|
"clsx": "2.1.1",
|
|
56
60
|
"tailwindcss": "4.1.18"
|
|
57
61
|
},
|
|
58
62
|
"sideEffects": [
|
|
59
63
|
"**/*.css"
|
|
60
64
|
],
|
|
61
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "7ed95d479dbfd4ef12b1e2fe9fdb763e2e538274"
|
|
62
66
|
}
|