beez-ui 0.8.1 → 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 +26 -0
- package/README.md +11 -9
- package/dist/components/animated-collapse.js +9 -3
- package/dist/components/animated-list-item.js +37 -34
- package/dist/components/filter-presets-bar.d.ts +71 -0
- package/dist/components/filter-presets-bar.js +508 -0
- package/dist/components/presence-swap.js +38 -51
- 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,32 @@ 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
|
+
|
|
27
|
+
## [0.8.2] - 2026-09-27
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Con movimiento reducido, `PresenceSwap`, `AnimatedListItem` y `AnimatedCollapse` conservan un fundido breve en lugar de cambiar sin transición; `PresenceSwap` ya no queda trabado en el estado anterior cuando su contenido cambia varias veces seguidas (por ejemplo, error → cargando → cargado).
|
|
32
|
+
|
|
7
33
|
## [0.8.1] - 2026-09-27
|
|
8
34
|
|
|
9
35
|
### Changed
|
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
|
|
|
@@ -17,8 +17,14 @@ const COLLAPSE_TRANSITION = {
|
|
|
17
17
|
ease: MOTION_EASE
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
/** Reduced motion
|
|
21
|
-
const
|
|
20
|
+
/** Reduced motion applies the height at once but keeps the short fade, so exits always settle. */
|
|
21
|
+
const REDUCED_MOTION_COLLAPSE_TRANSITION = {
|
|
22
|
+
height: { duration: 0 },
|
|
23
|
+
opacity: {
|
|
24
|
+
duration: MOTION_TIMING.panel,
|
|
25
|
+
ease: MOTION_EASE
|
|
26
|
+
}
|
|
27
|
+
};
|
|
22
28
|
/**
|
|
23
29
|
* Renders `children` only while `isOpen`, animating height and opacity.
|
|
24
30
|
* Keep padding inside `children`: the animated wrapper collapses to zero height.
|
|
@@ -50,7 +56,7 @@ export function AnimatedCollapse(t0) {
|
|
|
50
56
|
height: 0,
|
|
51
57
|
opacity: 0
|
|
52
58
|
},
|
|
53
|
-
transition: shouldReduceMotion ?
|
|
59
|
+
transition: shouldReduceMotion ? REDUCED_MOTION_COLLAPSE_TRANSITION : COLLAPSE_TRANSITION,
|
|
54
60
|
onAnimationStart: () => setIsSettled(false),
|
|
55
61
|
onAnimationComplete: () => setIsSettled(true),
|
|
56
62
|
children
|
|
@@ -19,8 +19,6 @@ const LIST_ITEM_EXIT_TRANSITION = {
|
|
|
19
19
|
duration: MOTION_TIMING.exit,
|
|
20
20
|
ease: MOTION_EASE
|
|
21
21
|
};
|
|
22
|
-
/** Reduced motion keeps enter/exit semantics (inert while leaving) but without travel or delay. */
|
|
23
|
-
const INSTANT_TRANSITION = { duration: 0 };
|
|
24
22
|
/**
|
|
25
23
|
* Wrap the list in `<AnimatePresence initial={false}>` so only items added after the
|
|
26
24
|
* first render animate in, and removed items animate out before leaving the DOM.
|
|
@@ -30,7 +28,7 @@ const INSTANT_TRANSITION = { duration: 0 };
|
|
|
30
28
|
* @returns The animated item.
|
|
31
29
|
*/
|
|
32
30
|
export function AnimatedListItem(t0) {
|
|
33
|
-
const $ = _c(
|
|
31
|
+
const $ = _c(24);
|
|
34
32
|
let children;
|
|
35
33
|
let className;
|
|
36
34
|
let elementProps;
|
|
@@ -56,6 +54,8 @@ export function AnimatedListItem(t0) {
|
|
|
56
54
|
const MotionElement = ANIMATED_LIST_ITEM_ELEMENTS[as];
|
|
57
55
|
const isPresent = useIsPresent();
|
|
58
56
|
const shouldReduceMotion = usePrefersReducedMotion();
|
|
57
|
+
const distance = shouldReduceMotion ? 0 : MOTION_LIST_ITEM_DISTANCE;
|
|
58
|
+
const enterScale = shouldReduceMotion ? 1 : MOTION_ITEM_ENTER_SCALE;
|
|
59
59
|
const t3 = isPresent ? elementProps["aria-hidden"] : true;
|
|
60
60
|
const t4 = !isPresent || undefined;
|
|
61
61
|
let t5;
|
|
@@ -68,41 +68,44 @@ export function AnimatedListItem(t0) {
|
|
|
68
68
|
}
|
|
69
69
|
const t6 = layout && !shouldReduceMotion ? "position" : false;
|
|
70
70
|
let t7;
|
|
71
|
-
|
|
72
|
-
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
|
71
|
+
if ($[8] !== distance || $[9] !== enterScale) {
|
|
73
72
|
t7 = {
|
|
74
73
|
opacity: 0,
|
|
75
|
-
y:
|
|
76
|
-
scale:
|
|
74
|
+
y: distance,
|
|
75
|
+
scale: enterScale
|
|
77
76
|
};
|
|
77
|
+
$[8] = distance;
|
|
78
|
+
$[9] = enterScale;
|
|
79
|
+
$[10] = t7;
|
|
80
|
+
} else {
|
|
81
|
+
t7 = $[10];
|
|
82
|
+
}
|
|
83
|
+
let t8;
|
|
84
|
+
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
|
78
85
|
t8 = {
|
|
79
86
|
opacity: 1,
|
|
80
87
|
y: 0,
|
|
81
88
|
scale: 1
|
|
82
89
|
};
|
|
83
|
-
$[
|
|
84
|
-
$[9] = t8;
|
|
90
|
+
$[11] = t8;
|
|
85
91
|
} else {
|
|
86
|
-
|
|
87
|
-
t8 = $[9];
|
|
92
|
+
t8 = $[11];
|
|
88
93
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
t10 = {
|
|
94
|
+
let t9;
|
|
95
|
+
if ($[12] !== enterScale) {
|
|
96
|
+
t9 = {
|
|
93
97
|
opacity: 0,
|
|
94
|
-
scale:
|
|
95
|
-
transition:
|
|
98
|
+
scale: enterScale,
|
|
99
|
+
transition: LIST_ITEM_EXIT_TRANSITION
|
|
96
100
|
};
|
|
97
|
-
$[
|
|
98
|
-
$[
|
|
101
|
+
$[12] = enterScale;
|
|
102
|
+
$[13] = t9;
|
|
99
103
|
} else {
|
|
100
|
-
|
|
104
|
+
t9 = $[13];
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
t12 = /* @__PURE__ */ _jsx(MotionElement, {
|
|
106
|
+
let t10;
|
|
107
|
+
if ($[14] !== MotionElement || $[15] !== children || $[16] !== elementProps || $[17] !== t3 || $[18] !== t4 || $[19] !== t5 || $[20] !== t6 || $[21] !== t7 || $[22] !== t9) {
|
|
108
|
+
t10 = /* @__PURE__ */ _jsx(MotionElement, {
|
|
106
109
|
"data-slot": "animated-list-item",
|
|
107
110
|
...elementProps,
|
|
108
111
|
"aria-hidden": t3,
|
|
@@ -111,22 +114,22 @@ export function AnimatedListItem(t0) {
|
|
|
111
114
|
layout: t6,
|
|
112
115
|
initial: t7,
|
|
113
116
|
animate: t8,
|
|
114
|
-
exit:
|
|
115
|
-
transition:
|
|
117
|
+
exit: t9,
|
|
118
|
+
transition: LIST_ITEM_TRANSITION,
|
|
116
119
|
children
|
|
117
120
|
});
|
|
118
|
-
$[
|
|
119
|
-
$[
|
|
120
|
-
$[
|
|
121
|
-
$[15] = t10;
|
|
122
|
-
$[16] = t11;
|
|
121
|
+
$[14] = MotionElement;
|
|
122
|
+
$[15] = children;
|
|
123
|
+
$[16] = elementProps;
|
|
123
124
|
$[17] = t3;
|
|
124
125
|
$[18] = t4;
|
|
125
126
|
$[19] = t5;
|
|
126
127
|
$[20] = t6;
|
|
127
|
-
$[21] =
|
|
128
|
+
$[21] = t7;
|
|
129
|
+
$[22] = t9;
|
|
130
|
+
$[23] = t10;
|
|
128
131
|
} else {
|
|
129
|
-
|
|
132
|
+
t10 = $[23];
|
|
130
133
|
}
|
|
131
|
-
return
|
|
134
|
+
return t10;
|
|
132
135
|
}
|
|
@@ -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;
|