beez-ui 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +11 -9
  3. package/dist/components/filter-presets-bar.d.ts +71 -0
  4. package/dist/components/filter-presets-bar.js +508 -0
  5. package/dist/components/reaction-button.d.ts +7 -2
  6. package/dist/components/reaction-button.js +59 -48
  7. package/dist/hooks.d.ts +10 -0
  8. package/dist/hooks.js +10 -0
  9. package/dist/index.d.ts +1 -23
  10. package/dist/index.js +1 -23
  11. package/dist/styles.css +1 -1
  12. package/package.json +11 -126
  13. package/dist/components/bouncing-dots-loader.d.ts +0 -19
  14. package/dist/components/bouncing-dots-loader.js +0 -47
  15. package/dist/components/confirm-delete-button.d.ts +0 -29
  16. package/dist/components/confirm-delete-button.js +0 -254
  17. package/dist/components/empty-state.d.ts +0 -24
  18. package/dist/components/empty-state.js +0 -37
  19. package/dist/components/error-state.d.ts +0 -20
  20. package/dist/components/error-state.js +0 -145
  21. package/dist/components/external-browser-handoff.d.ts +0 -27
  22. package/dist/components/external-browser-handoff.js +0 -306
  23. package/dist/components/info-popover.d.ts +0 -23
  24. package/dist/components/info-popover.js +0 -158
  25. package/dist/components/month-calendar-header.d.ts +0 -65
  26. package/dist/components/month-calendar-header.js +0 -337
  27. package/dist/components/month-grid.d.ts +0 -59
  28. package/dist/components/month-grid.js +0 -301
  29. package/dist/components/notification-bell.d.ts +0 -50
  30. package/dist/components/notification-bell.js +0 -338
  31. package/dist/components/notification-panel.d.ts +0 -56
  32. package/dist/components/notification-panel.js +0 -281
  33. package/dist/components/open-in-browser-cta.d.ts +0 -22
  34. package/dist/components/open-in-browser-cta.js +0 -61
  35. package/dist/components/pwa-update-control.d.ts +0 -18
  36. package/dist/components/pwa-update-control.js +0 -119
  37. package/dist/emoji-picker.d.ts +0 -27
  38. package/dist/emoji-picker.js +0 -283
  39. package/dist/hooks/use-minute-clock.d.ts +0 -23
  40. package/dist/hooks/use-minute-clock.js +0 -147
  41. package/dist/hooks/use-month-transition-direction.d.ts +0 -21
  42. package/dist/hooks/use-month-transition-direction.js +0 -110
  43. package/dist/lib/browser-navigation.d.ts +0 -21
  44. package/dist/lib/browser-navigation.js +0 -42
  45. package/dist/lib/in-app-browser.d.ts +0 -35
  46. package/dist/lib/in-app-browser.js +0 -80
  47. package/dist/lib/month-grid.d.ts +0 -42
  48. package/dist/lib/month-grid.js +0 -74
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ Todos los cambios relevantes de beez-ui se documentan en este archivo con el for
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.0] - 2026-09-27
8
+
9
+ ### Added
10
+
11
+ - `ReactionButton` acepta `activeColor` para elegir el color del ícono activo (por defecto, el color destructivo).
12
+ - Entrypoint `beez-ui/hooks` con los hooks de uso general: `useIsMobile`, `useIsHydrated`, `useViewerTimeZone`, `usePrefersReducedMotion`, `useHorizontalSwipe` y `resolveHorizontalSwipe`.
13
+ - `FilterPresetsBar` y `FilterPresetSaveButton` para guardar, aplicar, editar y borrar consultas con nombre de `FilterQueryBar`, con `parseFilterPresets` para validar las que la app restaura de su almacenamiento.
14
+
15
+ ### Changed
16
+
17
+ - `useIsHydrated`, `useViewerTimeZone`, `usePrefersReducedMotion`, `useHorizontalSwipe`, `resolveHorizontalSwipe` y `HORIZONTAL_SWIPE_DIRECTION` se importan desde `beez-ui/hooks` en lugar de la raíz; `useIsMobile` sigue también en la raíz.
18
+
19
+ ### Removed
20
+
21
+ - Se retiran `EmptyState`, `ErrorState`, `BouncingDotsLoader` e `InfoPopover`: son envoltorios simples que cada app mantiene con su propio diseño.
22
+ - `getNameInitials` deja de ser público.
23
+ - Se retiran componentes atados a un solo producto que llegaron en 0.8.0: `ExternalBrowserHandoff`, `OpenInBrowserCta`, `detectInAppBrowser`, `buildExternalBrowserUrl`, `NotificationBell`, `NotificationPanel`, `MonthGrid`, `MonthCalendarHeader`, `PwaUpdateControl` y `ConfirmDeleteButton`. Quien los use debe mantener su propia versión.
24
+ - Se retiran los helpers `createMonthGridDays`, `splitCalendarWeeks`, `groupItemsByDateKey`, `shiftMonthKey`, `useMonthTransitionDirection`, `useMinuteClock`, `advanceMinuteClockTo`, `navigateToUrl`, `reloadCurrentPage`, `replaceCurrentUrlSearchParams` y `replaceCurrentUrlSearchParamValues`.
25
+ - Se retira el entrypoint opcional `beez-ui/emoji-picker` y la dependencia opcional `emoji-picker-react`.
26
+
7
27
  ## [0.8.2] - 2026-09-27
8
28
 
9
29
  ### Fixed
package/README.md CHANGED
@@ -173,19 +173,21 @@ Los componentes compartidos conservan el tema de LaTribu. Se incorporaron atribu
173
173
 
174
174
  ## Componentes de producto reutilizables
175
175
 
176
- Provienen de las carpetas de producto de LaTribu y agenda-mensual y quedaron desacoplados de su dominio: reciben datos ya resueltos y textos por props, con valores por defecto en español.
176
+ Provienen de las carpetas de producto de LaTribu y agenda-mensual y quedaron desacoplados de su dominio: reciben datos ya resueltos y textos por props, con valores por defecto en español. Solo se incorporan piezas genéricas que no fijan decisiones de un producto y que no son una composición trivial de primitivas existentes; los flujos propios de una app (notificaciones, calendarios de eventos, handoff de navegadores internos, actualización de PWA) y los envoltorios simples (estados vacío y de error, loaders, popovers de ayuda) se mantienen en esa app.
177
177
 
178
178
  - Movimiento: `PresenceSwap`, `AnimatedCount`, `AnimatedCollapse` y `AnimatedListItem`. Los tokens que usa la biblioteca se exportan desde la raíz y desde `beez-ui/motion-tokens`, así las apps animan sus superficies con las mismas curvas y springs en lugar de copiarlos.
179
- - Feedback: `BouncingDotsLoader`, `ProgressRing`, `EmptyState`, `ErrorState`, `InfoPopover`, `ConfirmDeleteButton` y `ReactionButton`. El loader y la entrada del título de `MonthCalendarHeader` usan keyframes CSS porque se renderizan en el servidor; el fallback de movimiento reducido los detiene.
180
- - Cuenta y sesión: `AccountMenu` (inicio de sesión mediante `signInHref` o `onSignIn`), `OpenInBrowserCta`, `ExternalBrowserHandoff` y los helpers `detectInAppBrowser` y `buildExternalBrowserUrl`.
179
+ - Indicadores: `ProgressRing` y `ReactionButton`. El color del ícono activo de la reacción se configura con `activeColor`.
180
+ - Cuenta: `AccountMenu`, con inicio de sesión mediante `signInHref` o `onSignIn`.
181
181
  - Contenido: `RichTextContent`, `RichMarkdownContent`, `RichLinkEditor` y `useRichLinkEditor`. Solo interpretan links en markdown, URLs sueltas, listas y negrita; el HTML del contenido nunca se renderiza como markup.
182
- - Notificaciones: `NotificationBell` y `NotificationPanel` reciben `NotificationPanelItem` (`id`, `title`, `detail`, `sentAtLabel`, `href`, `isUnread`); la app traduce sus notificaciones a ese formato. Con `surface="responsive"` se renderizan el popover y el sheet hasta hidratar, y después solo el que corresponde al viewport.
183
- - Calendario: `MonthGrid` y `MonthCalendarHeader` trabajan con claves `YYYY-MM` y `YYYY-MM-DD` sin zona horaria; la app decide en qué zona convierte cada instante en día. La navegación acepta `href` (link del router) o `onSelect`.
182
+ - Filtros guardados: `FilterPresetsBar` y `FilterPresetSaveButton` guardan, aplican, editan y borran consultas con nombre de `FilterQueryBar`. La app decide dónde persistirlas; `parseFilterPresets` valida lo que vuelve del almacenamiento.
184
183
  - Archivos: `FileUpload`, `FileUploadDropZone`, `FileUploadList` y `FileUploadItem` validan tipo y tamaño, pero no suben archivos: el progreso y los errores los informa la app.
185
- - PWA: `PwaUpdateControl` aparece solo cuando hay un service worker en espera y le envía `{ type: "SKIP_WAITING" }` (configurable con `skipWaitingMessage`).
186
- - Hooks y utilidades: `useIsHydrated`, `useMinuteClock`, `useViewerTimeZone`, `useHorizontalSwipe`, `usePrefersReducedMotion`, `formatFileSize`, `copyTextToClipboard`, `getNameInitials`, búsqueda difusa y helpers de URL.
184
+ - Utilidades: `formatFileSize`, `copyTextToClipboard` y búsqueda difusa.
187
185
 
188
- `EmojiPicker` vive en el entrypoint opcional `beez-ui/emoji-picker` para no sumar `emoji-picker-react` a quien no lo usa. Hay que instalarlo como dependencia de la app (`pnpm add emoji-picker-react`); la librería se carga recién al abrir el selector, nunca durante el render del servidor.
186
+ Los hooks de uso general se importan desde `beez-ui/hooks`: `useIsMobile`, `useIsHydrated`, `useViewerTimeZone`, `usePrefersReducedMotion` y `useHorizontalSwipe` (con `resolveHorizontalSwipe`). `useIsMobile` también sigue disponible desde la raíz.
187
+
188
+ ```tsx
189
+ import { useIsHydrated } from "beez-ui/hooks";
190
+ ```
189
191
 
190
192
  ## React Compiler
191
193
 
@@ -222,7 +224,7 @@ pnpm release:prepare
222
224
 
223
225
  `pnpm build` genera las declaraciones con TypeScript 7, JavaScript con Oxc/React Compiler y CSS con el CLI de Tailwind. `pnpm check` ejecuta ESLint 10, los typechecks separados de código y tests con TypeScript 7, y Vitest 5. `tests/tsconfig.json` incorpora los matchers de Testing Library y los tipos de Vite sin incluirlos en el código de producción. `test:browser` verifica una app React/Vite nativa y una app Next real en Chromium y WebKit, tanto en desktop como en móvil. Los tests unitarios importan los archivos compilados. Los consumidores de navegador no instalan plugins de Tailwind, y una prueba adicional sirve el tarball por HTTP sin procesadores CSS para verificar estilos computados y carga de fuentes. Una prueba adicional instala el tarball en un consumidor aislado sin Next y verifica render, filtrado y declaraciones públicas.
224
226
 
225
- Los tests resuelven `beez-ui`, `beez-ui/next`, `beez-ui/tanstack` y `beez-ui/emoji-picker` mediante rutas explícitas a las declaraciones compiladas en `tests/tsconfig.json`. Ejecutar `pnpm build` después de clonar o si falta `dist`; los comandos de validación completos ya lo hacen. Si el editor conserva diagnósticos anteriores después del build, reiniciar su servidor de TypeScript.
227
+ Los tests resuelven `beez-ui`, `beez-ui/next`, `beez-ui/tanstack` y `beez-ui/hooks` mediante rutas explícitas a las declaraciones compiladas en `tests/tsconfig.json`. Ejecutar `pnpm build` después de clonar o si falta `dist`; los comandos de validación completos ya lo hacen. Si el editor conserva diagnósticos anteriores después del build, reiniciar su servidor de TypeScript.
226
228
 
227
229
  El compilador `tsc` es TypeScript 7. Para `typescript-eslint`, se mantiene la [API de compatibilidad oficial de TypeScript 6](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0) mediante un alias; no reemplaza el compilador de los typechecks.
228
230
 
@@ -0,0 +1,71 @@
1
+ import type { FilterQualifierConfig } from "./filter-query-grammar.js";
2
+ /** A named, reusable query of the filter bar. */
3
+ export interface FilterPreset {
4
+ name: string;
5
+ query: string;
6
+ }
7
+ /**
8
+ * Validates presets restored from untrusted storage, trimming names and queries and dropping
9
+ * malformed or empty entries.
10
+ * @param value - Parsed stored value, such as the result of `JSON.parse`.
11
+ * @returns The valid presets, or an empty list when the value is not a list.
12
+ */
13
+ export declare function parseFilterPresets(value: unknown): FilterPreset[];
14
+ export interface FilterPresetLabels {
15
+ saveAction: string;
16
+ saveConfirm: string;
17
+ saveConfirmAriaLabel: string;
18
+ presetName: string;
19
+ presetNamePlaceholder: string;
20
+ presetQuery: string;
21
+ presetQueryPlaceholder: string;
22
+ savedPresets: string;
23
+ editConfirmAriaLabel: string;
24
+ cancel: string;
25
+ delete: string;
26
+ emptyNameError: string;
27
+ emptyCurrentQueryError: string;
28
+ emptyPresetQueryError: string;
29
+ duplicateNameError: string;
30
+ applyPresetAriaLabel: (presetName: string) => string;
31
+ editPresetAriaLabel: (presetName: string) => string;
32
+ deletePresetAriaLabel: (presetName: string) => string;
33
+ deleteConfirmation: (presetName: string) => string;
34
+ }
35
+ export declare const FILTER_PRESET_DEFAULT_LABELS: FilterPresetLabels;
36
+ export interface FilterPresetSaveButtonProps {
37
+ /** Whether the filter bar currently has a query worth saving. */
38
+ canSaveCurrentQuery: boolean;
39
+ /** Saves the current query under the given name. Returns `false` when there is no query to save. */
40
+ onSaveCurrentQuery: (presetName: string) => boolean;
41
+ labels?: Partial<FilterPresetLabels>;
42
+ }
43
+ /**
44
+ * Compact icon button that saves the current query as a named preset. Fits the action slot of
45
+ * the filter bar input.
46
+ * @param props - Save availability, save callback and optional labels.
47
+ * @returns The button and its naming popover.
48
+ */
49
+ export declare function FilterPresetSaveButton({ canSaveCurrentQuery, onSaveCurrentQuery, labels }: FilterPresetSaveButtonProps): import("react").JSX.Element;
50
+ export interface FilterPresetsBarProps {
51
+ presets: readonly FilterPreset[];
52
+ /** Qualifiers of the filter bar, offered as suggestions while editing a preset query. */
53
+ queryFilterConfigs: FilterQualifierConfig[];
54
+ onApplyPreset: (preset: FilterPreset) => void;
55
+ onDeletePreset: (presetName: string) => void;
56
+ /** Replaces the preset named `originalName` with the given name and query. */
57
+ onUpdatePreset: (update: {
58
+ name: string;
59
+ originalName: string;
60
+ query: string;
61
+ }) => void;
62
+ labels?: Partial<FilterPresetLabels>;
63
+ className?: string;
64
+ }
65
+ /**
66
+ * Renders one chip per saved preset: its name applies it, the pencil edits its name and query,
67
+ * and the cross deletes it after a confirmation. Renders nothing without presets.
68
+ * @param props - Presets, filter configs, callbacks and optional labels.
69
+ * @returns The presets row, or nothing.
70
+ */
71
+ export declare function FilterPresetsBar({ presets, queryFilterConfigs, onApplyPreset, onDeletePreset, onUpdatePreset, labels, className, }: FilterPresetsBarProps): import("react").JSX.Element | null;
@@ -0,0 +1,508 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /**
4
+ * Saved queries for `FilterQueryBar`: a button that stores the current query under a name and a
5
+ * row of chips to apply, edit or delete them. Persistence stays with the application.
6
+ */
7
+ import { useId, useState } from "react";
8
+ import { Bookmark, Pencil, X } from "lucide-react";
9
+ import { cn } from "../lib/utils.js";
10
+ import { Button } from "./button.js";
11
+ import { FilterQueryBar } from "./filter-query-bar.js";
12
+ import { Input } from "./input.js";
13
+ import { Label } from "./label.js";
14
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
15
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
16
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ const ENTER_KEY = "Enter";
18
+ /** Suggestions of the query editor render in their own popover, outside the edit popover. */
19
+ const NESTED_POPOVER_SELECTOR = "[data-slot=\"popover-content\"]";
20
+ /**
21
+ * Tells whether a value is a non-null object.
22
+ * @param value - Untrusted value.
23
+ * @returns Whether its properties can be read.
24
+ */
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null;
27
+ }
28
+ /**
29
+ * Validates presets restored from untrusted storage, trimming names and queries and dropping
30
+ * malformed or empty entries.
31
+ * @param value - Parsed stored value, such as the result of `JSON.parse`.
32
+ * @returns The valid presets, or an empty list when the value is not a list.
33
+ */
34
+ export function parseFilterPresets(value) {
35
+ if (!Array.isArray(value)) return [];
36
+ return value.flatMap((entry) => {
37
+ if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.query !== "string") return [];
38
+ const name = entry.name.trim();
39
+ const query = entry.query.trim();
40
+ return name && query ? [{
41
+ name,
42
+ query
43
+ }] : [];
44
+ });
45
+ }
46
+ export const FILTER_PRESET_DEFAULT_LABELS = {
47
+ saveAction: "Guardar filtro",
48
+ saveConfirm: "Guardar",
49
+ saveConfirmAriaLabel: "Guardar filtro con nombre",
50
+ presetName: "Nombre del filtro",
51
+ presetNamePlaceholder: "Mi filtro",
52
+ presetQuery: "Búsqueda del filtro",
53
+ presetQueryPlaceholder: "",
54
+ savedPresets: "Filtros guardados:",
55
+ editConfirmAriaLabel: "Guardar cambios del filtro",
56
+ cancel: "Cancelar",
57
+ delete: "Eliminar",
58
+ emptyNameError: "Ingresá un nombre para el filtro.",
59
+ emptyCurrentQueryError: "Escribí una búsqueda en la barra antes de guardarla.",
60
+ emptyPresetQueryError: "Ingresá una búsqueda para el filtro.",
61
+ duplicateNameError: "Ya existe un filtro con ese nombre.",
62
+ applyPresetAriaLabel: (presetName) => `Aplicar filtro guardado ${presetName}`,
63
+ editPresetAriaLabel: (presetName) => `Editar filtro guardado ${presetName}`,
64
+ deletePresetAriaLabel: (presetName) => `Eliminar filtro guardado ${presetName}`,
65
+ deleteConfirmation: (presetName) => `¿Querés eliminar el filtro guardado "${presetName}"?`
66
+ };
67
+ const FORM_POPOVER_CLASS_NAME = "grid w-64 gap-2";
68
+ const ERROR_CLASS_NAME = "m-0 text-xs text-destructive";
69
+ const CHIP_ICON_BUTTON_CLASS_NAME = "inline-flex cursor-pointer items-center border-0 bg-transparent py-[0.35rem] text-muted-foreground [&_svg]:size-3";
70
+ /**
71
+ * Compact icon button that saves the current query as a named preset. Fits the action slot of
72
+ * the filter bar input.
73
+ * @param props - Save availability, save callback and optional labels.
74
+ * @returns The button and its naming popover.
75
+ */
76
+ export function FilterPresetSaveButton(t0) {
77
+ const $ = _c(45);
78
+ const { canSaveCurrentQuery, onSaveCurrentQuery, labels } = t0;
79
+ let t1;
80
+ if ($[0] !== labels) {
81
+ t1 = {
82
+ ...FILTER_PRESET_DEFAULT_LABELS,
83
+ ...labels
84
+ };
85
+ $[0] = labels;
86
+ $[1] = t1;
87
+ } else {
88
+ t1 = $[1];
89
+ }
90
+ const resolvedLabels = t1;
91
+ const nameInputId = useId();
92
+ const [isOpen, setIsOpen] = useState(false);
93
+ const [nameDraft, setNameDraft] = useState("");
94
+ const [error, setError] = useState(null);
95
+ let t2;
96
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
97
+ t2 = (nextOpen) => {
98
+ setIsOpen(nextOpen);
99
+ if (!nextOpen) {
100
+ setNameDraft("");
101
+ setError(null);
102
+ }
103
+ };
104
+ $[2] = t2;
105
+ } else {
106
+ t2 = $[2];
107
+ }
108
+ const handleOpenChange = t2;
109
+ let t3;
110
+ if ($[3] !== nameDraft || $[4] !== onSaveCurrentQuery || $[5] !== resolvedLabels.emptyCurrentQueryError || $[6] !== resolvedLabels.emptyNameError) {
111
+ t3 = () => {
112
+ const presetName = nameDraft.trim();
113
+ if (!presetName) {
114
+ setError(resolvedLabels.emptyNameError);
115
+ return;
116
+ }
117
+ if (!onSaveCurrentQuery(presetName)) {
118
+ setError(resolvedLabels.emptyCurrentQueryError);
119
+ return;
120
+ }
121
+ handleOpenChange(false);
122
+ };
123
+ $[3] = nameDraft;
124
+ $[4] = onSaveCurrentQuery;
125
+ $[5] = resolvedLabels.emptyCurrentQueryError;
126
+ $[6] = resolvedLabels.emptyNameError;
127
+ $[7] = t3;
128
+ } else {
129
+ t3 = $[7];
130
+ }
131
+ const handleConfirmSave = t3;
132
+ const t4 = !canSaveCurrentQuery;
133
+ let t5;
134
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
135
+ t5 = /* @__PURE__ */ _jsx(Bookmark, { "aria-hidden": "true" });
136
+ $[8] = t5;
137
+ } else {
138
+ t5 = $[8];
139
+ }
140
+ let t6;
141
+ if ($[9] !== resolvedLabels.saveAction || $[10] !== t4) {
142
+ t6 = /* @__PURE__ */ _jsx(TooltipTrigger, {
143
+ asChild: true,
144
+ children: /* @__PURE__ */ _jsx(PopoverTrigger, {
145
+ asChild: true,
146
+ children: /* @__PURE__ */ _jsx(Button, {
147
+ "data-slot": "filter-preset-save-button",
148
+ "aria-label": resolvedLabels.saveAction,
149
+ disabled: t4,
150
+ size: "icon-xs",
151
+ type: "button",
152
+ variant: "ghost",
153
+ children: t5
154
+ })
155
+ })
156
+ });
157
+ $[9] = resolvedLabels.saveAction;
158
+ $[10] = t4;
159
+ $[11] = t6;
160
+ } else {
161
+ t6 = $[11];
162
+ }
163
+ let t7;
164
+ if ($[12] !== resolvedLabels.saveAction) {
165
+ t7 = /* @__PURE__ */ _jsx(TooltipContent, { children: resolvedLabels.saveAction });
166
+ $[12] = resolvedLabels.saveAction;
167
+ $[13] = t7;
168
+ } else {
169
+ t7 = $[13];
170
+ }
171
+ let t8;
172
+ if ($[14] !== t6 || $[15] !== t7) {
173
+ t8 = /* @__PURE__ */ _jsxs(Tooltip, { children: [t6, t7] });
174
+ $[14] = t6;
175
+ $[15] = t7;
176
+ $[16] = t8;
177
+ } else {
178
+ t8 = $[16];
179
+ }
180
+ let t9;
181
+ if ($[17] !== nameInputId || $[18] !== resolvedLabels.presetName) {
182
+ t9 = /* @__PURE__ */ _jsx(Label, {
183
+ htmlFor: nameInputId,
184
+ children: resolvedLabels.presetName
185
+ });
186
+ $[17] = nameInputId;
187
+ $[18] = resolvedLabels.presetName;
188
+ $[19] = t9;
189
+ } else {
190
+ t9 = $[19];
191
+ }
192
+ let t10;
193
+ if ($[20] !== error) {
194
+ t10 = (event) => {
195
+ setNameDraft(event.target.value);
196
+ if (error) {
197
+ setError(null);
198
+ }
199
+ };
200
+ $[20] = error;
201
+ $[21] = t10;
202
+ } else {
203
+ t10 = $[21];
204
+ }
205
+ let t11;
206
+ if ($[22] !== handleConfirmSave) {
207
+ t11 = (event_0) => {
208
+ if (event_0.key !== ENTER_KEY) {
209
+ return;
210
+ }
211
+ event_0.preventDefault();
212
+ handleConfirmSave();
213
+ };
214
+ $[22] = handleConfirmSave;
215
+ $[23] = t11;
216
+ } else {
217
+ t11 = $[23];
218
+ }
219
+ let t12;
220
+ if ($[24] !== nameDraft || $[25] !== nameInputId || $[26] !== resolvedLabels.presetNamePlaceholder || $[27] !== t10 || $[28] !== t11) {
221
+ t12 = /* @__PURE__ */ _jsx(Input, {
222
+ id: nameInputId,
223
+ onChange: t10,
224
+ onKeyDown: t11,
225
+ placeholder: resolvedLabels.presetNamePlaceholder,
226
+ type: "text",
227
+ value: nameDraft
228
+ });
229
+ $[24] = nameDraft;
230
+ $[25] = nameInputId;
231
+ $[26] = resolvedLabels.presetNamePlaceholder;
232
+ $[27] = t10;
233
+ $[28] = t11;
234
+ $[29] = t12;
235
+ } else {
236
+ t12 = $[29];
237
+ }
238
+ let t13;
239
+ if ($[30] !== error) {
240
+ t13 = error ? /* @__PURE__ */ _jsx("p", {
241
+ className: ERROR_CLASS_NAME,
242
+ role: "alert",
243
+ children: error
244
+ }) : null;
245
+ $[30] = error;
246
+ $[31] = t13;
247
+ } else {
248
+ t13 = $[31];
249
+ }
250
+ let t14;
251
+ if ($[32] !== handleConfirmSave || $[33] !== resolvedLabels.saveConfirm || $[34] !== resolvedLabels.saveConfirmAriaLabel) {
252
+ t14 = /* @__PURE__ */ _jsx(Button, {
253
+ "aria-label": resolvedLabels.saveConfirmAriaLabel,
254
+ onClick: handleConfirmSave,
255
+ size: "sm",
256
+ type: "button",
257
+ children: resolvedLabels.saveConfirm
258
+ });
259
+ $[32] = handleConfirmSave;
260
+ $[33] = resolvedLabels.saveConfirm;
261
+ $[34] = resolvedLabels.saveConfirmAriaLabel;
262
+ $[35] = t14;
263
+ } else {
264
+ t14 = $[35];
265
+ }
266
+ let t15;
267
+ if ($[36] !== t12 || $[37] !== t13 || $[38] !== t14 || $[39] !== t9) {
268
+ t15 = /* @__PURE__ */ _jsxs(PopoverContent, {
269
+ align: "end",
270
+ className: FORM_POPOVER_CLASS_NAME,
271
+ children: [
272
+ t9,
273
+ t12,
274
+ t13,
275
+ t14
276
+ ]
277
+ });
278
+ $[36] = t12;
279
+ $[37] = t13;
280
+ $[38] = t14;
281
+ $[39] = t9;
282
+ $[40] = t15;
283
+ } else {
284
+ t15 = $[40];
285
+ }
286
+ let t16;
287
+ if ($[41] !== isOpen || $[42] !== t15 || $[43] !== t8) {
288
+ t16 = /* @__PURE__ */ _jsxs(Popover, {
289
+ onOpenChange: handleOpenChange,
290
+ open: isOpen,
291
+ children: [t8, t15]
292
+ });
293
+ $[41] = isOpen;
294
+ $[42] = t15;
295
+ $[43] = t8;
296
+ $[44] = t16;
297
+ } else {
298
+ t16 = $[44];
299
+ }
300
+ return t16;
301
+ }
302
+ /**
303
+ * Renders one chip per saved preset: its name applies it, the pencil edits its name and query,
304
+ * and the cross deletes it after a confirmation. Renders nothing without presets.
305
+ * @param props - Presets, filter configs, callbacks and optional labels.
306
+ * @returns The presets row, or nothing.
307
+ */
308
+ export function FilterPresetsBar(t0) {
309
+ const $ = _c(4);
310
+ const { presets, queryFilterConfigs, onApplyPreset, onDeletePreset, onUpdatePreset, labels, className } = t0;
311
+ const resolvedLabels = {
312
+ ...FILTER_PRESET_DEFAULT_LABELS,
313
+ ...labels
314
+ };
315
+ const nameInputId = useId();
316
+ const [editingPresetName, setEditingPresetName] = useState(null);
317
+ const [nameDraft, setNameDraft] = useState("");
318
+ const [queryDraft, setQueryDraft] = useState("");
319
+ const [editError, setEditError] = useState(null);
320
+ const [deletingPresetName, setDeletingPresetName] = useState(null);
321
+ let t1;
322
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
323
+ t1 = (preset, nextOpen) => {
324
+ setEditError(null);
325
+ if (!nextOpen) {
326
+ setEditingPresetName(null);
327
+ return;
328
+ }
329
+ setEditingPresetName(preset.name);
330
+ setNameDraft(preset.name);
331
+ setQueryDraft(preset.query);
332
+ };
333
+ $[0] = t1;
334
+ } else {
335
+ t1 = $[0];
336
+ }
337
+ const handleEditOpenChange = t1;
338
+ const handleConfirmEdit = () => {
339
+ if (editingPresetName === null) {
340
+ return;
341
+ }
342
+ const name = nameDraft.trim();
343
+ const query = queryDraft.trim();
344
+ if (!name) {
345
+ setEditError(resolvedLabels.emptyNameError);
346
+ return;
347
+ }
348
+ if (!query) {
349
+ setEditError(resolvedLabels.emptyPresetQueryError);
350
+ return;
351
+ }
352
+ if (presets.some((preset_0) => preset_0.name === name && preset_0.name !== editingPresetName)) {
353
+ setEditError(resolvedLabels.duplicateNameError);
354
+ return;
355
+ }
356
+ onUpdatePreset({
357
+ name,
358
+ originalName: editingPresetName,
359
+ query
360
+ });
361
+ setEditingPresetName(null);
362
+ };
363
+ if (presets.length === 0) {
364
+ return null;
365
+ }
366
+ let t2;
367
+ if ($[1] !== className) {
368
+ t2 = cn("flex flex-wrap items-center gap-[0.4rem]", className);
369
+ $[1] = className;
370
+ $[2] = t2;
371
+ } else {
372
+ t2 = $[2];
373
+ }
374
+ let t3;
375
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
376
+ t3 = /* @__PURE__ */ _jsx(Bookmark, { "aria-hidden": "true" });
377
+ $[3] = t3;
378
+ } else {
379
+ t3 = $[3];
380
+ }
381
+ return /* @__PURE__ */ _jsxs("div", {
382
+ "data-slot": "filter-presets-bar",
383
+ className: t2,
384
+ children: [/* @__PURE__ */ _jsxs("span", {
385
+ className: "inline-flex items-center gap-[0.3rem] text-xs font-medium text-muted-foreground [&_svg]:size-[0.8rem]",
386
+ children: [t3, resolvedLabels.savedPresets]
387
+ }), presets.map((preset_1) => /* @__PURE__ */ _jsxs("span", {
388
+ "data-slot": "filter-preset-chip",
389
+ className: "inline-flex items-center overflow-hidden rounded-full border border-border bg-muted text-xs leading-none",
390
+ children: [
391
+ /* @__PURE__ */ _jsx("button", {
392
+ "aria-label": resolvedLabels.applyPresetAriaLabel(preset_1.name),
393
+ className: "max-w-48 cursor-pointer truncate border-0 bg-transparent py-[0.35rem] pr-1 pl-[0.65rem] text-inherit hover:text-primary focus-visible:text-primary",
394
+ onClick: () => onApplyPreset(preset_1),
395
+ title: preset_1.query,
396
+ type: "button",
397
+ children: preset_1.name
398
+ }),
399
+ /* @__PURE__ */ _jsxs(Popover, {
400
+ onOpenChange: (nextOpen_0) => handleEditOpenChange(preset_1, nextOpen_0),
401
+ open: editingPresetName === preset_1.name,
402
+ children: [/* @__PURE__ */ _jsx(PopoverTrigger, {
403
+ asChild: true,
404
+ children: /* @__PURE__ */ _jsx("button", {
405
+ "aria-label": resolvedLabels.editPresetAriaLabel(preset_1.name),
406
+ className: cn(CHIP_ICON_BUTTON_CLASS_NAME, "px-[0.15rem] hover:text-primary focus-visible:text-primary"),
407
+ type: "button",
408
+ children: /* @__PURE__ */ _jsx(Pencil, { "aria-hidden": "true" })
409
+ })
410
+ }), /* @__PURE__ */ _jsxs(PopoverContent, {
411
+ align: "start",
412
+ className: FORM_POPOVER_CLASS_NAME,
413
+ onInteractOutside: _temp,
414
+ children: [
415
+ /* @__PURE__ */ _jsx(Label, {
416
+ htmlFor: nameInputId,
417
+ children: resolvedLabels.presetName
418
+ }),
419
+ /* @__PURE__ */ _jsx(Input, {
420
+ id: nameInputId,
421
+ onChange: (event_0) => {
422
+ setNameDraft(event_0.target.value);
423
+ if (editError) {
424
+ setEditError(null);
425
+ }
426
+ },
427
+ type: "text",
428
+ value: nameDraft
429
+ }),
430
+ /* @__PURE__ */ _jsx("span", {
431
+ className: "text-sm leading-none font-medium",
432
+ children: resolvedLabels.presetQuery
433
+ }),
434
+ /* @__PURE__ */ _jsx(FilterQueryBar, {
435
+ ariaLabel: resolvedLabels.presetQuery,
436
+ configs: queryFilterConfigs,
437
+ onValueChange: (nextQuery) => {
438
+ setQueryDraft(nextQuery);
439
+ if (editError) {
440
+ setEditError(null);
441
+ }
442
+ },
443
+ placeholder: resolvedLabels.presetQueryPlaceholder,
444
+ value: queryDraft
445
+ }),
446
+ editError ? /* @__PURE__ */ _jsx("p", {
447
+ className: ERROR_CLASS_NAME,
448
+ role: "alert",
449
+ children: editError
450
+ }) : null,
451
+ /* @__PURE__ */ _jsx(Button, {
452
+ "aria-label": resolvedLabels.editConfirmAriaLabel,
453
+ onClick: handleConfirmEdit,
454
+ size: "sm",
455
+ type: "button",
456
+ children: resolvedLabels.saveConfirm
457
+ })
458
+ ]
459
+ })]
460
+ }),
461
+ /* @__PURE__ */ _jsxs(Popover, {
462
+ onOpenChange: (nextOpen_1) => setDeletingPresetName(nextOpen_1 ? preset_1.name : null),
463
+ open: deletingPresetName === preset_1.name,
464
+ children: [/* @__PURE__ */ _jsx(PopoverTrigger, {
465
+ asChild: true,
466
+ children: /* @__PURE__ */ _jsx("button", {
467
+ "aria-label": resolvedLabels.deletePresetAriaLabel(preset_1.name),
468
+ className: cn(CHIP_ICON_BUTTON_CLASS_NAME, "pr-2 pl-[0.15rem] hover:text-destructive focus-visible:text-destructive"),
469
+ type: "button",
470
+ children: /* @__PURE__ */ _jsx(X, { "aria-hidden": "true" })
471
+ })
472
+ }), /* @__PURE__ */ _jsxs(PopoverContent, {
473
+ align: "start",
474
+ className: "grid w-68 gap-3",
475
+ children: [/* @__PURE__ */ _jsx("p", {
476
+ className: "m-0 text-sm",
477
+ children: resolvedLabels.deleteConfirmation(preset_1.name)
478
+ }), /* @__PURE__ */ _jsxs("div", {
479
+ className: "flex justify-end gap-2",
480
+ children: [/* @__PURE__ */ _jsx(Button, {
481
+ onClick: () => setDeletingPresetName(null),
482
+ size: "sm",
483
+ type: "button",
484
+ variant: "outline",
485
+ children: resolvedLabels.cancel
486
+ }), /* @__PURE__ */ _jsx(Button, {
487
+ onClick: () => {
488
+ setDeletingPresetName(null);
489
+ onDeletePreset(preset_1.name);
490
+ },
491
+ size: "sm",
492
+ type: "button",
493
+ variant: "destructive",
494
+ children: resolvedLabels.delete
495
+ })]
496
+ })]
497
+ })]
498
+ })
499
+ ]
500
+ }, preset_1.name))]
501
+ });
502
+ }
503
+ function _temp(event) {
504
+ const interactionTarget = event.target;
505
+ if (interactionTarget instanceof Element && interactionTarget.closest(NESTED_POPOVER_SELECTOR)) {
506
+ event.preventDefault();
507
+ }
508
+ }
@@ -10,8 +10,13 @@ export interface ReactionButtonProps {
10
10
  count: number;
11
11
  onClick: (event: MouseEvent<HTMLButtonElement>) => void;
12
12
  isDisabled?: boolean;
13
- /** Icon of the reaction. While active it is filled with the destructive color; override it with `[&_svg]:` classes. */
13
+ /** Icon of the reaction; it is filled with `activeColor` while active. */
14
14
  icon?: ComponentType<SVGProps<SVGSVGElement>>;
15
+ /**
16
+ * CSS color of the active icon, such as `var(--primary)` or `currentColor` to follow the
17
+ * button text. Defaults to the destructive color, the usual tone of a like.
18
+ */
19
+ activeColor?: string;
15
20
  /** Formats the visible count, for example with a locale-aware compact format. */
16
21
  formatCount?: (count: number) => string;
17
22
  className?: string;
@@ -23,4 +28,4 @@ export interface ReactionButtonProps {
23
28
  * @param props - Accessible name, state, count, click handler and optional icon.
24
29
  * @returns The reaction button.
25
30
  */
26
- export declare function ReactionButton({ ariaLabel, isActive, count, onClick, isDisabled, icon: Icon, formatCount, className, }: ReactionButtonProps): import("react").JSX.Element;
31
+ export declare function ReactionButton({ ariaLabel, isActive, count, onClick, isDisabled, icon: Icon, formatCount, activeColor, className, }: ReactionButtonProps): import("react").JSX.Element;