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,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;
@@ -0,0 +1,61 @@
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
+ }
@@ -0,0 +1,22 @@
1
+ /** Cross-fades between states of the same region, such as idle, loading and result. */
2
+ import type { ReactNode } from "react";
3
+ declare const PRESENCE_SWAP_ELEMENTS: {
4
+ readonly div: import("motion/react").ForwardRefComponent<HTMLDivElement, import("motion/react").HTMLMotionProps<"div">>;
5
+ readonly span: import("motion/react").ForwardRefComponent<HTMLSpanElement, import("motion/react").HTMLMotionProps<"span">>;
6
+ };
7
+ export interface PresenceSwapProps {
8
+ /** Changing the key plays the exit of the previous content and the entry of the new one. */
9
+ presenceKey: string;
10
+ children: ReactNode;
11
+ as?: keyof typeof PRESENCE_SWAP_ELEMENTS;
12
+ className?: string;
13
+ /** `wait` swaps sequentially; `popLayout` overlaps them for inline content. */
14
+ mode?: "wait" | "popLayout";
15
+ }
16
+ /**
17
+ * Swaps keyed content with a short lift-and-fade; the first render is not animated.
18
+ * @param props - Key identifying the current state and its content.
19
+ * @returns The animated content.
20
+ */
21
+ export declare function PresenceSwap({ presenceKey, children, as, className, mode }: PresenceSwapProps): import("react").JSX.Element;
22
+ export {};
@@ -0,0 +1,162 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ import { AnimatePresence, motion, useIsPresent } from "motion/react";
4
+ import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
5
+ import { cn } from "../lib/utils.js";
6
+ import { MOTION_CONTENT_DISTANCE, MOTION_EASE, MOTION_TIMING } from "../motion/tokens.js";
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const PRESENCE_SWAP_ELEMENTS = {
9
+ div: motion.div,
10
+ span: motion.span
11
+ };
12
+ const INSTANT_TRANSITION = { duration: 0 };
13
+ /**
14
+ * One keyed state of the swap. While it animates out it turns inert and hidden
15
+ * from assistive technology, so stale controls cannot be clicked or announced.
16
+ * @param props - Element type, class name and content.
17
+ * @returns The animated state.
18
+ */
19
+ function PresenceSwapItem(t0) {
20
+ const $ = _c(20);
21
+ const { as, className, children } = t0;
22
+ const MotionElement = PRESENCE_SWAP_ELEMENTS[as];
23
+ const isPresent = useIsPresent();
24
+ const shouldReduceMotion = usePrefersReducedMotion();
25
+ const distance = shouldReduceMotion ? 0 : MOTION_CONTENT_DISTANCE;
26
+ const t1 = isPresent ? undefined : true;
27
+ const t2 = !isPresent || undefined;
28
+ let t3;
29
+ if ($[0] !== distance) {
30
+ t3 = {
31
+ opacity: 0,
32
+ y: distance
33
+ };
34
+ $[0] = distance;
35
+ $[1] = t3;
36
+ } else {
37
+ t3 = $[1];
38
+ }
39
+ let t4;
40
+ if ($[2] !== shouldReduceMotion) {
41
+ t4 = shouldReduceMotion ? INSTANT_TRANSITION : {
42
+ duration: MOTION_TIMING.enter,
43
+ ease: MOTION_EASE
44
+ };
45
+ $[2] = shouldReduceMotion;
46
+ $[3] = t4;
47
+ } else {
48
+ t4 = $[3];
49
+ }
50
+ let t5;
51
+ if ($[4] !== t4) {
52
+ t5 = {
53
+ opacity: 1,
54
+ y: 0,
55
+ transition: t4
56
+ };
57
+ $[4] = t4;
58
+ $[5] = t5;
59
+ } else {
60
+ t5 = $[5];
61
+ }
62
+ const t6 = -distance;
63
+ let t7;
64
+ if ($[6] !== shouldReduceMotion) {
65
+ t7 = shouldReduceMotion ? INSTANT_TRANSITION : {
66
+ duration: MOTION_TIMING.exit,
67
+ ease: MOTION_EASE
68
+ };
69
+ $[6] = shouldReduceMotion;
70
+ $[7] = t7;
71
+ } else {
72
+ t7 = $[7];
73
+ }
74
+ let t8;
75
+ if ($[8] !== t6 || $[9] !== t7) {
76
+ t8 = {
77
+ opacity: 0,
78
+ y: t6,
79
+ transition: t7
80
+ };
81
+ $[8] = t6;
82
+ $[9] = t7;
83
+ $[10] = t8;
84
+ } else {
85
+ t8 = $[10];
86
+ }
87
+ let t9;
88
+ if ($[11] !== MotionElement || $[12] !== children || $[13] !== className || $[14] !== t1 || $[15] !== t2 || $[16] !== t3 || $[17] !== t5 || $[18] !== t8) {
89
+ t9 = /* @__PURE__ */ _jsx(MotionElement, {
90
+ "data-slot": "presence-swap",
91
+ "aria-hidden": t1,
92
+ inert: t2,
93
+ className,
94
+ initial: t3,
95
+ animate: t5,
96
+ exit: t8,
97
+ children
98
+ });
99
+ $[11] = MotionElement;
100
+ $[12] = children;
101
+ $[13] = className;
102
+ $[14] = t1;
103
+ $[15] = t2;
104
+ $[16] = t3;
105
+ $[17] = t5;
106
+ $[18] = t8;
107
+ $[19] = t9;
108
+ } else {
109
+ t9 = $[19];
110
+ }
111
+ return t9;
112
+ }
113
+ /**
114
+ * Swaps keyed content with a short lift-and-fade; the first render is not animated.
115
+ * @param props - Key identifying the current state and its content.
116
+ * @returns The animated content.
117
+ */
118
+ export function PresenceSwap(t0) {
119
+ const $ = _c(11);
120
+ const { presenceKey, children, as: t1, className, mode: t2 } = t0;
121
+ const as = t1 === undefined ? "div" : t1;
122
+ const mode = t2 === undefined ? "wait" : t2;
123
+ const t3 = as === "span" && "inline-flex items-center";
124
+ let t4;
125
+ if ($[0] !== className || $[1] !== t3) {
126
+ t4 = cn("min-w-0", t3, className);
127
+ $[0] = className;
128
+ $[1] = t3;
129
+ $[2] = t4;
130
+ } else {
131
+ t4 = $[2];
132
+ }
133
+ let t5;
134
+ if ($[3] !== as || $[4] !== children || $[5] !== presenceKey || $[6] !== t4) {
135
+ t5 = /* @__PURE__ */ _jsx(PresenceSwapItem, {
136
+ as,
137
+ className: t4,
138
+ children
139
+ }, presenceKey);
140
+ $[3] = as;
141
+ $[4] = children;
142
+ $[5] = presenceKey;
143
+ $[6] = t4;
144
+ $[7] = t5;
145
+ } else {
146
+ t5 = $[7];
147
+ }
148
+ let t6;
149
+ if ($[8] !== mode || $[9] !== t5) {
150
+ t6 = /* @__PURE__ */ _jsx(AnimatePresence, {
151
+ mode,
152
+ initial: false,
153
+ children: t5
154
+ });
155
+ $[8] = mode;
156
+ $[9] = t5;
157
+ $[10] = t6;
158
+ } else {
159
+ t6 = $[10];
160
+ }
161
+ return t6;
162
+ }
@@ -0,0 +1,20 @@
1
+ /** Compact circular progress indicator whose arc inherits the surrounding text color. */
2
+ import type { ComponentProps } from "react";
3
+ export interface ProgressRingProps extends Omit<ComponentProps<"svg">, "children"> {
4
+ /** Completion ratio in the `[0, 1]` range; values outside are clamped. */
5
+ fraction: number;
6
+ /** Rendered width and height in pixels. */
7
+ size?: number;
8
+ /**
9
+ * Accessible name. Without it the ring is decorative (`aria-hidden`), which fits when an
10
+ * adjacent label such as `3 / 4` already conveys the value.
11
+ */
12
+ label?: string;
13
+ }
14
+ /**
15
+ * Draws a ring whose arc covers `fraction` of the circle. The arc uses `currentColor`, so the
16
+ * parent's text color (for example success or warning) styles it without extra props.
17
+ * @param props - Fraction, size, optional accessible name and SVG attributes.
18
+ * @returns The progress ring.
19
+ */
20
+ export declare function ProgressRing({ fraction, size, label, className, ...svgProps }: ProgressRingProps): import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ const PROGRESS_RING_VIEWBOX_SIZE = 18;
4
+ const PROGRESS_RING_CENTER = PROGRESS_RING_VIEWBOX_SIZE / 2;
5
+ const PROGRESS_RING_RADIUS = 7;
6
+ const PROGRESS_RING_STROKE_WIDTH = 2.5;
7
+ const PROGRESS_RING_CIRCUMFERENCE = 2 * Math.PI * PROGRESS_RING_RADIUS;
8
+ const DEFAULT_PROGRESS_RING_SIZE_PX = 18;
9
+ /**
10
+ * Draws a ring whose arc covers `fraction` of the circle. The arc uses `currentColor`, so the
11
+ * parent's text color (for example success or warning) styles it without extra props.
12
+ * @param props - Fraction, size, optional accessible name and SVG attributes.
13
+ * @returns The progress ring.
14
+ */
15
+ export function ProgressRing({ fraction, size = DEFAULT_PROGRESS_RING_SIZE_PX, label, className, ...svgProps }) {
16
+ const clampedFraction = Math.min(Math.max(Number.isFinite(fraction) ? fraction : 0, 0), 1);
17
+ const dashOffset = PROGRESS_RING_CIRCUMFERENCE * (1 - clampedFraction);
18
+ const accessibilityProps = label ? {
19
+ role: "progressbar",
20
+ "aria-label": label,
21
+ "aria-valuemin": 0,
22
+ "aria-valuemax": 100,
23
+ "aria-valuenow": Math.round(clampedFraction * 100)
24
+ } : { "aria-hidden": true };
25
+ return /* @__PURE__ */ _jsxs("svg", {
26
+ "data-slot": "progress-ring",
27
+ ...accessibilityProps,
28
+ ...svgProps,
29
+ className: cn("shrink-0 -rotate-90", className),
30
+ height: size,
31
+ viewBox: `0 0 ${PROGRESS_RING_VIEWBOX_SIZE} ${PROGRESS_RING_VIEWBOX_SIZE}`,
32
+ width: size,
33
+ children: [/* @__PURE__ */ _jsx("circle", {
34
+ className: "stroke-border/70",
35
+ cx: PROGRESS_RING_CENTER,
36
+ cy: PROGRESS_RING_CENTER,
37
+ fill: "none",
38
+ r: PROGRESS_RING_RADIUS,
39
+ strokeWidth: PROGRESS_RING_STROKE_WIDTH
40
+ }), clampedFraction > 0 ? /* @__PURE__ */ _jsx("circle", {
41
+ "data-slot": "progress-ring-indicator",
42
+ cx: PROGRESS_RING_CENTER,
43
+ cy: PROGRESS_RING_CENTER,
44
+ fill: "none",
45
+ r: PROGRESS_RING_RADIUS,
46
+ stroke: "currentColor",
47
+ strokeDasharray: PROGRESS_RING_CIRCUMFERENCE,
48
+ strokeDashoffset: dashOffset,
49
+ strokeLinecap: "round",
50
+ strokeWidth: PROGRESS_RING_STROKE_WIDTH
51
+ }) : null]
52
+ });
53
+ }