bubble-toolbar 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/README.md +407 -120
- package/dist/index.d.mts +259 -12
- package/dist/index.d.ts +259 -12
- package/dist/index.js +1347 -196
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1352 -188
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +299 -8
- package/package.json +87 -54
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,195 +1,482 @@
|
|
|
1
1
|
# 🫧 Bubble Toolbar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/bubble-toolbar)
|
|
4
|
+
[](https://react.dev/)
|
|
5
|
+
[](./tsconfig.json)
|
|
6
|
+
[](./.storybook/main.ts)
|
|
7
|
+
[](./eslint.config.mjs)
|
|
8
|
+
[](./biome.json)
|
|
9
|
+
[](./LICENSE)
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
A responsive floating toolbar with a frosted glass effect for React 18 and 19. Built with TypeScript, it arranges content into groups that adapt from circles to pills based on their children.
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
## Features
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
- **React 18 and 19 support:** typed components with forwarded refs.
|
|
16
|
+
- **Group-based composition:** pass arrays of nodes or use `BubbleToolbar.Group` children.
|
|
17
|
+
- **Adaptive shapes:** a single child creates a circle; multiple children create a pill.
|
|
18
|
+
- **Frosted glass:** background blur, subtle borders, soft shadows, and glass, light, dark, or automatic themes.
|
|
19
|
+
- **Responsive layout:** fixed or relative positioning, automatic centering, and horizontal scrolling on smaller screens.
|
|
20
|
+
- **Tests and examples:** Jest and React Testing Library tests, plus interactive Storybook stories.
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
- 🔮 **Definición por Grupos**: Define la barra pasando arreglos de nodos o mediante sintaxis de componentes compuestos (`BubbleToolbar.Group`).
|
|
13
|
-
- 🔘 **Morfología Adaptativa Dinámica**:
|
|
14
|
-
- **1 solo nodo** $\rightarrow$ se renderiza como un **círculo** perfecto (`aspect-ratio: 1/1`).
|
|
15
|
-
- **Más de 1 nodo** $\rightarrow$ se convierte en un **rectángulo redondeado (cápsula / pill)**, respetando la curvatura en los extremos para mantener el equilibrio visual exacto con las burbujas circulares.
|
|
16
|
-
- 🪟 **Blurred Glass (Frosted Glassmorphism)**: Efecto translúcido con desenfoque de fondo (`backdrop-filter: blur(16px)`), bordes frosted, sombras suaves y soporte para temas claro, oscuro o automático.
|
|
17
|
-
- 📱 **Flotante y Responsivo**: Posicionamiento fijo o relativo, centrado automático y desplazamiento horizontal suave sin barras de scroll invasivas en pantallas móviles.
|
|
18
|
-
- 🧪 **100% Testeado con Jest**: Suite integral de pruebas unitarias y de integración con `@testing-library/react`.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## 📦 Instalación
|
|
22
|
+
## Installation
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
npm install bubble-toolbar
|
|
26
|
-
#
|
|
26
|
+
# Or use pnpm:
|
|
27
27
|
pnpm add bubble-toolbar
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
## Quick start
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### Group arrays
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
```tsx
|
|
35
|
+
import { BubbleArea, BubbleIconButton, BubbleToolbar } from 'bubble-toolbar';
|
|
36
|
+
import 'bubble-toolbar/styles.css';
|
|
37
|
+
|
|
38
|
+
const App = () => (
|
|
39
|
+
<BubbleToolbar
|
|
40
|
+
position="bottom"
|
|
41
|
+
theme="glass"
|
|
42
|
+
groups={[
|
|
43
|
+
// One child: circle.
|
|
44
|
+
[<BubbleIconButton key="logo" name="logo" />],
|
|
45
|
+
|
|
46
|
+
// Multiple children: pill.
|
|
47
|
+
[
|
|
48
|
+
<BubbleIconButton key="search" name="search" />,
|
|
49
|
+
<BubbleArea key="area">
|
|
50
|
+
<input aria-label="Search" placeholder="Search or type /" />
|
|
51
|
+
</BubbleArea>,
|
|
52
|
+
],
|
|
53
|
+
|
|
54
|
+
// Each group below becomes a circle.
|
|
55
|
+
[<BubbleIconButton key="theme" name="theme" />],
|
|
56
|
+
[<BubbleIconButton key="chat" name="chat" />],
|
|
57
|
+
[<BubbleIconButton key="notifications" name="notifications" badge={3} />],
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
```
|
|
35
62
|
|
|
36
|
-
|
|
63
|
+
### Compound components
|
|
37
64
|
|
|
38
65
|
```tsx
|
|
39
|
-
import
|
|
40
|
-
import
|
|
41
|
-
|
|
66
|
+
import { BubbleArea, BubbleIconButton, BubbleToolbar } from 'bubble-toolbar';
|
|
67
|
+
import 'bubble-toolbar/styles.css';
|
|
68
|
+
|
|
69
|
+
const FloatingMenu = () => (
|
|
70
|
+
<BubbleToolbar position="bottom" theme="glass">
|
|
71
|
+
<BubbleToolbar.Group>
|
|
72
|
+
<BubbleIconButton name="logo" />
|
|
73
|
+
</BubbleToolbar.Group>
|
|
74
|
+
|
|
75
|
+
<BubbleToolbar.Group>
|
|
76
|
+
<BubbleIconButton name="search" />
|
|
77
|
+
<BubbleArea>
|
|
78
|
+
<input aria-label="Search" placeholder="Search…" />
|
|
79
|
+
</BubbleArea>
|
|
80
|
+
</BubbleToolbar.Group>
|
|
81
|
+
|
|
82
|
+
<BubbleToolbar.Group>
|
|
83
|
+
<BubbleIconButton name="theme" />
|
|
84
|
+
</BubbleToolbar.Group>
|
|
85
|
+
</BubbleToolbar>
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The components include inline glass styles. Import the stylesheet for additional styling, including automatic dark theme support and focus indicators.
|
|
90
|
+
|
|
91
|
+
## Async activity
|
|
92
|
+
|
|
93
|
+
Import `bubble-toolbar/styles.css` to enable the rotating border and glow. Activity is indeterminate: it does not disable controls, cancel tasks, or display success or error states.
|
|
94
|
+
|
|
95
|
+
### External control
|
|
96
|
+
|
|
97
|
+
Set `busy` on the toolbar to illuminate every group, or on a group to illuminate only that bubble:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<BubbleToolbar busy={isSaving}>
|
|
101
|
+
<BubbleToolbar.Group>
|
|
102
|
+
<BubbleIconButton name="logo" />
|
|
103
|
+
</BubbleToolbar.Group>
|
|
104
|
+
<BubbleToolbar.Group busy={isSearching}>
|
|
105
|
+
<BubbleIconButton name="search" />
|
|
106
|
+
<BubbleArea>Search results</BubbleArea>
|
|
107
|
+
</BubbleToolbar.Group>
|
|
108
|
+
</BubbleToolbar>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Track tasks from inside a group
|
|
112
|
+
|
|
113
|
+
`useBubbleActivity()` returns `{ isBusy, run }`. It targets the nearest group, including groups created through the `groups` prop. Calling it outside a group throws a descriptive error.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { useState } from 'react';
|
|
117
|
+
import { BubbleIconButton, BubbleToolbar, useBubbleActivity } from 'bubble-toolbar';
|
|
118
|
+
import 'bubble-toolbar/styles.css';
|
|
119
|
+
|
|
120
|
+
const SaveAction = ({ save }: { save: () => Promise<void> }) => {
|
|
121
|
+
const { isBusy, run } = useBubbleActivity();
|
|
122
|
+
const [error, setError] = useState<string | null>(null);
|
|
42
123
|
|
|
43
|
-
function App() {
|
|
44
124
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<input placeholder="Buscar o escribir /" />
|
|
57
|
-
</BubbleArea>,
|
|
58
|
-
],
|
|
59
|
-
|
|
60
|
-
// Grupos 3, 4 y 5: 1 solo nodo cada uno => Círculos
|
|
61
|
-
[<BubbleIconButton key="theme" name="theme" />],
|
|
62
|
-
[<BubbleIconButton key="chat" name="chat" />],
|
|
63
|
-
[<BubbleIconButton key="notif" name="notifications" badge={3} />],
|
|
64
|
-
]}
|
|
125
|
+
<BubbleIconButton
|
|
126
|
+
name="chat"
|
|
127
|
+
tooltip={error ?? (isBusy ? 'Saving…' : 'Save')}
|
|
128
|
+
onClick={async () => {
|
|
129
|
+
setError(null);
|
|
130
|
+
try {
|
|
131
|
+
await run(save);
|
|
132
|
+
} catch {
|
|
133
|
+
setError('Save failed. Try again.');
|
|
134
|
+
}
|
|
135
|
+
}}
|
|
65
136
|
/>
|
|
66
137
|
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Render SaveAction inside a group:
|
|
141
|
+
// <BubbleToolbar.Group><SaveAction save={saveChanges} /></BubbleToolbar.Group>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`run<T>(task: () => Promise<T>): Promise<T>` returns the task's result and propagates synchronous exceptions and rejected promises. Handle errors in your application. Concurrent tasks keep the group busy until the last task settles. Unmounting the group does not cancel the consumer's task.
|
|
145
|
+
|
|
146
|
+
A group is busy when **any** of the following is true: the toolbar's `busy` prop, the group's `busy` prop, or a pending local task. Passing `busy={false}` does not hide activity from another source. Each group exposes its effective state through `aria-busy`.
|
|
147
|
+
|
|
148
|
+
Both toolbar and group accept these props; group appearance overrides the toolbar defaults:
|
|
149
|
+
|
|
150
|
+
| Prop | Type | Default | Description |
|
|
151
|
+
| :--- | :--- | :--- | :--- |
|
|
152
|
+
| `busy` | `boolean` | `false` | Adds externally managed activity. |
|
|
153
|
+
| `activityColors` | `readonly [string, string, string]` | `['#22d3ee', '#8b5cf6', '#ec4899']` | Three CSS colors for the gradient. |
|
|
154
|
+
| `activityDuration` | `number` | `2.4` | Seconds per rotation; non-positive or non-finite values fall back to 2.4. |
|
|
155
|
+
|
|
156
|
+
The effect follows circular and pill shapes without changing their dimensions. With `prefers-reduced-motion: reduce`, the border remains visible and static. Browsers without animated custom property support retain a static gradient; browsers without CSS masking retain a static colored border.
|
|
157
|
+
|
|
158
|
+
## Live Activities
|
|
159
|
+
|
|
160
|
+
Like iOS Live Activities: while a task runs, the whole toolbar collapses into a single pill with a spinner; when it finishes, the pill becomes a card with the result and a close button; after its duration (or on close) the toolbar is restored. The groups stay mounted the whole time, so their state (an open search, a counter) is kept.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { BubbleToolbar, useBubbleLiveActivity } from 'bubble-toolbar';
|
|
164
|
+
|
|
165
|
+
export function App() {
|
|
166
|
+
const live = useBubbleLiveActivity({ duration: 4000 });
|
|
167
|
+
|
|
168
|
+
const upload = async (files: File[]) => {
|
|
169
|
+
// Resolves or rejects exactly like the task itself.
|
|
170
|
+
const result = await live.run(() => api.upload(files), {
|
|
171
|
+
pending: { title: 'Uploading…' },
|
|
172
|
+
success: (r) => ({ title: 'Upload complete', description: `${r.count} files ready` }),
|
|
173
|
+
error: (e) => ({ title: 'Upload failed', description: String(e) }),
|
|
174
|
+
});
|
|
175
|
+
return result;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<UploadButton onFiles={upload} />
|
|
181
|
+
<BubbleToolbar position="top" liveActivity={live}>
|
|
182
|
+
{/* groups */}
|
|
183
|
+
</BubbleToolbar>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
67
186
|
}
|
|
68
187
|
```
|
|
69
188
|
|
|
70
|
-
|
|
189
|
+
- `live.run(task, options)` shows the task and returns its own promise. `live.show(content, { duration })` shows a card without a task, and `live.dismiss()` closes the current one.
|
|
190
|
+
- Activities are queued (FIFO). The pill shows `+n` when more are waiting, and a task that already finished goes straight to its card.
|
|
191
|
+
- The pill stays at least `minPendingMs` (600ms by default) so fast tasks do not flicker. `duration: 0` keeps the card until it is closed.
|
|
192
|
+
- The timer pauses while the card is hovered or focused. Esc closes the card. Results are announced through a polite live region, and errors through an alert.
|
|
193
|
+
- If focus was inside the toolbar, it moves to the pill and comes back to the same element afterwards.
|
|
194
|
+
- Inside the toolbar, `useBubbleToolbarLiveActivity()` returns the same controller without passing it down.
|
|
195
|
+
- Content has a fixed shape: `{ title, description?, icon?, tone?: 'neutral' | 'success' | 'error' }`. Texts go in `labels` (`pending`, `success`, `error`, `close`, `group`) and the look in `tokens`, both as `useBubbleLiveActivity` options.
|
|
196
|
+
- With `prefers-reduced-motion`, the toolbar switches without the width morph.
|
|
197
|
+
|
|
198
|
+
| `tokens` key | CSS variable | Default | Guard |
|
|
199
|
+
| :--- | :--- | :--- | :--- |
|
|
200
|
+
| `compactWidth` | `--bubble-live-compact-width` | `200px` | Capped at `100vw - 32px` |
|
|
201
|
+
| `width` | `--bubble-live-width` | `360px` | Capped at `100vw - 32px` |
|
|
202
|
+
| `radius` | `--bubble-live-radius` | `24px` | |
|
|
203
|
+
| `progressColor` | `--bubble-live-progress-color` | `currentColor` at 40% | |
|
|
204
|
+
| `successColor` / `errorColor` | `--bubble-live-success-color` / `--bubble-live-error-color` | `#22c55e` / `#ef4444` | |
|
|
205
|
+
| `titleSize` | `--bubble-live-title-size` | `13px` | 11–18px |
|
|
206
|
+
| `metaSize` | `--bubble-live-meta-size` | `11px` | 9–14px |
|
|
207
|
+
| `duration` | `--bubble-live-duration` | `320ms` | 0–600ms |
|
|
208
|
+
|
|
209
|
+
## Search and breadcrumb search
|
|
210
|
+
|
|
211
|
+
`BubbleSearch` is a search bubble: a button that expands into an accessible combobox with async results. `BubbleBreadcrumbSearch` shows the current location as a breadcrumb and turns into the same search when its button is pressed.
|
|
71
212
|
|
|
72
213
|
```tsx
|
|
73
|
-
import {
|
|
214
|
+
import { BubbleBreadcrumbSearch, BubbleToolbar } from 'bubble-toolbar';
|
|
215
|
+
|
|
216
|
+
const searchPages = async (query: string, signal: AbortSignal) => {
|
|
217
|
+
const response = await fetch(`/api/pages?q=${encodeURIComponent(query)}`, { signal });
|
|
218
|
+
return response.json(); // [{ id, title, description? }]
|
|
219
|
+
};
|
|
74
220
|
|
|
75
|
-
function
|
|
221
|
+
export function Header({ location, navigate }) {
|
|
76
222
|
return (
|
|
77
|
-
<BubbleToolbar position="
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<BubbleIconButton name="search" />
|
|
86
|
-
<BubbleArea>
|
|
87
|
-
<input placeholder="Buscar..." />
|
|
88
|
-
</BubbleArea>
|
|
89
|
-
</BubbleToolbar.Group>
|
|
90
|
-
|
|
91
|
-
{/* 1 nodo: Círculo */}
|
|
92
|
-
<BubbleToolbar.Group>
|
|
93
|
-
<BubbleIconButton name="theme" />
|
|
94
|
-
</BubbleToolbar.Group>
|
|
223
|
+
<BubbleToolbar position="top" style={{ alignItems: 'flex-start' }}>
|
|
224
|
+
<BubbleToolbar.BreadcrumbSearch
|
|
225
|
+
items={location} // [{ label: 'Projects', href: '/projects' }, { label: 'Overview' }]
|
|
226
|
+
onSearch={searchPages}
|
|
227
|
+
onSelect={(page) => navigate(page.id)}
|
|
228
|
+
suggestions={recentPages}
|
|
229
|
+
labels={{ location: 'Current page', placeholder: 'Search pages…' }}
|
|
230
|
+
/>
|
|
95
231
|
</BubbleToolbar>
|
|
96
232
|
);
|
|
97
233
|
}
|
|
98
234
|
```
|
|
99
235
|
|
|
100
|
-
|
|
236
|
+
- `onSearch` receives the trimmed, lower-cased query and an `AbortSignal`. The signal is aborted when the query changes, the search closes or the component unmounts, so stale results never replace newer ones. While a search is pending, the group shows the async activity border.
|
|
237
|
+
- Items have a fixed shape, `{ id, title, description? }`, so the listbox layout and keyboard navigation always work.
|
|
238
|
+
- The breadcrumb is controlled: pass the current `items` (usually from your router) and update them in `onSelect`. The last item is the current page and stays visible while parent levels truncate first.
|
|
239
|
+
- Keyboard: ↑ ↓ move through options, Enter selects, Esc closes and returns focus to the button. Clicking or tabbing outside closes the search.
|
|
240
|
+
- Without a breadcrumb, `BubbleSearch` is a circle while closed and expands when opened.
|
|
241
|
+
- In `top` and `static` toolbars the panel opens downward; give the toolbar `alignItems: 'flex-start'` so the other groups stay in place. In `bottom` toolbars it opens upward; use `alignItems: 'flex-end'`.
|
|
242
|
+
- The open/close collapse is applied inline, so the search works without `styles.css`; import it for the default look and the token-driven styles.
|
|
243
|
+
|
|
244
|
+
## Theming with tokens
|
|
245
|
+
|
|
246
|
+
Customization goes through CSS custom properties. Structure, layout and behavior stay fixed, and tokens that could break the layout are clamped. Import `bubble-toolbar/styles.css` to get the defaults.
|
|
101
247
|
|
|
102
|
-
|
|
248
|
+
Toolbar tokens, set on `:root` or any ancestor:
|
|
103
249
|
|
|
104
|
-
|
|
|
105
|
-
| :--- |
|
|
106
|
-
|
|
|
107
|
-
|
|
|
250
|
+
| Token | Default |
|
|
251
|
+
| :--- | :--- |
|
|
252
|
+
| `--bubble-glass-bg` / `--bubble-glass-bg-solid` | `rgba(255, 255, 255, 0.65)` / `rgba(255, 255, 255, 0.92)` |
|
|
253
|
+
| `--bubble-glass-border` / `--bubble-glass-fg` | `rgba(255, 255, 255, 0.4)` / `#09090b` |
|
|
254
|
+
| `--bubble-glass-shadow` | Default floating shadow |
|
|
255
|
+
| `--bubble-light-bg` / `-bg-solid` / `-border` / `-fg` | `rgba(255, 255, 255, 0.75)` / `0.95` / `rgba(0, 0, 0, 0.08)` / `#18181b` |
|
|
256
|
+
| `--bubble-dark-bg` / `-bg-solid` / `-border` / `-fg` | `rgba(24, 24, 27, 0.72)` / `0.95` / `rgba(255, 255, 255, 0.12)` / `#f4f4f5` |
|
|
257
|
+
| `--bubble-button-hover-bg` / `--bubble-button-active-bg` | `rgba(0, 0, 0, 0.05)` / `rgba(0, 0, 0, 0.08)` |
|
|
258
|
+
| `--bubble-focus-ring` | `#3b82f6` |
|
|
108
259
|
|
|
109
|
-
|
|
260
|
+
The `blurred` prop picks the `-bg` (translucent) or `-bg-solid` variant. A string `shadow` prop still wins over `--bubble-glass-shadow`.
|
|
110
261
|
|
|
111
|
-
|
|
262
|
+
Search tokens can be passed through the typed `tokens` prop, where numbers become `px` (or `ms` for `duration`), or set as CSS variables:
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<BubbleToolbar.Search tokens={{ width: 420, radius: 14, focusRing: '#f97316' }} … />
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```css
|
|
269
|
+
.brand-search {
|
|
270
|
+
--bubble-search-option-active-bg: rgb(249 115 22 / 16%);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
| `tokens` key | CSS variable | Default | Guard |
|
|
275
|
+
| :--- | :--- | :--- | :--- |
|
|
276
|
+
| `width` | `--bubble-search-width` | `360px` | Capped at `100vw - 96px` |
|
|
277
|
+
| `radius` | `--bubble-search-radius` | `24px` | |
|
|
278
|
+
| `divider` | `--bubble-search-divider` | `rgb(128 128 128 / 18%)` | |
|
|
279
|
+
| `focusRing` | `--bubble-search-focus-ring` | `#818cf8` | |
|
|
280
|
+
| `optionActiveBg` | `--bubble-search-option-active-bg` | `rgb(129 140 248 / 18%)` | |
|
|
281
|
+
| `optionRadius` | `--bubble-search-option-radius` | `12px` | |
|
|
282
|
+
| `titleSize` | `--bubble-search-title-size` | `13px` | 10–20px |
|
|
283
|
+
| `titleWeight` | `--bubble-search-title-weight` | `600` | |
|
|
284
|
+
| `metaSize` | `--bubble-search-meta-size` | `11px` | 9–16px |
|
|
285
|
+
| `mutedOpacity` | `--bubble-search-muted-opacity` | `0.7` | 0.4–1 |
|
|
286
|
+
| `resultsMaxHeight` | `--bubble-search-results-max-height` | `220px` | Capped at `100dvh - 208px` |
|
|
287
|
+
| `panelPadding` | `--bubble-search-panel-padding` | `8px` | 0–16px |
|
|
288
|
+
| `duration` | `--bubble-search-duration` | `180ms` | 0–600ms |
|
|
289
|
+
| `easing` | `--bubble-search-easing` | `ease` | |
|
|
290
|
+
| `breadcrumbSize` | `--bubble-breadcrumb-size` | `12px` | 10–16px |
|
|
291
|
+
| `breadcrumbSeparatorColor` | `--bubble-breadcrumb-separator-color` | `currentColor` | |
|
|
292
|
+
| `breadcrumbCurrentWeight` | `--bubble-breadcrumb-current-weight` | `500` | |
|
|
293
|
+
| `breadcrumbMutedOpacity` | `--bubble-breadcrumb-muted-opacity` | `0.7` | 0.4–1 |
|
|
294
|
+
|
|
295
|
+
Unknown `tokens` keys are ignored. The `breadcrumb*` keys only apply to `BubbleBreadcrumbSearch`.
|
|
296
|
+
|
|
297
|
+
## Adaptive shapes
|
|
298
|
+
|
|
299
|
+
| Group content | Shape | Behavior |
|
|
300
|
+
| :--- | :--- | :--- |
|
|
301
|
+
| One child | Circle | Equal width and height, with centered content. The default diameter is 48px. |
|
|
302
|
+
| Multiple children | Pill | Fixed height, content-based width, horizontal spacing, and rounded ends. |
|
|
303
|
+
|
|
304
|
+
Use the `shape` prop on a group to override its automatic shape.
|
|
305
|
+
|
|
306
|
+
## API reference
|
|
112
307
|
|
|
113
308
|
### `<BubbleToolbar />`
|
|
114
309
|
|
|
115
|
-
| Prop |
|
|
116
|
-
| :--- | :--- |
|
|
117
|
-
| `groups` | `ReactNode[][]` | `undefined` |
|
|
118
|
-
| `children` | `ReactNode` | `undefined` |
|
|
119
|
-
| `position` | `'bottom' \| 'top' \| 'static' \| 'custom'` | `'bottom'` |
|
|
120
|
-
| `theme` | `'glass' \| 'light' \| 'dark' \| 'auto'` | `'glass'` |
|
|
121
|
-
| `size` | `'sm' \| 'md' \| 'lg' \| number` | `'md'` |
|
|
122
|
-
| `blurred` | `boolean` | `true` |
|
|
123
|
-
| `blurAmount
|
|
124
|
-
| `groupGap` | `number \| string` | `8` |
|
|
125
|
-
| `itemGap` | `number \| string` | `8` |
|
|
126
|
-
| `bordered` | `boolean` | `true` |
|
|
127
|
-
| `shadow` | `boolean \| string` | `true` |
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
310
|
+
| Prop | Type | Default | Description |
|
|
311
|
+
| :--- | :--- | :--- | :--- |
|
|
312
|
+
| `groups` | `ReactNode[][]` | `undefined` | Arrays of grouped nodes. |
|
|
313
|
+
| `children` | `ReactNode` | `undefined` | Children such as `<BubbleToolbar.Group>`. |
|
|
314
|
+
| `position` | `'bottom' \| 'top' \| 'static' \| 'custom'` | `'bottom'` | Toolbar placement. Use `style` to set custom positioning. |
|
|
315
|
+
| `theme` | `'glass' \| 'light' \| 'dark' \| 'auto'` | `'glass'` | Visual theme. |
|
|
316
|
+
| `size` | `'sm' \| 'md' \| 'lg' \| number` | `'md'` | Group height and circle diameter: 40px, 48px, 56px, or a custom pixel value. |
|
|
317
|
+
| `blurred` | `boolean` | `true` | Enables background blur. |
|
|
318
|
+
| `blurAmount` | `number \| string` | `16` | Blur radius in pixels or a CSS length. |
|
|
319
|
+
| `groupGap` | `number \| string` | `8` | Space between groups, in pixels or a CSS length. |
|
|
320
|
+
| `itemGap` | `number \| string` | `8` | Space between children within a pill. |
|
|
321
|
+
| `bordered` | `boolean` | `true` | Shows subtle borders. |
|
|
322
|
+
| `shadow` | `boolean \| string` | `true` | Enables the default shadow or supplies a custom CSS shadow. |
|
|
323
|
+
| `ariaLabel` | `string` | `'Toolbar'` | Accessible toolbar label. |
|
|
324
|
+
| `liveActivity` | `BubbleLiveActivityController` | `undefined` | Controller from `useBubbleLiveActivity`; see [Live Activities](#live-activities). |
|
|
325
|
+
|
|
326
|
+
### `<BubbleGroup />` / `<BubbleToolbar.Group />`
|
|
327
|
+
|
|
328
|
+
| Prop | Type | Default | Description |
|
|
329
|
+
| :--- | :--- | :--- | :--- |
|
|
330
|
+
| `children` | `ReactNode` | `undefined` | Group content. |
|
|
331
|
+
| `shape` | `'circle' \| 'pill'` | Automatic | Overrides the shape inferred from the child count. |
|
|
332
|
+
| `size` | `'sm' \| 'md' \| 'lg' \| number` | Inherited | Overrides the toolbar's group size. |
|
|
333
|
+
| `itemGap` | `number \| string` | Inherited | Overrides spacing between children. |
|
|
137
334
|
|
|
138
335
|
### `<BubbleIconButton />`
|
|
139
336
|
|
|
140
|
-
| Prop |
|
|
141
|
-
| :--- | :--- |
|
|
142
|
-
| `name` | `
|
|
143
|
-
| `icon` | `ReactNode` | `undefined` |
|
|
144
|
-
| `badge` | `ReactNode
|
|
145
|
-
| `active` | `boolean` | `false` |
|
|
146
|
-
| `tooltip` | `string` | `undefined` |
|
|
337
|
+
| Prop | Type | Default | Description |
|
|
338
|
+
| :--- | :--- | :--- | :--- |
|
|
339
|
+
| `name` | `string` | `undefined` | Built-in icon: `search`, `theme`, `chat`, `notifications`, `logo`, or `close`. |
|
|
340
|
+
| `icon` | `ReactNode` | `undefined` | Custom icon content. |
|
|
341
|
+
| `badge` | `ReactNode` | `undefined` | Notification indicator or count. |
|
|
342
|
+
| `active` | `boolean` | `false` | Applies active styling. |
|
|
343
|
+
| `tooltip` | `string` | `undefined` | Tooltip and fallback accessible label. |
|
|
344
|
+
| `ariaLabel` | `string` | Derived | Explicit accessible label; falls back to the tooltip or icon name. |
|
|
147
345
|
|
|
148
|
-
|
|
346
|
+
### `<BubbleArea />`
|
|
149
347
|
|
|
150
|
-
|
|
348
|
+
| Prop | Type | Default | Description |
|
|
349
|
+
| :--- | :--- | :--- | :--- |
|
|
350
|
+
| `children` | `ReactNode` | `undefined` | Content such as a search input, label, or slider. |
|
|
351
|
+
| `minWidth` | `number \| string` | `120` | Minimum width in pixels or a CSS length. |
|
|
352
|
+
| `maxWidth` | `number \| string` | `undefined` | Maximum width in pixels or a CSS length. |
|
|
151
353
|
|
|
152
|
-
|
|
354
|
+
### `<BubbleSearch />` / `<BubbleToolbar.Search />`
|
|
153
355
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
356
|
+
| Prop | Type | Default | Description |
|
|
357
|
+
| :--- | :--- | :--- | :--- |
|
|
358
|
+
| `onSearch` | `(query, signal) => Promise<T[]>` | Required | Async search; `T extends { id, title, description? }`. |
|
|
359
|
+
| `onSelect` | `(item: T) => void` | Required | Called when an option is chosen. |
|
|
360
|
+
| `suggestions` | `T[]` | `[]` | Shown for an empty query and when nothing matches. |
|
|
361
|
+
| `onError` | `(error: unknown) => void` | `undefined` | Search errors other than cancellations. |
|
|
362
|
+
| `open` / `defaultOpen` / `onOpenChange` | `boolean` / `boolean` / `(open) => void` | Uncontrolled, `false` | Controlled or uncontrolled open state. |
|
|
363
|
+
| `debounceMs` | `number` | `0` | Delay before searching. |
|
|
364
|
+
| `labels` | `Partial<BubbleSearchLabels>` | English | Accessible names and visible texts (`group`, `button`, `input`, `listbox`, `placeholder`, `searching`, `noResults`, `suggestions`, `results(count)`, `hint`). |
|
|
365
|
+
| `tokens` | `BubbleSearchTokens` | `undefined` | See [Theming with tokens](#theming-with-tokens). |
|
|
366
|
+
| `className` | `string` | `undefined` | Class for the group, useful to scope tokens. |
|
|
367
|
+
|
|
368
|
+
### `<BubbleBreadcrumbSearch />` / `<BubbleToolbar.BreadcrumbSearch />`
|
|
157
369
|
|
|
158
|
-
|
|
370
|
+
Accepts every `BubbleSearch` prop, plus:
|
|
371
|
+
|
|
372
|
+
| Prop | Type | Default | Description |
|
|
373
|
+
| :--- | :--- | :--- | :--- |
|
|
374
|
+
| `items` | `{ label: string; href?: string }[]` | Required | Current location; the last item is the current page. |
|
|
375
|
+
| `separator` | `string` | `'/'` | Text between levels. |
|
|
376
|
+
| `labels.location` | `string` | `'Breadcrumb'` | Accessible name of the breadcrumb navigation. |
|
|
377
|
+
| `tokens` | `BubbleBreadcrumbSearchTokens` | `undefined` | Search tokens plus `breadcrumb*` tokens. |
|
|
378
|
+
|
|
379
|
+
The search components intentionally accept no `style` and render a fixed structure; refs point to the group `div`.
|
|
380
|
+
|
|
381
|
+
The toolbar, group, area and icon button accept `className`, `style`, and standard attributes for their underlying element. Toolbar, group, and area refs point to a `div`; icon button refs point to a `button`.
|
|
382
|
+
|
|
383
|
+
## Development
|
|
159
384
|
|
|
160
385
|
```bash
|
|
161
|
-
npm
|
|
386
|
+
npm ci
|
|
162
387
|
```
|
|
163
388
|
|
|
164
|
-
|
|
389
|
+
ESLint 10 uses `createEslintConfig` from `super-configs/eslint` with the React and Jest presets. Biome extends `super-configs/biome` for linting, formatting, and import organization.
|
|
165
390
|
|
|
166
391
|
```bash
|
|
167
|
-
npm run
|
|
392
|
+
npm run lint # Run ESLint with zero warnings allowed
|
|
393
|
+
npm run lint:fix # Apply ESLint fixes
|
|
394
|
+
npm run format:check # Check with Biome
|
|
395
|
+
npm run format # Apply Biome fixes
|
|
396
|
+
npm run typecheck # Check TypeScript types
|
|
397
|
+
npm test # Run Jest tests
|
|
398
|
+
npm run test:coverage # Generate a coverage report
|
|
399
|
+
npm run check # Run ESLint, Biome, TypeScript, and Jest
|
|
168
400
|
```
|
|
169
401
|
|
|
170
|
-
###
|
|
402
|
+
### Storybook
|
|
171
403
|
|
|
172
|
-
|
|
404
|
+
The story environment uses Storybook 10 and Vite 8.3.4. Explore components, themes, and adaptive shapes locally:
|
|
173
405
|
|
|
174
406
|
```bash
|
|
175
407
|
npm run storybook
|
|
176
408
|
```
|
|
177
409
|
|
|
178
|
-
|
|
410
|
+
Build the static Storybook site:
|
|
179
411
|
|
|
180
412
|
```bash
|
|
181
413
|
npm run build-storybook
|
|
182
414
|
```
|
|
183
415
|
|
|
184
|
-
|
|
416
|
+
### Library build
|
|
417
|
+
|
|
418
|
+
Build the library for distribution as ESM, CommonJS, and TypeScript declarations:
|
|
185
419
|
|
|
186
420
|
```bash
|
|
187
421
|
npm run build
|
|
188
422
|
```
|
|
189
423
|
|
|
190
|
-
|
|
424
|
+
Preview the npm package contents:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
npm pack --dry-run
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Releases and deployment
|
|
431
|
+
|
|
432
|
+
The workflows follow the same release pattern as `react-dockbar` and `super-configs`:
|
|
433
|
+
|
|
434
|
+
- **CI:** pull requests and pushes to branches other than `main` run type checks, tests, and builds with React 18 and 19. React 19 also runs ESLint, Biome, Storybook, and a package-content check. Pull request commits are validated with commitlint.
|
|
435
|
+
- **npm:** after CI passes on `main`, semantic-release calculates the version, generates `CHANGELOG.md` and GitHub release notes, updates package versions, creates a `vX.Y.Z` tag, and publishes `bubble-toolbar` with provenance.
|
|
436
|
+
- **GitHub Packages:** each new npm release is mirrored as `@eljijuna/bubble-toolbar` from its release tag. The name change happens only in the publishing job.
|
|
437
|
+
- **GitHub Pages:** every validated push to `main` deploys Storybook to [eljijuna.github.io/bubble-toolbar](https://eljijuna.github.io/bubble-toolbar/), even when there is no new package release. Deployment is independent of npm publishing.
|
|
438
|
+
|
|
439
|
+
### Conventional Commits
|
|
440
|
+
|
|
441
|
+
Use `type(optional-scope): description` for commits and squash-merge titles:
|
|
442
|
+
|
|
443
|
+
| Commit | Version change |
|
|
444
|
+
| :--- | :--- |
|
|
445
|
+
| `fix: correct activity cleanup` | Patch |
|
|
446
|
+
| `perf: reduce activity renders` | Patch |
|
|
447
|
+
| `feat: add activity colors` | Minor |
|
|
448
|
+
| `feat!: replace the group API` | Major |
|
|
449
|
+
| Any type with a `BREAKING CHANGE:` footer | Major |
|
|
450
|
+
| `docs:`, `test:`, `chore:`, `ci:`, `build:`, `refactor:`, `style:` without breaking changes | No release by themselves |
|
|
451
|
+
|
|
452
|
+
The highest applicable change determines the release. Without a previous release tag, semantic-release starts at **1.0.0**. Release commits use `chore(release): X.Y.Z [skip ci]` to avoid a release loop. Do not edit release versions or changelog entries manually.
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
printf 'feat: add activity colors\n' | npm run commitlint
|
|
456
|
+
npm run release:dry-run
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
The dry run needs access to the GitHub repository and publishing credentials to verify permissions. It does not publish a version. Release tooling uses Node.js 24 in CI; use Node.js 24.15 or newer for local release tooling.
|
|
460
|
+
|
|
461
|
+
### Repository configuration
|
|
462
|
+
|
|
463
|
+
- Set the Actions secret `NPM_TOKEN` to an npm granular token with publish access to `bubble-toolbar`, allowing the CI publishing workflow to bypass 2FA. It must belong to an account authorized to publish the package name.
|
|
464
|
+
- Keep **Settings → Pages → Build and deployment → Source** set to **GitHub Actions**.
|
|
465
|
+
- The workflows declare the required `GITHUB_TOKEN` permissions for releases, package publishing, and Pages. If branch protection blocks release commits, allow the release automation to update `main` using an appropriate GitHub App token.
|
|
466
|
+
- The **Release** workflow can also be dispatched manually on `main`. To retry a failed mirror after npm succeeded, rerun the failed GitHub Packages job.
|
|
467
|
+
|
|
468
|
+
Install the mirror with an npm configuration that routes its scope to GitHub Packages:
|
|
469
|
+
|
|
470
|
+
```ini
|
|
471
|
+
@eljijuna:registry=https://npm.pkg.github.com
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
npm install @eljijuna/bubble-toolbar
|
|
476
|
+
```
|
|
191
477
|
|
|
192
|
-
|
|
478
|
+
GitHub Packages installation requires GitHub authentication with package read access. Publishing and deployment happen when the workflows run on GitHub; local setup does not publish packages.
|
|
193
479
|
|
|
194
|
-
|
|
480
|
+
## License
|
|
195
481
|
|
|
482
|
+
[MIT](./LICENSE) © ElJijuna
|