beez-ui 0.7.0 → 0.8.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 (96) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/README.md +290 -274
  3. package/dist/components/account-menu.d.ts +53 -0
  4. package/dist/components/account-menu.js +405 -0
  5. package/dist/components/animated-collapse.d.ts +19 -0
  6. package/dist/components/animated-collapse.js +81 -0
  7. package/dist/components/animated-count.d.ts +13 -0
  8. package/dist/components/animated-count.js +105 -0
  9. package/dist/components/animated-list-item.d.ts +25 -0
  10. package/dist/components/animated-list-item.js +132 -0
  11. package/dist/components/bouncing-dots-loader.d.ts +19 -0
  12. package/dist/components/bouncing-dots-loader.js +47 -0
  13. package/dist/components/confirm-delete-button.d.ts +29 -0
  14. package/dist/components/confirm-delete-button.js +254 -0
  15. package/dist/components/empty-state.d.ts +24 -0
  16. package/dist/components/empty-state.js +37 -0
  17. package/dist/components/error-state.d.ts +20 -0
  18. package/dist/components/error-state.js +145 -0
  19. package/dist/components/external-browser-handoff.d.ts +27 -0
  20. package/dist/components/external-browser-handoff.js +306 -0
  21. package/dist/components/file-upload.d.ts +78 -0
  22. package/dist/components/file-upload.js +678 -0
  23. package/dist/components/info-popover.d.ts +23 -0
  24. package/dist/components/info-popover.js +158 -0
  25. package/dist/components/month-calendar-header.d.ts +65 -0
  26. package/dist/components/month-calendar-header.js +337 -0
  27. package/dist/components/month-grid.d.ts +59 -0
  28. package/dist/components/month-grid.js +301 -0
  29. package/dist/components/notification-bell.d.ts +50 -0
  30. package/dist/components/notification-bell.js +338 -0
  31. package/dist/components/notification-panel.d.ts +56 -0
  32. package/dist/components/notification-panel.js +281 -0
  33. package/dist/components/open-in-browser-cta.d.ts +22 -0
  34. package/dist/components/open-in-browser-cta.js +61 -0
  35. package/dist/components/presence-swap.d.ts +22 -0
  36. package/dist/components/presence-swap.js +162 -0
  37. package/dist/components/progress-ring.d.ts +20 -0
  38. package/dist/components/progress-ring.js +53 -0
  39. package/dist/components/pwa-update-control.d.ts +18 -0
  40. package/dist/components/pwa-update-control.js +119 -0
  41. package/dist/components/reaction-button.d.ts +26 -0
  42. package/dist/components/reaction-button.js +114 -0
  43. package/dist/components/rich-link-editor.d.ts +25 -0
  44. package/dist/components/rich-link-editor.js +320 -0
  45. package/dist/components/rich-markdown-content.d.ts +10 -0
  46. package/dist/components/rich-markdown-content.js +56 -0
  47. package/dist/components/rich-text-content.d.ts +19 -0
  48. package/dist/components/rich-text-content.js +26 -0
  49. package/dist/emoji-picker.d.ts +27 -0
  50. package/dist/emoji-picker.js +283 -0
  51. package/dist/hooks/use-horizontal-swipe.d.ts +23 -0
  52. package/dist/hooks/use-horizontal-swipe.js +80 -0
  53. package/dist/hooks/use-is-hydrated.d.ts +6 -0
  54. package/dist/hooks/use-is-hydrated.js +20 -0
  55. package/dist/hooks/use-minute-clock.d.ts +23 -0
  56. package/dist/hooks/use-minute-clock.js +147 -0
  57. package/dist/hooks/use-month-transition-direction.d.ts +21 -0
  58. package/dist/hooks/use-month-transition-direction.js +110 -0
  59. package/dist/hooks/use-rich-link-editor.d.ts +55 -0
  60. package/dist/hooks/use-rich-link-editor.js +515 -0
  61. package/dist/hooks/use-viewer-time-zone.d.ts +6 -0
  62. package/dist/hooks/use-viewer-time-zone.js +20 -0
  63. package/dist/index.d.ts +44 -0
  64. package/dist/index.js +43 -0
  65. package/dist/lib/browser-clipboard.d.ts +13 -0
  66. package/dist/lib/browser-clipboard.js +48 -0
  67. package/dist/lib/browser-navigation.d.ts +21 -0
  68. package/dist/lib/browser-navigation.js +42 -0
  69. package/dist/lib/file-acceptance.d.ts +43 -0
  70. package/dist/lib/file-acceptance.js +107 -0
  71. package/dist/lib/format-file-size.d.ts +16 -0
  72. package/dist/lib/format-file-size.js +34 -0
  73. package/dist/lib/fuzzy-search.d.ts +17 -0
  74. package/dist/lib/fuzzy-search.js +183 -0
  75. package/dist/lib/horizontal-swipe.d.ts +23 -0
  76. package/dist/lib/horizontal-swipe.js +29 -0
  77. package/dist/lib/in-app-browser.d.ts +35 -0
  78. package/dist/lib/in-app-browser.js +80 -0
  79. package/dist/lib/month-grid.d.ts +42 -0
  80. package/dist/lib/month-grid.js +74 -0
  81. package/dist/lib/name-initials.d.ts +7 -0
  82. package/dist/lib/name-initials.js +11 -0
  83. package/dist/lib/rich-text/link-markdown-constants.d.ts +147 -0
  84. package/dist/lib/rich-text/link-markdown-constants.js +458 -0
  85. package/dist/lib/rich-text/link-markdown-types.d.ts +53 -0
  86. package/dist/lib/rich-text/link-markdown-types.js +1 -0
  87. package/dist/lib/rich-text/link-markdown.d.ts +193 -0
  88. package/dist/lib/rich-text/link-markdown.js +592 -0
  89. package/dist/lib/rich-text/rich-markdown.d.ts +41 -0
  90. package/dist/lib/rich-text/rich-markdown.js +105 -0
  91. package/dist/lib/rich-text/rich-text-editor-dom.d.ts +26 -0
  92. package/dist/lib/rich-text/rich-text-editor-dom.js +79 -0
  93. package/dist/motion/tokens.d.ts +17 -1
  94. package/dist/motion/tokens.js +17 -1
  95. package/dist/styles.css +1 -1
  96. package/package.json +211 -1
@@ -0,0 +1,301 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ import { cn } from "../lib/utils.js";
4
+ import { createMonthGridDays, groupItemsByDateKey, splitCalendarWeeks } from "../lib/month-grid.js";
5
+ import { PresenceSwap } from "./presence-swap.js";
6
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
7
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
8
+ const MONTH_GRID_DEFAULT_LABELS = {
9
+ table: "Calendario mensual",
10
+ today: "Hoy",
11
+ cancelled: "Cancelado",
12
+ dayItems: "Eventos del día",
13
+ dayButton: (dayLabel, itemCount) => `${dayLabel}: ${itemCount} ${itemCount === 1 ? "evento" : "eventos"}`,
14
+ weekDays: [
15
+ "Lun",
16
+ "Mar",
17
+ "Mié",
18
+ "Jue",
19
+ "Vie",
20
+ "Sáb",
21
+ "Dom"
22
+ ]
23
+ };
24
+ /** Dots shown per day on phones before the "+N" overflow indicator. */
25
+ const DAY_DOTS_MAX = 3;
26
+ const DEFAULT_LOCALE = "es-AR";
27
+ const NO_ITEMS = [];
28
+ /** Day keys are formatted at noon UTC so no time zone shifts them to a neighbour day. */
29
+ const DAY_KEY_NOON_SUFFIX = "T12:00:00Z";
30
+ /**
31
+ * Builds the CSS variables carrying an item's colors.
32
+ * @param item - Item with optional colors.
33
+ * @returns Inline style with the defined variables.
34
+ */
35
+ function buildItemColorStyle(item) {
36
+ return {
37
+ ...item.accentColor ? { "--beez-item-accent": item.accentColor } : {},
38
+ ...item.surfaceColor ? { "--beez-item-surface": item.surfaceColor } : {}
39
+ };
40
+ }
41
+ /**
42
+ * Formats a day key as a long, readable date.
43
+ * @param dateKey - `YYYY-MM-DD` key.
44
+ * @param locale - BCP 47 locale.
45
+ * @returns For example "miércoles, 6 de mayo".
46
+ */
47
+ function formatLongDayLabel(dateKey, locale) {
48
+ return new Intl.DateTimeFormat(locale, {
49
+ weekday: "long",
50
+ day: "numeric",
51
+ month: "long",
52
+ timeZone: "UTC"
53
+ }).format(new Date(dateKey + DAY_KEY_NOON_SUFFIX));
54
+ }
55
+ /**
56
+ * Renders the month as an accessible table with today marked (`aria-current="date"`).
57
+ * @param props - Month, items, selection callbacks and optional labels.
58
+ * @returns The month grid and its phone day summary.
59
+ */
60
+ export function MonthGrid(t0) {
61
+ const $ = _c(53);
62
+ const { month, items, onSelectItem, todayKey: t1, weekStartsOn: t2, activeDayKey: t3, onSelectDay, renderDaySummary, locale: t4, labels, className } = t0;
63
+ const todayKey = t1 === undefined ? null : t1;
64
+ const weekStartsOn = t2 === undefined ? 1 : t2;
65
+ const activeDayKey = t3 === undefined ? null : t3;
66
+ const locale = t4 === undefined ? DEFAULT_LOCALE : t4;
67
+ let T0;
68
+ let T1;
69
+ let activeDayItems;
70
+ let renderItemButton;
71
+ let resolvedLabels;
72
+ let t10;
73
+ let t5;
74
+ let t6;
75
+ let t7;
76
+ let t8;
77
+ let t9;
78
+ if ($[0] !== activeDayKey || $[1] !== className || $[2] !== items || $[3] !== labels || $[4] !== locale || $[5] !== month || $[6] !== onSelectDay || $[7] !== onSelectItem || $[8] !== todayKey || $[9] !== weekStartsOn) {
79
+ resolvedLabels = {
80
+ ...MONTH_GRID_DEFAULT_LABELS,
81
+ ...labels
82
+ };
83
+ const weekDayLabels = weekStartsOn === 0 ? [...resolvedLabels.weekDays.slice(-1), ...resolvedLabels.weekDays.slice(0, -1)] : resolvedLabels.weekDays;
84
+ let t11;
85
+ if ($[21] !== month || $[22] !== weekStartsOn) {
86
+ t11 = splitCalendarWeeks(createMonthGridDays(month, weekStartsOn));
87
+ $[21] = month;
88
+ $[22] = weekStartsOn;
89
+ $[23] = t11;
90
+ } else {
91
+ t11 = $[23];
92
+ }
93
+ const weeks = t11;
94
+ let t12;
95
+ if ($[24] !== items) {
96
+ t12 = groupItemsByDateKey(items, _temp);
97
+ $[24] = items;
98
+ $[25] = t12;
99
+ } else {
100
+ t12 = $[25];
101
+ }
102
+ const itemsByDay = t12;
103
+ let t13;
104
+ if ($[26] !== activeDayKey || $[27] !== itemsByDay) {
105
+ t13 = activeDayKey ? itemsByDay[activeDayKey] ?? [] : [];
106
+ $[26] = activeDayKey;
107
+ $[27] = itemsByDay;
108
+ $[28] = t13;
109
+ } else {
110
+ t13 = $[28];
111
+ }
112
+ activeDayItems = t13;
113
+ renderItemButton = (item_0, className_0) => /* @__PURE__ */ _jsxs("button", {
114
+ "data-slot": "month-grid-item",
115
+ "data-past": item_0.isPast || undefined,
116
+ "data-cancelled": item_0.isCancelled || undefined,
117
+ className: cn("w-full cursor-pointer rounded-[0.45rem] border border-[color-mix(in_oklab,var(--beez-item-accent,var(--primary))_35%,var(--border))] border-l-[3px] border-l-[var(--beez-item-accent,var(--primary))] bg-[var(--beez-item-surface,var(--background))] p-[0.35rem] text-left text-[0.8rem] font-bold text-foreground [overflow-wrap:anywhere] [-webkit-tap-highlight-color:transparent] transition-[border-color,background-color,scale] hover:border-[color-mix(in_oklab,var(--beez-item-accent,var(--primary))_60%,var(--border))] active:scale-[0.98] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring", "data-cancelled:border-border data-cancelled:bg-muted/60 data-cancelled:font-semibold data-cancelled:text-muted-foreground data-past:border-border data-past:bg-muted/60 data-past:font-semibold data-past:text-muted-foreground", className_0),
118
+ style: buildItemColorStyle(item_0),
119
+ type: "button",
120
+ onClick: () => onSelectItem(item_0),
121
+ children: [
122
+ item_0.leading,
123
+ item_0.timeLabel ? /* @__PURE__ */ _jsxs("span", {
124
+ className: "font-semibold text-muted-foreground tabular-nums",
125
+ children: [" ", item_0.timeLabel]
126
+ }) : null,
127
+ /* @__PURE__ */ _jsxs("span", {
128
+ className: cn(item_0.isCancelled && "line-through"),
129
+ children: [" ", item_0.title]
130
+ }),
131
+ item_0.isCancelled ? /* @__PURE__ */ _jsx("span", {
132
+ className: "block text-[0.72rem] font-bold",
133
+ children: resolvedLabels.cancelled
134
+ }) : null
135
+ ]
136
+ }, item_0.id);
137
+ t9 = "month-grid";
138
+ if ($[29] !== className) {
139
+ t10 = cn("grid min-w-0 gap-5", className);
140
+ $[29] = className;
141
+ $[30] = t10;
142
+ } else {
143
+ t10 = $[30];
144
+ }
145
+ T1 = Table;
146
+ t6 = resolvedLabels.table;
147
+ t7 = "w-full table-fixed border border-border md:min-w-[42rem]";
148
+ t8 = /* @__PURE__ */ _jsx(TableHeader, { children: /* @__PURE__ */ _jsx(TableRow, { children: weekDayLabels.map(_temp2) }) });
149
+ T0 = TableBody;
150
+ t5 = weeks.map((week) => /* @__PURE__ */ _jsx(TableRow, { children: week.map((day) => {
151
+ const isToday = day.dateKey === todayKey;
152
+ const dayItems = itemsByDay[day.dateKey] ?? NO_ITEMS;
153
+ return /* @__PURE__ */ _jsxs(TableCell, {
154
+ "aria-current": isToday ? "date" : undefined,
155
+ className: cn("h-32 w-[calc(100%/7)] border-l border-border align-top whitespace-normal max-md:h-auto max-md:min-h-14 max-md:p-[0.2rem]", isToday && "bg-[color-mix(in_oklab,var(--primary)_6%,var(--background))]"),
156
+ children: [
157
+ /* @__PURE__ */ _jsxs("span", {
158
+ className: cn("inline-flex size-7 items-center justify-center rounded-full font-bold tabular-nums max-md:size-6 max-md:text-[0.8rem]", !day.isCurrentMonth && "font-medium text-muted-foreground", isToday && "bg-primary text-primary-foreground"),
159
+ children: [day.dayNumber, isToday ? /* @__PURE__ */ _jsxs("span", {
160
+ className: "sr-only",
161
+ children: [" ", resolvedLabels.today]
162
+ }) : null]
163
+ }),
164
+ dayItems.length > 0 && onSelectDay ? /* @__PURE__ */ _jsxs("button", {
165
+ "aria-label": resolvedLabels.dayButton(formatLongDayLabel(day.dateKey, locale), dayItems.length),
166
+ "aria-pressed": activeDayKey === day.dateKey,
167
+ className: "mt-[0.2rem] hidden min-h-6 w-full cursor-pointer flex-wrap items-center justify-center gap-[0.2rem] rounded-[0.4rem] border-0 bg-transparent py-[0.2rem] text-muted-foreground [-webkit-tap-highlight-color:transparent] transition-[background-color,scale] active:scale-[0.94] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring aria-pressed:bg-[color-mix(in_oklab,var(--primary)_14%,var(--background))] max-md:flex",
168
+ type: "button",
169
+ onClick: () => onSelectDay(day.dateKey),
170
+ children: [dayItems.slice(0, DAY_DOTS_MAX).map(_temp3), dayItems.length > DAY_DOTS_MAX ? /* @__PURE__ */ _jsxs("span", {
171
+ "aria-hidden": true,
172
+ className: "text-[0.65rem] leading-none font-semibold tabular-nums",
173
+ children: ["+", dayItems.length - DAY_DOTS_MAX]
174
+ }) : null]
175
+ }) : null,
176
+ /* @__PURE__ */ _jsx("div", {
177
+ className: cn("mt-[0.35rem] grid gap-[0.35rem]", onSelectDay && "max-md:hidden"),
178
+ children: dayItems.map((item_2) => renderItemButton(item_2, ""))
179
+ })
180
+ ]
181
+ }, day.dateKey);
182
+ }) }, week[0].dateKey));
183
+ $[0] = activeDayKey;
184
+ $[1] = className;
185
+ $[2] = items;
186
+ $[3] = labels;
187
+ $[4] = locale;
188
+ $[5] = month;
189
+ $[6] = onSelectDay;
190
+ $[7] = onSelectItem;
191
+ $[8] = todayKey;
192
+ $[9] = weekStartsOn;
193
+ $[10] = T0;
194
+ $[11] = T1;
195
+ $[12] = activeDayItems;
196
+ $[13] = renderItemButton;
197
+ $[14] = resolvedLabels;
198
+ $[15] = t10;
199
+ $[16] = t5;
200
+ $[17] = t6;
201
+ $[18] = t7;
202
+ $[19] = t8;
203
+ $[20] = t9;
204
+ } else {
205
+ T0 = $[10];
206
+ T1 = $[11];
207
+ activeDayItems = $[12];
208
+ renderItemButton = $[13];
209
+ resolvedLabels = $[14];
210
+ t10 = $[15];
211
+ t5 = $[16];
212
+ t6 = $[17];
213
+ t7 = $[18];
214
+ t8 = $[19];
215
+ t9 = $[20];
216
+ }
217
+ let t11;
218
+ if ($[31] !== T0 || $[32] !== t5) {
219
+ t11 = /* @__PURE__ */ _jsx(T0, { children: t5 });
220
+ $[31] = T0;
221
+ $[32] = t5;
222
+ $[33] = t11;
223
+ } else {
224
+ t11 = $[33];
225
+ }
226
+ let t12;
227
+ if ($[34] !== T1 || $[35] !== t11 || $[36] !== t6 || $[37] !== t7 || $[38] !== t8) {
228
+ t12 = /* @__PURE__ */ _jsxs(T1, {
229
+ "aria-label": t6,
230
+ className: t7,
231
+ children: [t8, t11]
232
+ });
233
+ $[34] = T1;
234
+ $[35] = t11;
235
+ $[36] = t6;
236
+ $[37] = t7;
237
+ $[38] = t8;
238
+ $[39] = t12;
239
+ } else {
240
+ t12 = $[39];
241
+ }
242
+ let t13;
243
+ if ($[40] !== activeDayItems || $[41] !== activeDayKey || $[42] !== locale || $[43] !== onSelectDay || $[44] !== renderDaySummary || $[45] !== renderItemButton || $[46] !== resolvedLabels) {
244
+ t13 = onSelectDay ? /* @__PURE__ */ _jsx("div", {
245
+ className: "hidden min-w-0 empty:hidden max-md:grid",
246
+ children: activeDayKey && activeDayItems.length > 0 ? /* @__PURE__ */ _jsx(PresenceSwap, {
247
+ presenceKey: activeDayKey,
248
+ children: renderDaySummary ? renderDaySummary(activeDayKey, activeDayItems) : /* @__PURE__ */ _jsxs("section", {
249
+ "aria-label": resolvedLabels.dayItems,
250
+ className: "grid gap-2",
251
+ children: [/* @__PURE__ */ _jsx("h3", {
252
+ className: "m-0 text-sm font-semibold first-letter:uppercase",
253
+ children: formatLongDayLabel(activeDayKey, locale)
254
+ }), activeDayItems.map((item_3) => renderItemButton(item_3, "text-sm"))]
255
+ })
256
+ }) : null
257
+ }) : null;
258
+ $[40] = activeDayItems;
259
+ $[41] = activeDayKey;
260
+ $[42] = locale;
261
+ $[43] = onSelectDay;
262
+ $[44] = renderDaySummary;
263
+ $[45] = renderItemButton;
264
+ $[46] = resolvedLabels;
265
+ $[47] = t13;
266
+ } else {
267
+ t13 = $[47];
268
+ }
269
+ let t14;
270
+ if ($[48] !== t10 || $[49] !== t12 || $[50] !== t13 || $[51] !== t9) {
271
+ t14 = /* @__PURE__ */ _jsxs("div", {
272
+ "data-slot": t9,
273
+ className: t10,
274
+ children: [t12, t13]
275
+ });
276
+ $[48] = t10;
277
+ $[49] = t12;
278
+ $[50] = t13;
279
+ $[51] = t9;
280
+ $[52] = t14;
281
+ } else {
282
+ t14 = $[52];
283
+ }
284
+ return t14;
285
+ }
286
+ function _temp3(item_1) {
287
+ return /* @__PURE__ */ _jsx("span", {
288
+ "aria-hidden": true,
289
+ className: cn("size-[0.4rem] rounded-full bg-[var(--beez-item-accent,var(--primary))]", item_1.isPast && "bg-muted-foreground", item_1.isCancelled && "border border-muted-foreground bg-transparent"),
290
+ style: buildItemColorStyle(item_1)
291
+ }, item_1.id);
292
+ }
293
+ function _temp2(dayLabel) {
294
+ return /* @__PURE__ */ _jsx(TableHead, {
295
+ className: "border-l border-border text-center text-[0.8rem] font-semibold text-muted-foreground max-md:px-[0.15rem] max-md:text-[0.7rem]",
296
+ children: dayLabel
297
+ }, dayLabel);
298
+ }
299
+ function _temp(item) {
300
+ return item.dateKey;
301
+ }
@@ -0,0 +1,50 @@
1
+ import { type NotificationPanelProps } from "./notification-panel.js";
2
+ /**
3
+ * Surfaces the bell can render. `responsive` ships both until hydration, so a media query (the
4
+ * 768px breakpoint of `useIsMobile`) shows the right one without a layout shift; once hydrated it
5
+ * keeps only the surface matching the viewport, since both share the open state.
6
+ */
7
+ export declare const NOTIFICATION_BELL_SURFACE: {
8
+ readonly popover: "popover";
9
+ readonly responsive: "responsive";
10
+ readonly sheet: "sheet";
11
+ };
12
+ export type NotificationBellSurface = (typeof NOTIFICATION_BELL_SURFACE)[keyof typeof NOTIFICATION_BELL_SURFACE];
13
+ /** The badge shows up to this number; above it, "9+". */
14
+ export declare const NOTIFICATION_BADGE_MAX = 9;
15
+ /**
16
+ * Visible badge text.
17
+ * @param unreadCount - Unread notifications.
18
+ * @returns "3" or "9+".
19
+ */
20
+ export declare function formatUnreadBadge(unreadCount: number): string;
21
+ /**
22
+ * Default accessible name of the bell, announcing the unread count.
23
+ * @param unreadCount - Unread notifications.
24
+ * @returns "Notificaciones" or "Notificaciones, 3 sin leer".
25
+ */
26
+ export declare function describeUnreadNotifications(unreadCount: number): string;
27
+ export interface NotificationBellLabels {
28
+ title: string;
29
+ /** Visually hidden description of the mobile sheet. */
30
+ sheetDescription: string;
31
+ /** Accessible name of the bell for a given unread count. */
32
+ describeUnread: (unreadCount: number) => string;
33
+ }
34
+ export type NotificationBellProps = Omit<NotificationPanelProps, "titleSlot" | "reservesCloseButtonSpace" | "labels"> & {
35
+ /** Desktop popover, mobile bottom sheet, or both before hydration. */
36
+ surface: NotificationBellSurface;
37
+ isOpen: boolean;
38
+ onOpenChange: (isOpen: boolean) => void;
39
+ labels?: Partial<NotificationBellLabels>;
40
+ panelLabels?: NotificationPanelProps["labels"];
41
+ };
42
+ /**
43
+ * Renders the bell. Its accessible name carries the count and a polite live region announces
44
+ * changes. Both surfaces trap focus while open and return it to the bell on close. Each surface
45
+ * keeps a fixed slot in the tree, so dropping the inactive one after hydration never remounts the
46
+ * active one. The root uses `display: contents`, so the triggers stay direct header items.
47
+ * @param props - Surface, open state, unread count, panel data and optional labels.
48
+ * @returns The bell and its inbox surfaces.
49
+ */
50
+ export declare function NotificationBell({ isOpen, onOpenChange, surface, unreadCount, labels, panelLabels, className, ...panelProps }: NotificationBellProps): import("react").JSX.Element;
@@ -0,0 +1,338 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /**
4
+ * Header bell with an unread badge that opens the notification inbox: a popover on desktop and a
5
+ * bottom sheet on mobile. Presentational: all state and callbacks come from the caller.
6
+ */
7
+ import { useId, useState } from "react";
8
+ import { BellIcon } from "lucide-react";
9
+ import { AnimatePresence, motion } from "motion/react";
10
+ import { useIsHydrated } from "../hooks/use-is-hydrated.js";
11
+ import { useIsMobile } from "../hooks/use-mobile.js";
12
+ import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
13
+ import { cn } from "../lib/utils.js";
14
+ import { MOTION_EASE, MOTION_TIMING, SPRING_POP } from "../motion/tokens.js";
15
+ import { AnimatedCount } from "./animated-count.js";
16
+ import { Button } from "./button.js";
17
+ import { NotificationPanel } from "./notification-panel.js";
18
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
19
+ import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "./sheet.js";
20
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
+ import { createElement as _createElement } from "react";
22
+ /**
23
+ * Surfaces the bell can render. `responsive` ships both until hydration, so a media query (the
24
+ * 768px breakpoint of `useIsMobile`) shows the right one without a layout shift; once hydrated it
25
+ * keeps only the surface matching the viewport, since both share the open state.
26
+ */
27
+ export const NOTIFICATION_BELL_SURFACE = {
28
+ popover: "popover",
29
+ responsive: "responsive",
30
+ sheet: "sheet"
31
+ };
32
+ /** The badge shows up to this number; above it, "9+". */
33
+ export const NOTIFICATION_BADGE_MAX = 9;
34
+ /**
35
+ * Visible badge text.
36
+ * @param unreadCount - Unread notifications.
37
+ * @returns "3" or "9+".
38
+ */
39
+ export function formatUnreadBadge(unreadCount) {
40
+ return unreadCount > NOTIFICATION_BADGE_MAX ? `${NOTIFICATION_BADGE_MAX}+` : String(unreadCount);
41
+ }
42
+ /**
43
+ * Default accessible name of the bell, announcing the unread count.
44
+ * @param unreadCount - Unread notifications.
45
+ * @returns "Notificaciones" or "Notificaciones, 3 sin leer".
46
+ */
47
+ export function describeUnreadNotifications(unreadCount) {
48
+ if (unreadCount === 0) return "Notificaciones";
49
+ return `Notificaciones, ${formatUnreadBadge(unreadCount)} sin leer`;
50
+ }
51
+ const NOTIFICATION_BELL_DEFAULT_LABELS = {
52
+ title: "Notificaciones",
53
+ sheetDescription: "Avisos recientes de tu cuenta.",
54
+ describeUnread: describeUnreadNotifications
55
+ };
56
+ /** Scale the unread badge grows from and shrinks to when it appears or clears. */
57
+ const BADGE_HIDDEN_SCALE = .4;
58
+ const BADGE_MOTION = {
59
+ animate: {
60
+ opacity: 1,
61
+ scale: 1,
62
+ transition: SPRING_POP
63
+ },
64
+ exit: {
65
+ opacity: 0,
66
+ scale: BADGE_HIDDEN_SCALE,
67
+ transition: {
68
+ duration: MOTION_TIMING.exit,
69
+ ease: MOTION_EASE
70
+ }
71
+ },
72
+ initial: {
73
+ opacity: 0,
74
+ scale: BADGE_HIDDEN_SCALE
75
+ }
76
+ };
77
+ /** One short swing of the bell (out, back past rest, settling), played when the count grows. */
78
+ const BELL_NUDGE_ROTATION_DEGREES = [
79
+ 0,
80
+ -14,
81
+ 10,
82
+ -4,
83
+ 0
84
+ ];
85
+ const BELL_NUDGE_TRANSITION = {
86
+ duration: MOTION_TIMING.pop,
87
+ ease: MOTION_EASE
88
+ };
89
+ /**
90
+ * Highest value the rolling badge animates to. Every count above the visible maximum renders
91
+ * "9+", so they share one value and never roll between identical labels.
92
+ */
93
+ const BADGE_ROLL_CEILING = NOTIFICATION_BADGE_MAX + 1;
94
+ /**
95
+ * Counts how many times the unread count grew while mounted, adjusting state during render so
96
+ * the swing starts in the same commit that shows the new badge.
97
+ * @param unreadCount - Current unread notifications.
98
+ * @returns Zero until the count grows, then the number of increases seen.
99
+ */
100
+ function useUnreadIncreaseCount(unreadCount) {
101
+ const [previousUnreadCount, setPreviousUnreadCount] = useState(unreadCount);
102
+ const [increaseCount, setIncreaseCount] = useState(0);
103
+ if (previousUnreadCount !== unreadCount) {
104
+ if (unreadCount > previousUnreadCount) {
105
+ setIncreaseCount(_temp);
106
+ }
107
+ setPreviousUnreadCount(unreadCount);
108
+ }
109
+ return increaseCount;
110
+ }
111
+ function _temp(currentIncreaseCount) {
112
+ return currentIncreaseCount + 1;
113
+ }
114
+ /**
115
+ * Renders the bell. Its accessible name carries the count and a polite live region announces
116
+ * changes. Both surfaces trap focus while open and return it to the bell on close. Each surface
117
+ * keeps a fixed slot in the tree, so dropping the inactive one after hydration never remounts the
118
+ * active one. The root uses `display: contents`, so the triggers stay direct header items.
119
+ * @param props - Surface, open state, unread count, panel data and optional labels.
120
+ * @returns The bell and its inbox surfaces.
121
+ */
122
+ export function NotificationBell(t0) {
123
+ const $ = _c(33);
124
+ let className;
125
+ let isOpen;
126
+ let labels;
127
+ let onOpenChange;
128
+ let panelLabels;
129
+ let panelProps;
130
+ let surface;
131
+ let unreadCount;
132
+ if ($[0] !== t0) {
133
+ ({isOpen, onOpenChange, surface, unreadCount, labels, panelLabels, className, ...panelProps} = t0);
134
+ $[0] = t0;
135
+ $[1] = className;
136
+ $[2] = isOpen;
137
+ $[3] = labels;
138
+ $[4] = onOpenChange;
139
+ $[5] = panelLabels;
140
+ $[6] = panelProps;
141
+ $[7] = surface;
142
+ $[8] = unreadCount;
143
+ } else {
144
+ className = $[1];
145
+ isOpen = $[2];
146
+ labels = $[3];
147
+ onOpenChange = $[4];
148
+ panelLabels = $[5];
149
+ panelProps = $[6];
150
+ surface = $[7];
151
+ unreadCount = $[8];
152
+ }
153
+ const resolvedLabels = {
154
+ ...NOTIFICATION_BELL_DEFAULT_LABELS,
155
+ ...labels
156
+ };
157
+ const shouldReduceMotion = usePrefersReducedMotion();
158
+ const titleId = useId();
159
+ const isHydrated = useIsHydrated();
160
+ const isMobile = useIsMobile();
161
+ const resolvedSurface = surface === NOTIFICATION_BELL_SURFACE.responsive && isHydrated ? isMobile ? NOTIFICATION_BELL_SURFACE.sheet : NOTIFICATION_BELL_SURFACE.popover : surface;
162
+ const isResponsive = resolvedSurface === NOTIFICATION_BELL_SURFACE.responsive;
163
+ const showsSheet = isResponsive || resolvedSurface === NOTIFICATION_BELL_SURFACE.sheet;
164
+ const showsPopover = isResponsive || resolvedSurface === NOTIFICATION_BELL_SURFACE.popover;
165
+ const accessibleLabel = resolvedLabels.describeUnread(unreadCount);
166
+ const unreadIncreaseCount = useUnreadIncreaseCount(unreadCount);
167
+ let t1;
168
+ if ($[9] !== shouldReduceMotion || $[10] !== unreadIncreaseCount) {
169
+ t1 = unreadIncreaseCount > 0 && !shouldReduceMotion ? { rotate: BELL_NUDGE_ROTATION_DEGREES } : undefined;
170
+ $[9] = shouldReduceMotion;
171
+ $[10] = unreadIncreaseCount;
172
+ $[11] = t1;
173
+ } else {
174
+ t1 = $[11];
175
+ }
176
+ let t2;
177
+ if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
178
+ t2 = /* @__PURE__ */ _jsx(BellIcon, { "aria-hidden": "true" });
179
+ $[12] = t2;
180
+ } else {
181
+ t2 = $[12];
182
+ }
183
+ let t3;
184
+ if ($[13] !== t1 || $[14] !== unreadIncreaseCount) {
185
+ t3 = /* @__PURE__ */ _jsx(motion.span, {
186
+ animate: t1,
187
+ "aria-hidden": "true",
188
+ className: "inline-flex origin-[50%_10%]",
189
+ transition: BELL_NUDGE_TRANSITION,
190
+ children: t2
191
+ }, unreadIncreaseCount);
192
+ $[13] = t1;
193
+ $[14] = unreadIncreaseCount;
194
+ $[15] = t3;
195
+ } else {
196
+ t3 = $[15];
197
+ }
198
+ let t4;
199
+ if ($[16] !== unreadCount) {
200
+ t4 = unreadCount > 0 ? /* @__PURE__ */ _createElement(motion.span, {
201
+ ...BADGE_MOTION,
202
+ "aria-hidden": "true",
203
+ className: "pointer-events-none absolute top-0 right-0 inline-flex h-5 min-w-5 translate-x-[35%] -translate-y-[30%] items-center justify-center rounded-full border-2 border-background bg-primary px-1 text-xs leading-none font-semibold text-primary-foreground tabular-nums",
204
+ key: "unread-badge"
205
+ }, /* @__PURE__ */ _jsx(AnimatedCount, {
206
+ format: formatUnreadBadge,
207
+ value: Math.min(unreadCount, BADGE_ROLL_CEILING)
208
+ })) : null;
209
+ $[16] = unreadCount;
210
+ $[17] = t4;
211
+ } else {
212
+ t4 = $[17];
213
+ }
214
+ let t5;
215
+ if ($[18] !== t4) {
216
+ t5 = /* @__PURE__ */ _jsx(AnimatePresence, {
217
+ initial: false,
218
+ children: t4
219
+ });
220
+ $[18] = t4;
221
+ $[19] = t5;
222
+ } else {
223
+ t5 = $[19];
224
+ }
225
+ let t6;
226
+ if ($[20] !== accessibleLabel || $[21] !== t3 || $[22] !== t5) {
227
+ t6 = /* @__PURE__ */ _jsxs(Button, {
228
+ "data-slot": "notification-bell-trigger",
229
+ "aria-label": accessibleLabel,
230
+ className: "relative pointer-coarse:min-h-10 pointer-coarse:min-w-10",
231
+ size: "icon",
232
+ type: "button",
233
+ variant: "ghost",
234
+ children: [t3, t5]
235
+ });
236
+ $[20] = accessibleLabel;
237
+ $[21] = t3;
238
+ $[22] = t5;
239
+ $[23] = t6;
240
+ } else {
241
+ t6 = $[23];
242
+ }
243
+ const trigger = t6;
244
+ let t7;
245
+ if ($[24] !== className) {
246
+ t7 = cn("contents", className);
247
+ $[24] = className;
248
+ $[25] = t7;
249
+ } else {
250
+ t7 = $[25];
251
+ }
252
+ const t8 = unreadCount > 0 ? accessibleLabel : "";
253
+ let t9;
254
+ if ($[26] !== t8) {
255
+ t9 = /* @__PURE__ */ _jsx("span", {
256
+ "aria-live": "polite",
257
+ className: "sr-only",
258
+ role: "status",
259
+ children: t8
260
+ });
261
+ $[26] = t8;
262
+ $[27] = t9;
263
+ } else {
264
+ t9 = $[27];
265
+ }
266
+ const t10 = showsPopover ? /* @__PURE__ */ _jsx("span", {
267
+ className: cn("contents", isResponsive && "max-md:hidden"),
268
+ children: /* @__PURE__ */ _jsxs(Popover, {
269
+ onOpenChange,
270
+ open: isOpen,
271
+ children: [/* @__PURE__ */ _jsx(PopoverTrigger, {
272
+ asChild: true,
273
+ children: trigger
274
+ }), /* @__PURE__ */ _jsx(PopoverContent, {
275
+ align: "end",
276
+ "aria-labelledby": titleId,
277
+ className: "flex max-h-[min(32rem,calc(100svh-5rem))] w-[min(24rem,calc(100vw-2rem))] flex-col overflow-hidden p-0",
278
+ children: /* @__PURE__ */ _jsx(NotificationPanel, {
279
+ ...panelProps,
280
+ labels: panelLabels,
281
+ titleSlot: /* @__PURE__ */ _jsx("h2", {
282
+ className: "m-0 text-base font-semibold",
283
+ id: titleId,
284
+ children: resolvedLabels.title
285
+ }),
286
+ unreadCount
287
+ })
288
+ })]
289
+ })
290
+ }) : null;
291
+ const t11 = showsSheet ? /* @__PURE__ */ _jsx("span", {
292
+ className: cn("contents", isResponsive && "md:hidden"),
293
+ children: /* @__PURE__ */ _jsxs(Sheet, {
294
+ onOpenChange,
295
+ open: isOpen,
296
+ children: [/* @__PURE__ */ _jsx(SheetTrigger, {
297
+ asChild: true,
298
+ children: trigger
299
+ }), /* @__PURE__ */ _jsxs(SheetContent, {
300
+ className: "flex max-h-[85dvh] flex-col gap-0 overflow-hidden p-0 pb-[env(safe-area-inset-bottom)]",
301
+ side: "bottom",
302
+ children: [/* @__PURE__ */ _jsx(SheetDescription, {
303
+ className: "sr-only",
304
+ children: resolvedLabels.sheetDescription
305
+ }), /* @__PURE__ */ _jsx(NotificationPanel, {
306
+ ...panelProps,
307
+ labels: panelLabels,
308
+ reservesCloseButtonSpace: true,
309
+ titleSlot: /* @__PURE__ */ _jsx(SheetTitle, {
310
+ className: "m-0 text-base font-semibold",
311
+ children: resolvedLabels.title
312
+ }),
313
+ unreadCount
314
+ })]
315
+ })]
316
+ })
317
+ }) : null;
318
+ let t12;
319
+ if ($[28] !== t10 || $[29] !== t11 || $[30] !== t7 || $[31] !== t9) {
320
+ t12 = /* @__PURE__ */ _jsxs("span", {
321
+ "data-slot": "notification-bell",
322
+ className: t7,
323
+ children: [
324
+ t9,
325
+ t10,
326
+ t11
327
+ ]
328
+ });
329
+ $[28] = t10;
330
+ $[29] = t11;
331
+ $[30] = t7;
332
+ $[31] = t9;
333
+ $[32] = t12;
334
+ } else {
335
+ t12 = $[32];
336
+ }
337
+ return t12;
338
+ }