beez-ui 0.6.2 → 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 (127) hide show
  1. package/CHANGELOG.md +150 -96
  2. package/README.md +33 -6
  3. package/dist/components/account-menu.d.ts +53 -0
  4. package/dist/components/account-menu.js +405 -0
  5. package/dist/components/alert.js +1 -1
  6. package/dist/components/animated-collapse.d.ts +19 -0
  7. package/dist/components/animated-collapse.js +81 -0
  8. package/dist/components/animated-count.d.ts +13 -0
  9. package/dist/components/animated-count.js +105 -0
  10. package/dist/components/animated-list-item.d.ts +25 -0
  11. package/dist/components/animated-list-item.js +132 -0
  12. package/dist/components/avatar.d.ts +2 -1
  13. package/dist/components/avatar.js +4 -3
  14. package/dist/components/bouncing-dots-loader.d.ts +19 -0
  15. package/dist/components/bouncing-dots-loader.js +47 -0
  16. package/dist/components/calendar.js +102 -0
  17. package/dist/components/carousel.js +61 -48
  18. package/dist/components/checkbox.js +1 -1
  19. package/dist/components/confirm-delete-button.d.ts +29 -0
  20. package/dist/components/confirm-delete-button.js +254 -0
  21. package/dist/components/data-table.js +34 -4
  22. package/dist/components/dropdown-menu.js +5 -2
  23. package/dist/components/empty-state.d.ts +24 -0
  24. package/dist/components/empty-state.js +37 -0
  25. package/dist/components/error-state.d.ts +20 -0
  26. package/dist/components/error-state.js +145 -0
  27. package/dist/components/external-browser-handoff.d.ts +27 -0
  28. package/dist/components/external-browser-handoff.js +306 -0
  29. package/dist/components/file-upload.d.ts +78 -0
  30. package/dist/components/file-upload.js +678 -0
  31. package/dist/components/filter-query-bar.js +11 -6
  32. package/dist/components/form.d.ts +5 -0
  33. package/dist/components/form.js +11 -9
  34. package/dist/components/info-popover.d.ts +23 -0
  35. package/dist/components/info-popover.js +158 -0
  36. package/dist/components/input-group.js +1 -1
  37. package/dist/components/input.js +1 -1
  38. package/dist/components/month-calendar-header.d.ts +65 -0
  39. package/dist/components/month-calendar-header.js +337 -0
  40. package/dist/components/month-grid.d.ts +59 -0
  41. package/dist/components/month-grid.js +301 -0
  42. package/dist/components/notification-bell.d.ts +50 -0
  43. package/dist/components/notification-bell.js +338 -0
  44. package/dist/components/notification-panel.d.ts +56 -0
  45. package/dist/components/notification-panel.js +281 -0
  46. package/dist/components/open-in-browser-cta.d.ts +22 -0
  47. package/dist/components/open-in-browser-cta.js +61 -0
  48. package/dist/components/pagination.d.ts +1 -0
  49. package/dist/components/pagination.js +9 -4
  50. package/dist/components/presence-swap.d.ts +22 -0
  51. package/dist/components/presence-swap.js +162 -0
  52. package/dist/components/progress-ring.d.ts +20 -0
  53. package/dist/components/progress-ring.js +53 -0
  54. package/dist/components/pwa-update-control.d.ts +18 -0
  55. package/dist/components/pwa-update-control.js +119 -0
  56. package/dist/components/radio-group.js +1 -1
  57. package/dist/components/reaction-button.d.ts +26 -0
  58. package/dist/components/reaction-button.js +114 -0
  59. package/dist/components/rich-link-editor.d.ts +25 -0
  60. package/dist/components/rich-link-editor.js +320 -0
  61. package/dist/components/rich-markdown-content.d.ts +10 -0
  62. package/dist/components/rich-markdown-content.js +56 -0
  63. package/dist/components/rich-text-content.d.ts +19 -0
  64. package/dist/components/rich-text-content.js +26 -0
  65. package/dist/components/select.js +1 -1
  66. package/dist/components/sheet.js +12 -9
  67. package/dist/components/sidebar.js +9 -5
  68. package/dist/components/skeleton.d.ts +6 -0
  69. package/dist/components/skeleton.js +7 -1
  70. package/dist/components/switch.js +1 -1
  71. package/dist/components/textarea.js +1 -1
  72. package/dist/components/typing-animation.js +42 -15
  73. package/dist/emoji-picker.d.ts +27 -0
  74. package/dist/emoji-picker.js +283 -0
  75. package/dist/hooks/use-horizontal-swipe.d.ts +23 -0
  76. package/dist/hooks/use-horizontal-swipe.js +80 -0
  77. package/dist/hooks/use-is-hydrated.d.ts +6 -0
  78. package/dist/hooks/use-is-hydrated.js +20 -0
  79. package/dist/hooks/use-minute-clock.d.ts +23 -0
  80. package/dist/hooks/use-minute-clock.js +147 -0
  81. package/dist/hooks/use-month-transition-direction.d.ts +21 -0
  82. package/dist/hooks/use-month-transition-direction.js +110 -0
  83. package/dist/hooks/use-rich-link-editor.d.ts +55 -0
  84. package/dist/hooks/use-rich-link-editor.js +515 -0
  85. package/dist/hooks/use-viewer-time-zone.d.ts +6 -0
  86. package/dist/hooks/use-viewer-time-zone.js +20 -0
  87. package/dist/index.d.ts +44 -0
  88. package/dist/index.js +43 -0
  89. package/dist/lib/browser-clipboard.d.ts +13 -0
  90. package/dist/lib/browser-clipboard.js +48 -0
  91. package/dist/lib/browser-navigation.d.ts +21 -0
  92. package/dist/lib/browser-navigation.js +42 -0
  93. package/dist/lib/column-filter-value.d.ts +12 -0
  94. package/dist/lib/column-filter-value.js +52 -0
  95. package/dist/lib/file-acceptance.d.ts +43 -0
  96. package/dist/lib/file-acceptance.js +107 -0
  97. package/dist/lib/format-file-size.d.ts +16 -0
  98. package/dist/lib/format-file-size.js +34 -0
  99. package/dist/lib/fuzzy-search.d.ts +17 -0
  100. package/dist/lib/fuzzy-search.js +183 -0
  101. package/dist/lib/horizontal-swipe.d.ts +23 -0
  102. package/dist/lib/horizontal-swipe.js +29 -0
  103. package/dist/lib/in-app-browser.d.ts +35 -0
  104. package/dist/lib/in-app-browser.js +80 -0
  105. package/dist/lib/month-grid.d.ts +42 -0
  106. package/dist/lib/month-grid.js +74 -0
  107. package/dist/lib/name-initials.d.ts +7 -0
  108. package/dist/lib/name-initials.js +11 -0
  109. package/dist/lib/rich-text/link-markdown-constants.d.ts +147 -0
  110. package/dist/lib/rich-text/link-markdown-constants.js +458 -0
  111. package/dist/lib/rich-text/link-markdown-types.d.ts +53 -0
  112. package/dist/lib/rich-text/link-markdown-types.js +1 -0
  113. package/dist/lib/rich-text/link-markdown.d.ts +193 -0
  114. package/dist/lib/rich-text/link-markdown.js +592 -0
  115. package/dist/lib/rich-text/rich-markdown.d.ts +41 -0
  116. package/dist/lib/rich-text/rich-markdown.js +105 -0
  117. package/dist/lib/rich-text/rich-text-editor-dom.d.ts +26 -0
  118. package/dist/lib/rich-text/rich-text-editor-dom.js +79 -0
  119. package/dist/motion/glide-indicator.d.ts +37 -3
  120. package/dist/motion/glide-indicator.js +37 -3
  121. package/dist/motion/motion-slot.js +47 -18
  122. package/dist/motion/surface-keyframes.js +18 -6
  123. package/dist/motion/tokens.d.ts +41 -2
  124. package/dist/motion/tokens.js +40 -1
  125. package/dist/providers/beez-ui-provider.js +49 -17
  126. package/dist/styles.css +1 -1
  127. package/package.json +212 -2
@@ -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
+ }
@@ -0,0 +1,56 @@
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;
@@ -0,0 +1,281 @@
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
+ }
@@ -0,0 +1,22 @@
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;