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.
- package/CHANGELOG.md +20 -0
- package/README.md +11 -9
- package/dist/components/filter-presets-bar.d.ts +71 -0
- package/dist/components/filter-presets-bar.js +508 -0
- package/dist/components/reaction-button.d.ts +7 -2
- package/dist/components/reaction-button.js +59 -48
- package/dist/hooks.d.ts +10 -0
- package/dist/hooks.js +10 -0
- package/dist/index.d.ts +1 -23
- package/dist/index.js +1 -23
- package/dist/styles.css +1 -1
- package/package.json +11 -126
- package/dist/components/bouncing-dots-loader.d.ts +0 -19
- package/dist/components/bouncing-dots-loader.js +0 -47
- package/dist/components/confirm-delete-button.d.ts +0 -29
- package/dist/components/confirm-delete-button.js +0 -254
- package/dist/components/empty-state.d.ts +0 -24
- package/dist/components/empty-state.js +0 -37
- package/dist/components/error-state.d.ts +0 -20
- package/dist/components/error-state.js +0 -145
- package/dist/components/external-browser-handoff.d.ts +0 -27
- package/dist/components/external-browser-handoff.js +0 -306
- package/dist/components/info-popover.d.ts +0 -23
- package/dist/components/info-popover.js +0 -158
- package/dist/components/month-calendar-header.d.ts +0 -65
- package/dist/components/month-calendar-header.js +0 -337
- package/dist/components/month-grid.d.ts +0 -59
- package/dist/components/month-grid.js +0 -301
- package/dist/components/notification-bell.d.ts +0 -50
- package/dist/components/notification-bell.js +0 -338
- package/dist/components/notification-panel.d.ts +0 -56
- package/dist/components/notification-panel.js +0 -281
- package/dist/components/open-in-browser-cta.d.ts +0 -22
- package/dist/components/open-in-browser-cta.js +0 -61
- package/dist/components/pwa-update-control.d.ts +0 -18
- package/dist/components/pwa-update-control.js +0 -119
- package/dist/emoji-picker.d.ts +0 -27
- package/dist/emoji-picker.js +0 -283
- package/dist/hooks/use-minute-clock.d.ts +0 -23
- package/dist/hooks/use-minute-clock.js +0 -147
- package/dist/hooks/use-month-transition-direction.d.ts +0 -21
- package/dist/hooks/use-month-transition-direction.js +0 -110
- package/dist/lib/browser-navigation.d.ts +0 -21
- package/dist/lib/browser-navigation.js +0 -42
- package/dist/lib/in-app-browser.d.ts +0 -35
- package/dist/lib/in-app-browser.js +0 -80
- package/dist/lib/month-grid.d.ts +0 -42
- 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
|
-
-
|
|
180
|
-
- Cuenta
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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;
|