beez-ui 0.8.2 → 0.9.1

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 (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +12 -10
  3. package/dist/components/account-menu.d.ts +10 -0
  4. package/dist/components/account-menu.js +139 -118
  5. package/dist/components/file-upload.d.ts +14 -4
  6. package/dist/components/file-upload.js +304 -206
  7. package/dist/components/filter-presets-bar.d.ts +73 -0
  8. package/dist/components/filter-presets-bar.js +510 -0
  9. package/dist/components/reaction-button.d.ts +7 -2
  10. package/dist/components/reaction-button.js +59 -48
  11. package/dist/hooks.d.ts +10 -0
  12. package/dist/hooks.js +10 -0
  13. package/dist/index.d.ts +1 -23
  14. package/dist/index.js +1 -23
  15. package/dist/styles.css +1 -1
  16. package/package.json +11 -126
  17. package/dist/components/bouncing-dots-loader.d.ts +0 -19
  18. package/dist/components/bouncing-dots-loader.js +0 -47
  19. package/dist/components/confirm-delete-button.d.ts +0 -29
  20. package/dist/components/confirm-delete-button.js +0 -254
  21. package/dist/components/empty-state.d.ts +0 -24
  22. package/dist/components/empty-state.js +0 -37
  23. package/dist/components/error-state.d.ts +0 -20
  24. package/dist/components/error-state.js +0 -145
  25. package/dist/components/external-browser-handoff.d.ts +0 -27
  26. package/dist/components/external-browser-handoff.js +0 -306
  27. package/dist/components/info-popover.d.ts +0 -23
  28. package/dist/components/info-popover.js +0 -158
  29. package/dist/components/month-calendar-header.d.ts +0 -65
  30. package/dist/components/month-calendar-header.js +0 -337
  31. package/dist/components/month-grid.d.ts +0 -59
  32. package/dist/components/month-grid.js +0 -301
  33. package/dist/components/notification-bell.d.ts +0 -50
  34. package/dist/components/notification-bell.js +0 -338
  35. package/dist/components/notification-panel.d.ts +0 -56
  36. package/dist/components/notification-panel.js +0 -281
  37. package/dist/components/open-in-browser-cta.d.ts +0 -22
  38. package/dist/components/open-in-browser-cta.js +0 -61
  39. package/dist/components/pwa-update-control.d.ts +0 -18
  40. package/dist/components/pwa-update-control.js +0 -119
  41. package/dist/emoji-picker.d.ts +0 -27
  42. package/dist/emoji-picker.js +0 -283
  43. package/dist/hooks/use-minute-clock.d.ts +0 -23
  44. package/dist/hooks/use-minute-clock.js +0 -147
  45. package/dist/hooks/use-month-transition-direction.d.ts +0 -21
  46. package/dist/hooks/use-month-transition-direction.js +0 -110
  47. package/dist/lib/browser-navigation.d.ts +0 -21
  48. package/dist/lib/browser-navigation.js +0 -42
  49. package/dist/lib/in-app-browser.d.ts +0 -35
  50. package/dist/lib/in-app-browser.js +0 -80
  51. package/dist/lib/month-grid.d.ts +0 -42
  52. package/dist/lib/month-grid.js +0 -74
@@ -1,56 +0,0 @@
1
- /**
2
- * Content of a notification inbox: header with "mark all as read", the list, and the loading,
3
- * error and empty states. Presentational: items, statuses and callbacks come from the caller.
4
- */
5
- import type { ReactNode } from "react";
6
- export declare const NOTIFICATION_PANEL_STATUS: {
7
- readonly error: "error";
8
- readonly idle: "idle";
9
- readonly loaded: "loaded";
10
- readonly loading: "loading";
11
- };
12
- export type NotificationPanelStatus = (typeof NOTIFICATION_PANEL_STATUS)[keyof typeof NOTIFICATION_PANEL_STATUS];
13
- /** Presentation of one inbox item, already resolved by the application. */
14
- export interface NotificationPanelItem {
15
- id: string;
16
- title: string;
17
- /** Secondary line, such as when or where. */
18
- detail?: string;
19
- /** Short creation stamp, such as "12 may · 18:00". */
20
- sentAtLabel?: string;
21
- /** In-app destination; navigates through the configured router adapter. */
22
- href: string;
23
- isUnread: boolean;
24
- }
25
- export interface NotificationPanelLabels {
26
- markAllRead: string;
27
- loading: string;
28
- error: string;
29
- retry: string;
30
- empty: string;
31
- emptyHint: string;
32
- /** Visually hidden prefix of unread items. */
33
- unreadPrefix: string;
34
- }
35
- export declare const NOTIFICATION_PANEL_DEFAULT_LABELS: NotificationPanelLabels;
36
- export interface NotificationPanelProps {
37
- notifications: readonly NotificationPanelItem[];
38
- listStatus: NotificationPanelStatus;
39
- unreadCount: number;
40
- isMarkingAll: boolean;
41
- onMarkAllRead: () => void;
42
- onRetry: () => void;
43
- onSelectNotification: (notificationId: string) => void;
44
- /** Heading rendered by the surface (popover or sheet title). */
45
- titleSlot: ReactNode;
46
- /** Leaves room for a sheet close button at the end of the header. */
47
- reservesCloseButtonSpace?: boolean;
48
- labels?: Partial<NotificationPanelLabels>;
49
- className?: string;
50
- }
51
- /**
52
- * Renders the inbox. Unread items keep a dot that fades out when they are read.
53
- * @param props - Items, load status, callbacks, title slot and optional labels.
54
- * @returns The panel.
55
- */
56
- export declare function NotificationPanel({ notifications, listStatus, unreadCount, isMarkingAll, onMarkAllRead, onRetry, onSelectNotification, titleSlot, reservesCloseButtonSpace, labels, className, }: NotificationPanelProps): import("react").JSX.Element;
@@ -1,281 +0,0 @@
1
- "use client";
2
- import { c as _c } from "react/compiler-runtime";
3
- import { BellOffIcon, CheckCheckIcon } from "lucide-react";
4
- import { AnimatePresence, motion } from "motion/react";
5
- import { cn } from "../lib/utils.js";
6
- import { MOTION_EASE, MOTION_TIMING } from "../motion/tokens.js";
7
- import { AnimatedListItem } from "./animated-list-item.js";
8
- import { Button } from "./button.js";
9
- import { Link } from "./link.js";
10
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- export const NOTIFICATION_PANEL_STATUS = {
12
- error: "error",
13
- idle: "idle",
14
- loaded: "loaded",
15
- loading: "loading"
16
- };
17
- export const NOTIFICATION_PANEL_DEFAULT_LABELS = {
18
- markAllRead: "Marcar todas como leídas",
19
- loading: "Cargando notificaciones…",
20
- error: "No pudimos cargar tus notificaciones.",
21
- retry: "Reintentar",
22
- empty: "No tenés notificaciones.",
23
- emptyHint: "Te avisamos acá cuando cambie algo que te importa.",
24
- unreadPrefix: "No leída: "
25
- };
26
- /** Body regions fade in when they mount, so the new state is in the DOM at once. */
27
- const REGION_FADE_CLASS_NAME = "animate-in fade-in duration-180";
28
- const MARK_ALL_FADE = {
29
- animate: { opacity: 1 },
30
- exit: { opacity: 0 },
31
- initial: { opacity: 0 },
32
- transition: {
33
- duration: MOTION_TIMING.enter,
34
- ease: MOTION_EASE
35
- }
36
- };
37
- /**
38
- * Picks the body region: the list whenever there are items (a background refresh never hides
39
- * them), otherwise the state of the last load.
40
- * @param hasNotifications - Whether the inbox has at least one item.
41
- * @param listStatus - Status of the latest list load.
42
- * @returns The region to render.
43
- */
44
- function resolvePanelView(hasNotifications, listStatus) {
45
- if (hasNotifications) return "list";
46
- if (listStatus === NOTIFICATION_PANEL_STATUS.loading) return "loading";
47
- if (listStatus === NOTIFICATION_PANEL_STATUS.error) return "error";
48
- if (listStatus === NOTIFICATION_PANEL_STATUS.loaded) return "empty";
49
- return "idle";
50
- }
51
- /**
52
- * Renders the inbox. Unread items keep a dot that fades out when they are read.
53
- * @param props - Items, load status, callbacks, title slot and optional labels.
54
- * @returns The panel.
55
- */
56
- export function NotificationPanel(t0) {
57
- const $ = _c(42);
58
- const { notifications, listStatus, unreadCount, isMarkingAll, onMarkAllRead, onRetry, onSelectNotification, titleSlot, reservesCloseButtonSpace: t1, labels, className } = t0;
59
- const reservesCloseButtonSpace = t1 === undefined ? false : t1;
60
- let t2;
61
- if ($[0] !== labels) {
62
- t2 = {
63
- ...NOTIFICATION_PANEL_DEFAULT_LABELS,
64
- ...labels
65
- };
66
- $[0] = labels;
67
- $[1] = t2;
68
- } else {
69
- t2 = $[1];
70
- }
71
- const resolvedLabels = t2;
72
- const t3 = notifications.length > 0;
73
- let t4;
74
- if ($[2] !== listStatus || $[3] !== t3) {
75
- t4 = resolvePanelView(t3, listStatus);
76
- $[2] = listStatus;
77
- $[3] = t3;
78
- $[4] = t4;
79
- } else {
80
- t4 = $[4];
81
- }
82
- const panelView = t4;
83
- let t5;
84
- if ($[5] !== className) {
85
- t5 = cn("flex min-h-0 min-w-0 flex-col", className);
86
- $[5] = className;
87
- $[6] = t5;
88
- } else {
89
- t5 = $[6];
90
- }
91
- const t6 = reservesCloseButtonSpace && "pr-12";
92
- let t7;
93
- if ($[7] !== t6) {
94
- t7 = cn("flex flex-wrap items-center justify-between gap-2 border-b border-border px-4 py-3", t6);
95
- $[7] = t6;
96
- $[8] = t7;
97
- } else {
98
- t7 = $[8];
99
- }
100
- let t8;
101
- if ($[9] !== isMarkingAll || $[10] !== onMarkAllRead || $[11] !== resolvedLabels || $[12] !== unreadCount) {
102
- t8 = unreadCount > 0 ? /* @__PURE__ */ _jsx(motion.span, {
103
- className: "-mr-2 inline-flex",
104
- ...MARK_ALL_FADE,
105
- children: /* @__PURE__ */ _jsxs(Button, {
106
- className: "text-[0.8rem] text-muted-foreground",
107
- disabled: isMarkingAll,
108
- onClick: onMarkAllRead,
109
- size: "sm",
110
- type: "button",
111
- variant: "ghost",
112
- children: [/* @__PURE__ */ _jsx(CheckCheckIcon, { "aria-hidden": "true" }), resolvedLabels.markAllRead]
113
- })
114
- }, "mark-all") : null;
115
- $[9] = isMarkingAll;
116
- $[10] = onMarkAllRead;
117
- $[11] = resolvedLabels;
118
- $[12] = unreadCount;
119
- $[13] = t8;
120
- } else {
121
- t8 = $[13];
122
- }
123
- let t9;
124
- if ($[14] !== t8) {
125
- t9 = /* @__PURE__ */ _jsx(AnimatePresence, {
126
- initial: false,
127
- children: t8
128
- });
129
- $[14] = t8;
130
- $[15] = t9;
131
- } else {
132
- t9 = $[15];
133
- }
134
- let t10;
135
- if ($[16] !== t7 || $[17] !== t9 || $[18] !== titleSlot) {
136
- t10 = /* @__PURE__ */ _jsxs("div", {
137
- className: t7,
138
- children: [titleSlot, t9]
139
- });
140
- $[16] = t7;
141
- $[17] = t9;
142
- $[18] = titleSlot;
143
- $[19] = t10;
144
- } else {
145
- t10 = $[19];
146
- }
147
- let t11;
148
- if ($[20] !== panelView || $[21] !== resolvedLabels) {
149
- t11 = panelView === "loading" ? /* @__PURE__ */ _jsx("p", {
150
- className: cn("m-0 grid justify-items-start gap-3 p-4 text-[0.9rem] text-muted-foreground", REGION_FADE_CLASS_NAME),
151
- role: "status",
152
- children: resolvedLabels.loading
153
- }) : null;
154
- $[20] = panelView;
155
- $[21] = resolvedLabels;
156
- $[22] = t11;
157
- } else {
158
- t11 = $[22];
159
- }
160
- let t12;
161
- if ($[23] !== onRetry || $[24] !== panelView || $[25] !== resolvedLabels) {
162
- t12 = panelView === "error" ? /* @__PURE__ */ _jsxs("div", {
163
- className: cn("grid justify-items-start gap-3 p-4 text-[0.9rem] text-muted-foreground", REGION_FADE_CLASS_NAME),
164
- role: "alert",
165
- children: [/* @__PURE__ */ _jsx("p", {
166
- className: "m-0",
167
- children: resolvedLabels.error
168
- }), /* @__PURE__ */ _jsx(Button, {
169
- onClick: onRetry,
170
- size: "sm",
171
- type: "button",
172
- variant: "outline",
173
- children: resolvedLabels.retry
174
- })]
175
- }) : null;
176
- $[23] = onRetry;
177
- $[24] = panelView;
178
- $[25] = resolvedLabels;
179
- $[26] = t12;
180
- } else {
181
- t12 = $[26];
182
- }
183
- let t13;
184
- if ($[27] !== panelView || $[28] !== resolvedLabels) {
185
- t13 = panelView === "empty" ? /* @__PURE__ */ _jsxs("div", {
186
- className: cn("grid justify-items-center gap-[0.35rem] px-5 py-8 text-center text-[0.9rem] text-muted-foreground", REGION_FADE_CLASS_NAME),
187
- children: [
188
- /* @__PURE__ */ _jsx(BellOffIcon, {
189
- "aria-hidden": "true",
190
- className: "mb-1 size-6 opacity-80"
191
- }),
192
- /* @__PURE__ */ _jsx("p", {
193
- className: "m-0",
194
- children: resolvedLabels.empty
195
- }),
196
- /* @__PURE__ */ _jsx("p", {
197
- className: "m-0 max-w-72 text-[0.8rem]",
198
- children: resolvedLabels.emptyHint
199
- })
200
- ]
201
- }) : null;
202
- $[27] = panelView;
203
- $[28] = resolvedLabels;
204
- $[29] = t13;
205
- } else {
206
- t13 = $[29];
207
- }
208
- let t14;
209
- if ($[30] !== notifications || $[31] !== onSelectNotification || $[32] !== panelView || $[33] !== resolvedLabels) {
210
- t14 = panelView === "list" ? /* @__PURE__ */ _jsx("ul", {
211
- className: cn("m-0 min-h-0 list-none overflow-y-auto overscroll-contain py-1", REGION_FADE_CLASS_NAME),
212
- children: /* @__PURE__ */ _jsx(AnimatePresence, {
213
- initial: false,
214
- children: notifications.map((notification) => /* @__PURE__ */ _jsx(AnimatedListItem, {
215
- className: "[&+&]:border-t [&+&]:border-border/60",
216
- children: /* @__PURE__ */ _jsxs(Link, {
217
- className: cn("grid grid-cols-[0.5rem_minmax(0,1fr)] items-start gap-3 px-4 py-3 text-foreground no-underline outline-none [-webkit-tap-highlight-color:transparent] transition-[background-color,box-shadow] focus-visible:bg-accent focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] active:bg-accent [@media(hover:hover)]:hover:bg-accent", notification.isUnread && "bg-primary/6"),
218
- href: notification.href,
219
- onClick: () => onSelectNotification(notification.id),
220
- children: [/* @__PURE__ */ _jsx("span", {
221
- "aria-hidden": "true",
222
- "data-unread": notification.isUnread || undefined,
223
- className: "mt-[0.4rem] size-2 scale-50 rounded-full bg-primary opacity-0 transition-[opacity,scale] data-unread:scale-100 data-unread:opacity-100"
224
- }), /* @__PURE__ */ _jsxs("span", {
225
- className: "grid min-w-0 gap-[0.15rem]",
226
- children: [
227
- notification.isUnread ? /* @__PURE__ */ _jsx("span", {
228
- className: "sr-only",
229
- children: resolvedLabels.unreadPrefix
230
- }) : null,
231
- /* @__PURE__ */ _jsx("span", {
232
- className: "text-[0.9rem] leading-[1.35] font-medium [overflow-wrap:anywhere]",
233
- children: notification.title
234
- }),
235
- notification.detail ? /* @__PURE__ */ _jsx("span", {
236
- className: "text-[0.8rem] leading-[1.4] text-muted-foreground [overflow-wrap:anywhere]",
237
- children: notification.detail
238
- }) : null,
239
- notification.sentAtLabel ? /* @__PURE__ */ _jsx("span", {
240
- className: "text-xs text-muted-foreground",
241
- children: notification.sentAtLabel
242
- }) : null
243
- ]
244
- })]
245
- })
246
- }, notification.id))
247
- })
248
- }) : null;
249
- $[30] = notifications;
250
- $[31] = onSelectNotification;
251
- $[32] = panelView;
252
- $[33] = resolvedLabels;
253
- $[34] = t14;
254
- } else {
255
- t14 = $[34];
256
- }
257
- let t15;
258
- if ($[35] !== t10 || $[36] !== t11 || $[37] !== t12 || $[38] !== t13 || $[39] !== t14 || $[40] !== t5) {
259
- t15 = /* @__PURE__ */ _jsxs("div", {
260
- "data-slot": "notification-panel",
261
- className: t5,
262
- children: [
263
- t10,
264
- t11,
265
- t12,
266
- t13,
267
- t14
268
- ]
269
- });
270
- $[35] = t10;
271
- $[36] = t11;
272
- $[37] = t12;
273
- $[38] = t13;
274
- $[39] = t14;
275
- $[40] = t5;
276
- $[41] = t15;
277
- } else {
278
- t15 = $[41];
279
- }
280
- return t15;
281
- }
@@ -1,22 +0,0 @@
1
- export interface OpenInBrowserCtaCopy {
2
- title: string;
3
- description: string;
4
- iosButtonLabel: string;
5
- iosHint: string;
6
- manualHint: string;
7
- }
8
- export interface OpenInBrowserCtaProps {
9
- /** Absolute https URL to continue with, such as the sign-in page. */
10
- url: string;
11
- /** iOS gets a Safari deep link; other platforms get manual steps. */
12
- isIos: boolean;
13
- copy?: Partial<OpenInBrowserCtaCopy>;
14
- className?: string;
15
- }
16
- /**
17
- * Renders the handoff section. The URL is selectable with one tap, since copying is the only
18
- * universal escape from embedded browsers.
19
- * @param props - Destination URL, platform and optional copy.
20
- * @returns The call to action.
21
- */
22
- export declare function OpenInBrowserCta({ url, isIos, copy, className }: OpenInBrowserCtaProps): import("react").JSX.Element;
@@ -1,61 +0,0 @@
1
- /**
2
- * Asks people inside an in-app browser to continue in their regular browser: a Safari deep link
3
- * on iOS, manual steps elsewhere, and the destination URL ready to copy. Server-safe.
4
- */
5
- import { cn } from "../lib/utils.js";
6
- import { EXTERNAL_BROWSER_PLATFORM, buildExternalBrowserUrl } from "../lib/in-app-browser.js";
7
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
8
- const ENTER_CLASS_NAME = "animate-in fade-in slide-in-from-bottom-1.5 fill-mode-both duration-300";
9
- const OPEN_IN_BROWSER_DEFAULT_COPY = {
10
- title: "Abrí este sitio en tu navegador",
11
- description: "Estás dentro del navegador interno de la app que te trajo hasta acá. Para continuar, abrí este sitio en tu navegador habitual.",
12
- iosButtonLabel: "Abrir en Safari",
13
- iosHint: "Tocá el botón para abrir el sitio en Safari y continuá desde ahí.",
14
- manualHint: "Tocá los tres puntos arriba a la derecha y elegí “Abrir en el navegador”, o copiá este link y pegalo en tu navegador."
15
- };
16
- /**
17
- * Renders the handoff section. The URL is selectable with one tap, since copying is the only
18
- * universal escape from embedded browsers.
19
- * @param props - Destination URL, platform and optional copy.
20
- * @returns The call to action.
21
- */
22
- export function OpenInBrowserCta({ url, isIos, copy, className }) {
23
- const resolvedCopy = {
24
- ...OPEN_IN_BROWSER_DEFAULT_COPY,
25
- ...copy
26
- };
27
- const safariUrl = buildExternalBrowserUrl({
28
- platform: EXTERNAL_BROWSER_PLATFORM.ios,
29
- targetHttpsUrl: url
30
- }) ?? url;
31
- return /* @__PURE__ */ _jsxs("section", {
32
- "data-slot": "open-in-browser-cta",
33
- className: cn("mt-6 flex flex-col gap-4", className),
34
- children: [
35
- /* @__PURE__ */ _jsx("h2", {
36
- className: cn("m-0 text-lg font-semibold text-foreground", ENTER_CLASS_NAME),
37
- children: resolvedCopy.title
38
- }),
39
- /* @__PURE__ */ _jsx("p", {
40
- className: cn("m-0 text-sm leading-6 text-muted-foreground", ENTER_CLASS_NAME, "delay-35"),
41
- children: resolvedCopy.description
42
- }),
43
- isIos ? /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx("a", {
44
- className: cn("inline-flex h-10 items-center justify-center self-stretch rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground no-underline [-webkit-tap-highlight-color:transparent] transition-[background-color,box-shadow,scale] outline-none active:scale-[0.98] focus-visible:shadow-[0_0_0_2px_var(--background),0_0_0_4px_var(--ring)] [@media(hover:hover)]:hover:bg-primary/80", ENTER_CLASS_NAME, "delay-70"),
45
- href: safariUrl,
46
- rel: "noopener noreferrer",
47
- children: resolvedCopy.iosButtonLabel
48
- }), /* @__PURE__ */ _jsx("p", {
49
- className: cn("m-0 text-[0.8125rem] leading-5 text-muted-foreground", ENTER_CLASS_NAME, "delay-105"),
50
- children: resolvedCopy.iosHint
51
- })] }) : /* @__PURE__ */ _jsx("p", {
52
- className: cn("m-0 text-[0.8125rem] leading-5 text-muted-foreground", ENTER_CLASS_NAME, "delay-70"),
53
- children: resolvedCopy.manualHint
54
- }),
55
- /* @__PURE__ */ _jsx("p", {
56
- className: cn("m-0 rounded-lg border border-border bg-muted/50 p-3 font-mono text-xs leading-[1.125rem] break-all text-muted-foreground select-all", ENTER_CLASS_NAME, "delay-140"),
57
- children: url
58
- })
59
- ]
60
- });
61
- }
@@ -1,18 +0,0 @@
1
- export interface PwaUpdateControlLabels {
2
- /** Short notice next to the button; hidden on narrow screens. */
3
- badge?: string;
4
- action?: string;
5
- }
6
- export interface PwaUpdateControlProps {
7
- labels?: PwaUpdateControlLabels;
8
- /** Message posted to the waiting worker so it activates. */
9
- skipWaitingMessage?: unknown;
10
- className?: string;
11
- }
12
- /**
13
- * Renders the update notice and action once an update is waiting; nothing otherwise, including
14
- * during server rendering and hydration.
15
- * @param props - Optional labels, activation message and class name.
16
- * @returns The update control, or nothing.
17
- */
18
- export declare function PwaUpdateControl({ labels, skipWaitingMessage, className }: PwaUpdateControlProps): import("react").JSX.Element | null;
@@ -1,119 +0,0 @@
1
- "use client";
2
- /**
3
- * Offers to apply a waiting service worker update. It stays hidden until the browser reports a
4
- * waiting worker, asks it to activate on demand and reloads once the new worker takes control.
5
- */
6
- import { useEffect, useRef, useState } from "react";
7
- import { RefreshCw } from "lucide-react";
8
- import { useIsHydrated } from "../hooks/use-is-hydrated.js";
9
- import { cn } from "../lib/utils.js";
10
- import { Button } from "./button.js";
11
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
- /** Message understood by Workbox and most hand-written workers to call `skipWaiting()`. */
13
- const DEFAULT_SKIP_WAITING_MESSAGE = { type: "SKIP_WAITING" };
14
- const INSTALLED_WORKER_STATE = "installed";
15
- const PWA_UPDATE_DEFAULT_LABELS = {
16
- badge: "Hay una nueva versión",
17
- action: "Actualizar app"
18
- };
19
- /**
20
- * Tells whether the current page can talk to service workers.
21
- * @returns Whether the Service Worker API is available.
22
- */
23
- function canUseServiceWorker() {
24
- return typeof window !== "undefined" && "serviceWorker" in navigator;
25
- }
26
- /**
27
- * Reports a waiting worker now, or as soon as a newly found one finishes installing.
28
- * @param registration - Registration of the current page.
29
- * @param onWaiting - Called when an update is ready to activate.
30
- * @returns A cleanup that stops watching.
31
- */
32
- function watchForWaitingWorker(registration, onWaiting) {
33
- if (registration.waiting) onWaiting();
34
- const handleUpdateFound = () => {
35
- const installingWorker = registration.installing;
36
- if (!installingWorker) return;
37
- installingWorker.addEventListener("statechange", () => {
38
- // Without a controller this is the first install, not an update.
39
- if (installingWorker.state === INSTALLED_WORKER_STATE && navigator.serviceWorker.controller) onWaiting();
40
- });
41
- };
42
- registration.addEventListener("updatefound", handleUpdateFound);
43
- return () => registration.removeEventListener("updatefound", handleUpdateFound);
44
- }
45
- /**
46
- * Renders the update notice and action once an update is waiting; nothing otherwise, including
47
- * during server rendering and hydration.
48
- * @param props - Optional labels, activation message and class name.
49
- * @returns The update control, or nothing.
50
- */
51
- export function PwaUpdateControl({ labels, skipWaitingMessage = DEFAULT_SKIP_WAITING_MESSAGE, className }) {
52
- const resolvedLabels = {
53
- ...PWA_UPDATE_DEFAULT_LABELS,
54
- ...labels
55
- };
56
- const isHydrated = useIsHydrated();
57
- const [hasUpdateReady, setHasUpdateReady] = useState(false);
58
- const [isApplyingUpdate, setIsApplyingUpdate] = useState(false);
59
- const hasReloadedForControllerChange = useRef(false);
60
- useEffect(() => {
61
- if (!isHydrated || !canUseServiceWorker()) return;
62
- let stopWatchingRegistration = () => {};
63
- let isDisposed = false;
64
- const markUpdateReady = () => {
65
- if (!isDisposed) setHasUpdateReady(true);
66
- };
67
- const handleControllerChange = () => {
68
- if (hasReloadedForControllerChange.current) return;
69
- hasReloadedForControllerChange.current = true;
70
- window.location.reload();
71
- };
72
- navigator.serviceWorker.addEventListener("controllerchange", handleControllerChange);
73
- const attachUpdateWatcher = async () => {
74
- const registration = await navigator.serviceWorker.getRegistration();
75
- if (!registration || isDisposed) return;
76
- stopWatchingRegistration = watchForWaitingWorker(registration, markUpdateReady);
77
- // A failed check (offline, server error) keeps the current version without blocking the page.
78
- await registration.update().catch(() => undefined);
79
- if (registration.waiting) markUpdateReady();
80
- };
81
- void attachUpdateWatcher();
82
- return () => {
83
- isDisposed = true;
84
- stopWatchingRegistration();
85
- navigator.serviceWorker.removeEventListener("controllerchange", handleControllerChange);
86
- };
87
- }, [isHydrated]);
88
- const applyUpdate = async () => {
89
- setIsApplyingUpdate(true);
90
- try {
91
- const registration = await navigator.serviceWorker.getRegistration();
92
- if (!registration) return;
93
- if (!registration.waiting) await registration.update();
94
- registration.waiting?.postMessage(skipWaitingMessage);
95
- } catch (updateError) {
96
- console.error("PwaUpdateControl:applyUpdate failed to activate the waiting service worker", updateError);
97
- } finally {
98
- setIsApplyingUpdate(false);
99
- }
100
- };
101
- if (!isHydrated || !canUseServiceWorker() || !hasUpdateReady) return null;
102
- return /* @__PURE__ */ _jsxs("div", {
103
- "data-slot": "pwa-update-control",
104
- className: cn("inline-flex items-center gap-[0.45rem]", className),
105
- children: [/* @__PURE__ */ _jsx("span", {
106
- className: "hidden items-center rounded-full border border-[color-mix(in_srgb,var(--border)_84%,var(--primary)_16%)] bg-[color-mix(in_srgb,var(--muted)_90%,var(--primary)_10%)] px-2 py-[0.1rem] text-[0.72rem] leading-tight text-[color-mix(in_srgb,var(--foreground)_78%,var(--primary)_22%)] sm:inline-flex",
107
- children: resolvedLabels.badge
108
- }), /* @__PURE__ */ _jsxs(Button, {
109
- disabled: isApplyingUpdate,
110
- size: "sm",
111
- type: "button",
112
- onClick: () => void applyUpdate(),
113
- children: [/* @__PURE__ */ _jsx(RefreshCw, {
114
- "aria-hidden": "true",
115
- className: cn(isApplyingUpdate && "animate-spin")
116
- }), resolvedLabels.action]
117
- })]
118
- });
119
- }
@@ -1,27 +0,0 @@
1
- export interface EmojiPickerLabels {
2
- /** Accessible name of the trigger. */
3
- chooseEmoji: string;
4
- /** Accessible name of the popover. */
5
- picker: string;
6
- searchPlaceholder: string;
7
- loading: string;
8
- }
9
- export interface EmojiPickerProps {
10
- /** Currently selected emoji; empty shows a placeholder icon. */
11
- value: string;
12
- onChange: (emoji: string) => void;
13
- /** Visible label of the field. */
14
- label: string;
15
- /** Keeps the label for assistive technology but hides it visually. */
16
- isLabelVisuallyHidden?: boolean;
17
- disabled?: boolean;
18
- labels?: Partial<EmojiPickerLabels>;
19
- className?: string;
20
- }
21
- /**
22
- * Renders an outline trigger showing the current emoji that opens a searchable emoji popover. The
23
- * chosen emoji swaps in with a short cross-fade; the picker follows the resolved color theme.
24
- * @param props - Current value, change handler, label and optional copy.
25
- * @returns The labelled emoji picker.
26
- */
27
- export declare function EmojiPicker({ value, onChange, label, isLabelVisuallyHidden, disabled, labels, className, }: EmojiPickerProps): import("react").JSX.Element;