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 ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ Release entries are generated automatically by semantic-release from Conventional Commits.
package/README.md CHANGED
@@ -1,195 +1,482 @@
1
1
  # 🫧 Bubble Toolbar
2
2
 
3
- > Componente de barra flotante responsiva con efecto **blurred glass** (glassmorphism) para **React 18 y 19** con **TypeScript**, organizado por grupos y con morfología adaptativa inteligente (círculo ↔ cápsula).
3
+ [![npm version](https://img.shields.io/npm/v/bubble-toolbar)](https://www.npmjs.com/package/bubble-toolbar)
4
+ [![React](https://img.shields.io/badge/React-18%20%7C%2019-61DAFB?logo=react&logoColor=white)](https://react.dev/)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](./tsconfig.json)
6
+ [![Storybook](https://img.shields.io/badge/Storybook-10-FF4785?logo=storybook&logoColor=white)](./.storybook/main.ts)
7
+ [![ESLint](https://img.shields.io/badge/ESLint-10-4B32C3?logo=eslint&logoColor=white)](./eslint.config.mjs)
8
+ [![Biome](https://img.shields.io/badge/Biome-2-60A5FA?logo=biome&logoColor=white)](./biome.json)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green)](./LICENSE)
4
10
 
5
- ![Bubble Toolbar Morphology](https://raw.githubusercontent.com/ElJijuna/bubble-toolbar/main/assets/banner.png)
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
- ## ✨ Características
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
- - ⚛️ **Compatibilidad Total**: Diseñado y probado con **React 18** y **React 19** usando `forwardRef` y tipado estricto en TypeScript.
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
- # o con yarn / pnpm / bun
26
+ # Or use pnpm:
27
27
  pnpm add bubble-toolbar
28
28
  ```
29
29
 
30
- ---
30
+ ## Quick start
31
31
 
32
- ## 🚀 Uso Rápido
32
+ ### Group arrays
33
33
 
34
- ### Ejemplo con definición por grupos (prop `groups`)
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
- Tal como en tu especificación:
63
+ ### Compound components
37
64
 
38
65
  ```tsx
39
- import React from 'react';
40
- import { BubbleToolbar, BubbleIconButton, BubbleArea } from 'bubble-toolbar';
41
- import 'bubble-toolbar/styles.css'; // Opcional, los estilos inline glass ya vienen incluidos
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
- <BubbleToolbar
46
- position="bottom"
47
- theme="glass"
48
- groups={[
49
- // Grupo 1: 1 solo nodo => Círculo
50
- [<Logo key="logo" />],
51
-
52
- // Grupo 2: Múltiples nodos => Rectángulo redondeado (Pill)
53
- [
54
- <BubbleIconButton key="search" name="search" />,
55
- <BubbleArea key="area">
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
- ### Ejemplo con Componentes Compuestos (`BubbleToolbar.Group`)
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 { BubbleToolbar, BubbleIconButton, BubbleArea } from 'bubble-toolbar';
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 FloatingMenu() {
221
+ export function Header({ location, navigate }) {
76
222
  return (
77
- <BubbleToolbar position="bottom" theme="glass">
78
- {/* 1 nodo: Círculo */}
79
- <BubbleToolbar.Group>
80
- <Logo />
81
- </BubbleToolbar.Group>
82
-
83
- {/* 2 nodos: Rectángulo redondeado (Cápsula) */}
84
- <BubbleToolbar.Group>
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
- ## 📐 Morfología Visual
248
+ Toolbar tokens, set on `:root` or any ancestor:
103
249
 
104
- | Contenido del Grupo | Forma Resultante | Comportamiento |
105
- | :--- | :---: | :--- |
106
- | **1 Nodo** (`[<Logo />]`) | 🟡 **Círculo** | Ancho y alto fijados a `--bubble-size` (por defecto `48px`). `aspect-ratio: 1 / 1`. Contenido centrado. |
107
- | **> 1 Nodo** (`[<Search />, <Area />]`) | 💊 **Cápsula / Pill** | Altura fijada a `--bubble-size`, ancho dinámico según contenido con padding lateral. Los extremos redondeados coinciden exactamente con el diámetro de las burbujas circulares adyacentes para una armonía visual simétrica. |
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
- ## 🛠️ API Reference
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 | Tipo | Por defecto | Descripción |
116
- | :--- | :--- | :---: | :--- |
117
- | `groups` | `ReactNode[][]` | `undefined` | Arreglo de arreglos con los nodos agrupados. |
118
- | `children` | `ReactNode` | `undefined` | Elementos JSX hijos (ej. `<BubbleToolbar.Group>`). |
119
- | `position` | `'bottom' \| 'top' \| 'static' \| 'custom'` | `'bottom'` | Posicionamiento de la barra en pantalla. |
120
- | `theme` | `'glass' \| 'light' \| 'dark' \| 'auto'` | `'glass'` | Estilo visual y translucidez. |
121
- | `size` | `'sm' \| 'md' \| 'lg' \| number` | `'md'` | Altura de la barra / diámetro de los círculos (40px, 48px, 56px o px numérico). |
122
- | `blurred` | `boolean` | `true` | Activa el efecto frosted glass (`backdrop-filter: blur`). |
123
- | `blurAmount`| `number \| string` | `16` | Cantidad de desenfoque en píxeles. |
124
- | `groupGap` | `number \| string` | `8` | Espacio entre cada burbuja/grupo. |
125
- | `itemGap` | `number \| string` | `8` | Espacio entre elementos dentro de un grupo cápsula. |
126
- | `bordered` | `boolean` | `true` | Muestra bordes sutiles con brillo frosted. |
127
- | `shadow` | `boolean \| string` | `true` | Sombra flotante proyectada. |
128
-
129
- ### `<BubbleGroup />` (o `<BubbleToolbar.Group />`)
130
-
131
- | Prop | Tipo | Por defecto | Descripción |
132
- | :--- | :--- | :---: | :--- |
133
- | `children` | `ReactNode` | `undefined` | Nodos que contiene el grupo. |
134
- | `shape` | `'circle' \| 'pill'` | Automático | Fuerza una forma específica omitiendo el cálculo automático por cantidad de nodos. |
135
- | `size` | `'sm' \| 'md' \| 'lg' \| number` | Heredado | Permite sobrescribir el tamaño para este grupo. |
136
- | `itemGap` | `number \| string` | Heredado | Espaciado entre elementos del grupo. |
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 | Tipo | Por defecto | Descripción |
141
- | :--- | :--- | :---: | :--- |
142
- | `name` | `'search' \| 'theme' \| 'chat' \| 'notifications' \| 'logo' \| 'close'` | `undefined` | Nombre del icono vectorial SVG pre-integrado. |
143
- | `icon` | `ReactNode` | `undefined` | Icono SVG personalizado. |
144
- | `badge` | `ReactNode \| number \| string` | `undefined` | Indicador o contador de notificación. |
145
- | `active` | `boolean` | `false` | Estado activo / seleccionado. |
146
- | `tooltip` | `string` | `undefined` | Texto emergente y accesible. |
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
- ## 🧪 Pruebas con Jest
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
- Ejecutar la suite completa de pruebas:
354
+ ### `<BubbleSearch />` / `<BubbleToolbar.Search />`
153
355
 
154
- ```bash
155
- npm test
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
- Ejecutar con reporte de cobertura de código:
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 run test:coverage
386
+ npm ci
162
387
  ```
163
388
 
164
- Ejecutar chequeo de tipos en TypeScript:
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 typecheck
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
- ### Ejecutar Storybook en local
402
+ ### Storybook
171
403
 
172
- Para explorar visualmente los componentes, probar el cambio dinámico de formas y los temas:
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
- Compilar Storybook estático:
410
+ Build the static Storybook site:
179
411
 
180
412
  ```bash
181
413
  npm run build-storybook
182
414
  ```
183
415
 
184
- Compilar la librería para distribución (ESM, CJS y `.d.ts`):
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
- ## 📄 Licencia
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
- MIT © ElJijuna
480
+ ## License
195
481
 
482
+ [MIT](./LICENSE) © ElJijuna