tempest-react-sdk 0.63.0 → 0.65.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 (133) hide show
  1. package/README.md +5 -5
  2. package/dist/components/Chat/Chat.cjs +1 -1
  3. package/dist/components/Chat/Chat.cjs.map +1 -1
  4. package/dist/components/Chat/Chat.js +71 -96
  5. package/dist/components/Chat/Chat.js.map +1 -1
  6. package/dist/components/Chat/Chat.module.cjs +1 -1
  7. package/dist/components/Chat/Chat.module.cjs.map +1 -1
  8. package/dist/components/Chat/Chat.module.js +30 -8
  9. package/dist/components/Chat/Chat.module.js.map +1 -1
  10. package/dist/components/Chat/ChatActionsMenu.cjs +2 -0
  11. package/dist/components/Chat/ChatActionsMenu.cjs.map +1 -0
  12. package/dist/components/Chat/ChatActionsMenu.js +14 -0
  13. package/dist/components/Chat/ChatActionsMenu.js.map +1 -0
  14. package/dist/components/Chat/ChatAttachments.cjs +2 -0
  15. package/dist/components/Chat/ChatAttachments.cjs.map +1 -0
  16. package/dist/components/Chat/ChatAttachments.js +80 -0
  17. package/dist/components/Chat/ChatAttachments.js.map +1 -0
  18. package/dist/components/Chat/ChatBubble.cjs +2 -0
  19. package/dist/components/Chat/ChatBubble.cjs.map +1 -0
  20. package/dist/components/Chat/ChatBubble.js +155 -0
  21. package/dist/components/Chat/ChatBubble.js.map +1 -0
  22. package/dist/components/Chat/chat-groups.cjs +1 -1
  23. package/dist/components/Chat/chat-groups.cjs.map +1 -1
  24. package/dist/components/Chat/chat-groups.js +42 -2
  25. package/dist/components/Chat/chat-groups.js.map +1 -1
  26. package/dist/components/Chat/chat-receipt.cjs +2 -0
  27. package/dist/components/Chat/chat-receipt.cjs.map +1 -0
  28. package/dist/components/Chat/chat-receipt.js +14 -0
  29. package/dist/components/Chat/chat-receipt.js.map +1 -0
  30. package/dist/components/Command/Command.module.cjs.map +1 -1
  31. package/dist/components/Command/Command.module.js.map +1 -1
  32. package/dist/components/ContextMenu/ContextMenu.cjs +1 -1
  33. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  34. package/dist/components/ContextMenu/ContextMenu.js +100 -48
  35. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  36. package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
  37. package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
  38. package/dist/components/DataTable/DataTable.cjs +1 -1
  39. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  40. package/dist/components/DataTable/DataTable.js +1 -0
  41. package/dist/components/DataTable/DataTable.js.map +1 -1
  42. package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
  43. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
  44. package/dist/components/DataTable/use-dev-warnings.js +4 -3
  45. package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
  46. package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
  47. package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
  48. package/dist/components/InstallBanner/InstallBanner.cjs +1 -1
  49. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  50. package/dist/components/InstallBanner/InstallBanner.js +62 -37
  51. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  52. package/dist/components/InstallBanner/InstallBanner.module.cjs +1 -1
  53. package/dist/components/InstallBanner/InstallBanner.module.cjs.map +1 -1
  54. package/dist/components/InstallBanner/InstallBanner.module.js +4 -3
  55. package/dist/components/InstallBanner/InstallBanner.module.js.map +1 -1
  56. package/dist/components/InstallButton/InstallButton.cjs +1 -1
  57. package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
  58. package/dist/components/InstallButton/InstallButton.js +34 -10
  59. package/dist/components/InstallButton/InstallButton.js.map +1 -1
  60. package/dist/components/InstallButton/InstallButton.module.cjs +3 -0
  61. package/dist/components/InstallButton/InstallButton.module.cjs.map +1 -0
  62. package/dist/components/InstallButton/InstallButton.module.js +10 -0
  63. package/dist/components/InstallButton/InstallButton.module.js.map +1 -0
  64. package/dist/components/InstallButton/install-hints.cjs +2 -0
  65. package/dist/components/InstallButton/install-hints.cjs.map +1 -0
  66. package/dist/components/InstallButton/install-hints.js +32 -0
  67. package/dist/components/InstallButton/install-hints.js.map +1 -0
  68. package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
  69. package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
  70. package/dist/components/Select/Select.cjs +1 -1
  71. package/dist/components/Select/Select.cjs.map +1 -1
  72. package/dist/components/Select/Select.js +43 -40
  73. package/dist/components/Select/Select.js.map +1 -1
  74. package/dist/components/Select/Select.module.cjs +1 -1
  75. package/dist/components/Select/Select.module.cjs.map +1 -1
  76. package/dist/components/Select/Select.module.js +5 -3
  77. package/dist/components/Select/Select.module.js.map +1 -1
  78. package/dist/hooks/use-install-prompt.cjs +1 -1
  79. package/dist/hooks/use-install-prompt.cjs.map +1 -1
  80. package/dist/hooks/use-install-prompt.js +9 -2
  81. package/dist/hooks/use-install-prompt.js.map +1 -1
  82. package/dist/http/api-client.cjs +1 -1
  83. package/dist/http/api-client.cjs.map +1 -1
  84. package/dist/http/api-client.js +80 -74
  85. package/dist/http/api-client.js.map +1 -1
  86. package/dist/http/decode-response.cjs +2 -0
  87. package/dist/http/decode-response.cjs.map +1 -0
  88. package/dist/http/decode-response.js +28 -0
  89. package/dist/http/decode-response.js.map +1 -0
  90. package/dist/styles/InstallButton.css +2 -0
  91. package/dist/styles/actions.css +6 -1
  92. package/dist/styles/advanced.css +4 -2
  93. package/dist/styles/chat.css +28 -0
  94. package/dist/styles/component/Chat.css +28 -0
  95. package/dist/styles/component/Command.css +1 -1
  96. package/dist/styles/component/ContextMenu.css +3 -1
  97. package/dist/styles/component/DropdownMenu.css +1 -1
  98. package/dist/styles/component/InstallBanner.css +2 -0
  99. package/dist/styles/component/InstallButton.css +4 -0
  100. package/dist/styles/component/SegmentedControl.css +1 -1
  101. package/dist/styles/component/Select.css +4 -0
  102. package/dist/styles/core.css +2 -2
  103. package/dist/styles/forms.css +4 -0
  104. package/dist/styles/manifest.json +14 -1
  105. package/dist/styles/navigation.css +1 -1
  106. package/dist/styles/tokens.css +2 -2
  107. package/dist/styles.css +1 -1
  108. package/dist/tempest-react-sdk.cjs +1 -1
  109. package/dist/tempest-react-sdk.d.ts +511 -36
  110. package/dist/tempest-react-sdk.js +319 -316
  111. package/dist/theme/ThemeProvider.cjs +1 -1
  112. package/dist/theme/ThemeProvider.cjs.map +1 -1
  113. package/dist/theme/ThemeProvider.js +40 -45
  114. package/dist/theme/ThemeProvider.js.map +1 -1
  115. package/dist/theme/create-theme.cjs +3 -3
  116. package/dist/theme/create-theme.cjs.map +1 -1
  117. package/dist/theme/create-theme.js +109 -57
  118. package/dist/theme/create-theme.js.map +1 -1
  119. package/dist/theme/initial-theme.cjs +2 -2
  120. package/dist/theme/initial-theme.cjs.map +1 -1
  121. package/dist/theme/initial-theme.js +7 -12
  122. package/dist/theme/initial-theme.js.map +1 -1
  123. package/dist/utils/format.cjs +1 -1
  124. package/dist/utils/format.cjs.map +1 -1
  125. package/dist/utils/format.js +5 -3
  126. package/dist/utils/format.js.map +1 -1
  127. package/dist/utils/storage.cjs +1 -1
  128. package/dist/utils/storage.cjs.map +1 -1
  129. package/dist/utils/storage.js +13 -0
  130. package/dist/utils/storage.js.map +1 -1
  131. package/loader/css-loader-hooks.mjs +37 -0
  132. package/loader/css-loader.mjs +46 -0
  133. package/package.json +3 -1
@@ -597,6 +597,33 @@ export declare interface ApiClient {
597
597
  put<T>(path: string, options?: RequestOptions): Promise<T>;
598
598
  patch<T>(path: string, options?: RequestOptions): Promise<T>;
599
599
  delete<T>(path: string, options?: RequestOptions): Promise<T>;
600
+ /**
601
+ * Download a binary body through the full client pipeline.
602
+ *
603
+ * Same base URL, `getToken` header, 401 refresh-and-replay, `onUnauthorized`,
604
+ * logging, timeout and retry policy as {@link ApiClient.request} — only the
605
+ * decoding differs. Without it every download left the client: a hand-rolled
606
+ * `fetch` that re-declares the `Authorization` header and re-implements the
607
+ * error handling, and that has no refresh, which is the client's reason to
608
+ * exist.
609
+ *
610
+ * @param path - Path joined onto the client's base URL.
611
+ * @param options - The same options `request` takes; `method` defaults to GET.
612
+ * @returns The response body as a `Blob`.
613
+ * @throws TempestApiError On any non-2xx response, carrying the status.
614
+ */
615
+ blob(path: string, options?: RequestOptions): Promise<Blob>;
616
+ /**
617
+ * Download a binary body as an `ArrayBuffer`, for the callers a `Blob` does
618
+ * not serve — a decoder, a hash, a typed array fed to WebGL or to
619
+ * `onnxruntime-web`.
620
+ *
621
+ * @param path - Path joined onto the client's base URL.
622
+ * @param options - The same options `request` takes; `method` defaults to GET.
623
+ * @returns The response body as an `ArrayBuffer`.
624
+ * @throws TempestApiError On any non-2xx response, carrying the status.
625
+ */
626
+ arrayBuffer(path: string, options?: RequestOptions): Promise<ArrayBuffer>;
600
627
  upload<T>(path: string, formData: FormData, method?: "POST" | "PUT" | "PATCH", options?: Omit<RequestOptions, "body" | "method">): Promise<T>;
601
628
  }
602
629
 
@@ -2362,7 +2389,67 @@ export declare type ChartColorToken = string;
2362
2389
  * onRetry={(message) => resend(message.id)}
2363
2390
  * />
2364
2391
  */
2365
- export declare function Chat({ messages, currentUserId, onSend, onRetry, typing, renderAvatar, header, emptyState, groupWindowMs, locale, now, placeholder, composerActions, composerDisabled, onSendError, className, ...rest }: ChatProps): JSX.Element;
2392
+ export declare function Chat({ messages, currentUserId, onSend, onRetry, typing, renderAvatar, header, emptyState, groupWindowMs, locale, now, placeholder, composerActions, composerDisabled, onSendError, onReact, messageActions, onQuoteClick, className, ...rest }: ChatProps): JSX.Element;
2393
+
2394
+ /** One attachment on a message. */
2395
+ export declare interface ChatAttachment {
2396
+ /** Decides how it renders: a picture, a player, a voice note, a download row. */
2397
+ kind: ChatAttachmentKind;
2398
+ /** Where the bytes are. A blob URL works for a message still uploading. */
2399
+ url: string;
2400
+ /** File name, shown for `"file"` and used as the download name. */
2401
+ name?: string;
2402
+ /** MIME type, when the app knows it. */
2403
+ mimeType?: string;
2404
+ /** Size in bytes, shown next to a file. */
2405
+ sizeBytes?: number;
2406
+ /** Duration in ms, for audio, voice and video. */
2407
+ durationMs?: number;
2408
+ /** Poster for a video, or a smaller copy of an image. */
2409
+ thumbnailUrl?: string;
2410
+ /**
2411
+ * Normalised peaks, `0`–`1`, for a voice note.
2412
+ *
2413
+ * A voice note without one is a grey rectangle: the waveform is what tells
2414
+ * somebody whether this is a word or a two-minute monologue.
2415
+ */
2416
+ waveform?: readonly number[];
2417
+ /** Alternative text for an image. */
2418
+ alt?: string;
2419
+ }
2420
+
2421
+ /** What a bubble can carry besides text. */
2422
+ export declare type ChatAttachmentKind = "image" | "video" | "audio" | "voice" | "file";
2423
+
2424
+ /**
2425
+ * One message bubble.
2426
+ *
2427
+ * Everything a messenger needs on a single message lives here, because every one
2428
+ * of these was a thing each app rebuilt on top of a text-only bubble: the reply
2429
+ * stub, attachments, reactions, per-recipient ticks, the actions menu, and the
2430
+ * tombstone of a deleted message.
2431
+ *
2432
+ * @param props - The message and its handlers.
2433
+ * @returns The bubble, wrapped in a context menu when actions are given.
2434
+ */
2435
+ export declare function ChatBubble({ message, own, locale, onRetry, onReact, messageActions, onQuoteClick, }: ChatBubbleProps): JSX.Element;
2436
+
2437
+ export declare interface ChatBubbleProps {
2438
+ /** The message this bubble renders. */
2439
+ message: ChatMessage;
2440
+ /** Whether it belongs to the current user — side, colour and ticks. */
2441
+ own: boolean;
2442
+ /** Locale for labels. */
2443
+ locale: "pt-BR" | "en";
2444
+ /** Retry handler for a `"failed"` message. */
2445
+ onRetry?: (message: ChatMessage) => void;
2446
+ /** Toggles a reaction. Receives the emoji that was pressed. */
2447
+ onReact?: (message: ChatMessage, emoji: string) => void;
2448
+ /** Per-message actions, opened by right click, long press or the ⋮ button. */
2449
+ messageActions?: (message: ChatMessage) => ContextMenuItem[];
2450
+ /** Called when the quote stub is activated — scroll to the quoted message. */
2451
+ onQuoteClick?: (message: ChatMessage) => void;
2452
+ }
2366
2453
 
2367
2454
  /**
2368
2455
  * The message field of a thread: a textarea that grows with its content, sends on
@@ -2422,8 +2509,14 @@ export declare interface ChatDay {
2422
2509
  export declare interface ChatMessage {
2423
2510
  /** Stable identity. Used as the React key and by `onRetry`. */
2424
2511
  id: string;
2425
- /** What was said. A node, so an app can render a link, an image or a quote. */
2426
- body: ReactNode;
2512
+ /**
2513
+ * What was said. A node, so an app can render a link, an image or a quote.
2514
+ *
2515
+ * Optional, because a message can be all attachment, or be a tombstone: a
2516
+ * deleted message has no body to show, and requiring one made every app
2517
+ * invent a placeholder string for the state the component now owns.
2518
+ */
2519
+ body?: ReactNode;
2427
2520
  /** Who said it. Compared against `currentUserId` to decide sides. */
2428
2521
  authorId: string;
2429
2522
  /** Display name. Falls back to `authorId` in the header of a run. */
@@ -2438,6 +2531,28 @@ export declare interface ChatMessage {
2438
2531
  * sitting right there.
2439
2532
  */
2440
2533
  status?: "sending" | "sent" | "read" | "failed";
2534
+ /**
2535
+ * Per-recipient delivery. Takes precedence over `status` for the ticks.
2536
+ *
2537
+ * Use it in a group, where "delivered to all" and "read by all" are different
2538
+ * states and `status` can only say one thing.
2539
+ */
2540
+ receipt?: ChatReceipt;
2541
+ /** Media, voice notes and documents carried by this message. */
2542
+ attachments?: readonly ChatAttachment[];
2543
+ /** The message this one replies to. */
2544
+ quote?: ChatQuote;
2545
+ /** Emoji reactions, already tallied by the app. */
2546
+ reactions?: readonly ChatReaction[];
2547
+ /**
2548
+ * The message was deleted — it renders as a tombstone.
2549
+ *
2550
+ * A state, not a `body` the app swaps for a string: as a state the quote of
2551
+ * it can be blanked too, and every app stops writing its own wording.
2552
+ */
2553
+ deleted?: boolean;
2554
+ /** The message was edited after it was sent. */
2555
+ edited?: boolean;
2441
2556
  /** Anything the app wants to carry through to its own renderers. */
2442
2557
  data?: Record<string, unknown>;
2443
2558
  }
@@ -2473,8 +2588,80 @@ export declare interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>,
2473
2588
  composerDisabled?: boolean;
2474
2589
  /** Called when `onSend` rejects. The draft stays in the field either way. */
2475
2590
  onSendError?: (error: unknown) => void;
2591
+ /**
2592
+ * Toggles a reaction on a message. Enables the reaction chips.
2593
+ *
2594
+ * Called with the emoji that was pressed, including one the user already
2595
+ * reacted with: reacting again clears it, rather than stacking a second copy.
2596
+ * The app owns that rule — the component only reports the press.
2597
+ */
2598
+ onReact?: (message: ChatMessage, emoji: string) => void;
2599
+ /**
2600
+ * Per-message actions: reply, forward, star, edit, delete.
2601
+ *
2602
+ * Returns the items for one message, opened by right click, by long press on
2603
+ * touch, and by a `⋮` button on the bubble. Buttons painted on every bubble
2604
+ * turn a conversation into a toolbar, which is why this is a menu.
2605
+ */
2606
+ messageActions?: (message: ChatMessage) => ContextMenuItem[];
2607
+ /** Called when a reply stub is activated — scroll to the quoted message. */
2608
+ onQuoteClick?: (message: ChatMessage) => void;
2609
+ }
2610
+
2611
+ /**
2612
+ * The stub of the message being replied to, shown above the body.
2613
+ *
2614
+ * `revoked` is not decoration: deleting the quoted message has to blank the
2615
+ * quoted text, or it leaks through everyone who replied to it.
2616
+ */
2617
+ export declare interface ChatQuote {
2618
+ /** Id of the quoted message, so the app can scroll to it. */
2619
+ messageId: string;
2620
+ /** Who wrote it. */
2621
+ senderName?: string;
2622
+ /** A short piece of what it said. The component never truncates for you. */
2623
+ excerpt?: string;
2624
+ /** What the quoted message was, when it was not text. */
2625
+ kind?: "text" | ChatAttachmentKind;
2626
+ /** The quoted message was deleted — the excerpt is not shown. */
2627
+ revoked?: boolean;
2476
2628
  }
2477
2629
 
2630
+ /** One emoji on a message, with its tally. */
2631
+ export declare interface ChatReaction {
2632
+ /** The emoji itself. */
2633
+ emoji: string;
2634
+ /** How many people reacted with it. */
2635
+ count: number;
2636
+ /** Whether the current user is one of them — the chip reads as pressed. */
2637
+ reacted?: boolean;
2638
+ /** Who reacted, for the chip's tooltip. */
2639
+ names?: readonly string[];
2640
+ }
2641
+
2642
+ /**
2643
+ * Per-recipient delivery, which is what a group needs.
2644
+ *
2645
+ * `status` is a boolean for the sender: it cannot say "delivered to everyone"
2646
+ * versus "read by everyone", and only the second one turns the ticks blue.
2647
+ */
2648
+ export declare interface ChatReceipt {
2649
+ /** How many recipients have received it. */
2650
+ deliveredTo: number;
2651
+ /** How many have read it. */
2652
+ readBy: number;
2653
+ /** How many recipients there are. */
2654
+ totalRecipients: number;
2655
+ }
2656
+
2657
+ /**
2658
+ * What the ticks on an outgoing message say.
2659
+ *
2660
+ * `"read"` is the only one that turns them blue, and in a group that means
2661
+ * **everyone** has read it — the distinction `status: "read"` cannot make.
2662
+ */
2663
+ export declare type ChatReceiptState = "sending" | "sent" | "delivered" | "read" | "failed";
2664
+
2478
2665
  /** A run of consecutive messages from one author, under one day. */
2479
2666
  export declare interface ChatRun {
2480
2667
  kind: "run";
@@ -2936,17 +3123,26 @@ export declare interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
2936
3123
  export declare type ContainerSize = "sm" | "md" | "lg" | "xl" | "full";
2937
3124
 
2938
3125
  /**
2939
- * Right-click context menu.
3126
+ * Context menu — right click, long press, or click.
2940
3127
  *
2941
- * - Opens at the cursor position on `onContextMenu` (default browser menu suppressed).
2942
- * - Rendered through a {@link Portal} so it escapes parent overflow/stacking contexts.
2943
- * - Closes on outside click, Escape, or item selection.
2944
- * - Arrow Up/Down move focus across selectable items; Enter activates the focused item.
3128
+ * - Opens at the pointer on `contextmenu`, and on a long press from touch or pen,
3129
+ * which is the gesture those pointers have instead of a right click. `trigger`
3130
+ * adds or replaces the mouse gesture; the long press is independent of it.
3131
+ * - Clamped inside the viewport after it mounts, so a menu opened near an edge
3132
+ * moves in rather than overflowing — measured before the clamp: a 180 px menu
3133
+ * opened at `x=235` in a 320 px window ran 95 px off screen, cutting the right
3134
+ * edge off every label.
3135
+ * - Rendered through a {@link Portal} so it escapes parent overflow/stacking
3136
+ * contexts.
3137
+ * - Closes on outside click, Escape, scroll, resize, or item selection.
3138
+ * - Arrow Up/Down move focus across selectable items and wrap; Home/End jump to
3139
+ * the ends; Enter activates the focused item. The menu itself takes focus when
3140
+ * it opens, so a screen reader announces it instead of staying on the trigger.
2945
3141
  *
2946
3142
  * @param props - The context menu props.
2947
3143
  * @returns The trigger wrapper plus the portalled menu when open.
2948
3144
  */
2949
- export declare function ContextMenu({ items, children, className }: ContextMenuProps): JSX.Element;
3145
+ export declare function ContextMenu({ items, children, className, trigger, longPressDelay, viewportMargin, }: ContextMenuProps): JSX.Element;
2950
3146
 
2951
3147
  export declare type ContextMenuItem = {
2952
3148
  label: ReactNode;
@@ -2964,8 +3160,33 @@ export declare interface ContextMenuProps {
2964
3160
  children: ReactNode;
2965
3161
  /** Extra class names forwarded to the menu element. */
2966
3162
  className?: string;
3163
+ /**
3164
+ * Which pointer gesture opens the menu. Default `"contextmenu"`.
3165
+ *
3166
+ * `"click"` is the `⋮` button case, where a right click is the wrong gesture
3167
+ * on the desktop and impossible on touch. `"both"` keeps the right click and
3168
+ * adds the left one.
3169
+ */
3170
+ trigger?: ContextMenuTrigger;
3171
+ /**
3172
+ * Hold, in ms, that opens the menu from a coarse pointer. Default `500`.
3173
+ *
3174
+ * Set `0` to turn the long press off. It is on by default because touch has
3175
+ * no right click: measured in Chrome with touch emulation (Pixel 7 and
3176
+ * iPhone 13 profiles), a 900 ms hold fires `pointerdown`, `touchstart`,
3177
+ * `pointerup`, `touchend` and `click` — and no `contextmenu` at all. Every
3178
+ * action behind this menu was unreachable on a phone.
3179
+ */
3180
+ longPressDelay?: number;
3181
+ /**
3182
+ * Gap, in px, kept between the menu and the edge of the viewport. Default `8`.
3183
+ */
3184
+ viewportMargin?: number;
2967
3185
  }
2968
3186
 
3187
+ /** How the menu is opened by a pointer. Touch always gets the long press. */
3188
+ declare type ContextMenuTrigger = "contextmenu" | "click" | "both";
3189
+
2969
3190
  /** WCAG 2.x contrast ratio between two hex colors, from `1` to `21`. */
2970
3191
  export declare function contrastRatio(a: string, b: string): number;
2971
3192
 
@@ -3039,6 +3260,12 @@ export declare const CPFInput: ForwardRefExoticComponent<Omit<InputProps, "value
3039
3260
  * is the signal to clear the session. Without `refresh`, the first `401` calls
3040
3261
  * it directly.
3041
3262
  *
3263
+ * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which
3264
+ * run the same pipeline and skip the parse. `request()` no longer decodes an
3265
+ * unknown `Content-Type` as text either: it returns a `Blob` and says so once in
3266
+ * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes
3267
+ * on the way in.
3268
+ *
3042
3269
  * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}
3043
3270
  * for the built-in policy; it never replays a write.
3044
3271
  *
@@ -3059,7 +3286,8 @@ export declare const CPFInput: ForwardRefExoticComponent<Omit<InputProps, "value
3059
3286
  * });
3060
3287
  *
3061
3288
  * @param config - Base URL plus the optional auth, retry and fetch hooks.
3062
- * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.
3289
+ * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/
3290
+ * `arrayBuffer`/`upload`.
3063
3291
  */
3064
3292
  export declare function createApiClient(config: ApiClientConfig): ApiClient;
3065
3293
 
@@ -3401,7 +3629,7 @@ export declare function createInMemoryFlags(options?: InMemoryFlagsOptions): Fea
3401
3629
  * const packed = createJsonStorage(compressedStorageCodec);
3402
3630
  *
3403
3631
  * @param codec - Encoder pair. Defaults to plain JSON.
3404
- * @returns A store with `get` / `set` / `remove`.
3632
+ * @returns A store with `get` / `set` / `getRaw` / `setRaw` / `remove`.
3405
3633
  *
3406
3634
  * @tempest-limits empty-catch — every method is best-effort by contract:
3407
3635
  * `localStorage` throws on quota exhaustion, in Safari private mode, and when a
@@ -4256,7 +4484,15 @@ export declare interface CreateThemeOptions {
4256
4484
  chart?: string[];
4257
4485
  /** Corner radius scale. A preset name, or explicit per-step values. */
4258
4486
  radius?: ThemeRadius | Partial<Record<"xs" | "sm" | "md" | "lg" | "xl" | "2xl", string>>;
4259
- /** Alpha of `--tempest-focus-ring-color`, derived from the brand color. Default `0.35`. */
4487
+ /**
4488
+ * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.
4489
+ *
4490
+ * A translucent ring has no contrast of its own; it has the contrast of
4491
+ * whatever it composites over. Passing a value below `1` reintroduces the
4492
+ * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a
4493
+ * development build. Kept because a translucent ring over a background you
4494
+ * control is a legitimate choice.
4495
+ */
4260
4496
  focusRingAlpha?: number;
4261
4497
  /** Selector the light tokens are written under. Default `":root"`. */
4262
4498
  selector?: string;
@@ -5188,6 +5424,23 @@ export declare const DEFAULT_PUSH_TO_TALK_KEY = "Space";
5188
5424
  */
5189
5425
  export declare const DEFAULT_VOICE_CHAIN: VoiceChainSettings;
5190
5426
 
5427
+ /**
5428
+ * The default instruction for a browser that cannot be prompted.
5429
+ *
5430
+ * Written out rather than left to the app because every app wrote the same two
5431
+ * sentences, and getting them wrong is invisible: an iOS user who is told to
5432
+ * look for an "Install" menu never finds one, because Safari puts it behind the
5433
+ * Share sheet.
5434
+ *
5435
+ * On an Android fork the copy also offers the `intent://` link the SDK already
5436
+ * builds — reopening the page in Chrome is the only path that reaches a real
5437
+ * install on a browser whose menu has no install entry at all.
5438
+ *
5439
+ * @param input - The resolved method and the optional Chrome intent.
5440
+ * @returns The instruction, or `null` when the method needs none.
5441
+ */
5442
+ export declare function defaultInstallHint({ method, openInChromeIntent }: InstallHintInput): ReactNode;
5443
+
5191
5444
  /**
5192
5445
  * Identity helper that types a declarative route tree. Use it so editors give
5193
5446
  * autocomplete and type-checking on every node; the array is returned as-is.
@@ -6331,12 +6584,37 @@ export declare function formatDateTimeForInput(value: string | Date): string;
6331
6584
  export declare function formatDurationMs(value: number): string;
6332
6585
 
6333
6586
  /**
6334
- * Format a fraction (0-1) as a percentage with one decimal.
6587
+ * Format a fraction (0-1) as a percentage.
6588
+ *
6589
+ * A non-finite input renders as `"—"` rather than `"NaN%"` or `"∞%"`, which is
6590
+ * what `Intl.NumberFormat` produces and what a division by zero upstream puts on
6591
+ * the screen. It matches `formatDurationMs`, which already answers that way.
6592
+ *
6593
+ * @example
6594
+ * ```typescript
6595
+ * formatPercent(0.9874); // "98,7%"
6596
+ * formatPercent(0.9874, { decimals: 2 }); // "98,74%"
6597
+ * formatPercent(0 / 0); // "—"
6598
+ * ```
6335
6599
  *
6336
6600
  * @param value - Fraction between 0 and 1.
6337
- * @returns Formatted percent string, e.g. "12,5%".
6601
+ * @param options - Rendering options; `decimals` defaults to 1.
6602
+ * @returns Formatted percent string, e.g. "12,5%", or `"—"` for a non-finite input.
6338
6603
  */
6339
- export declare function formatPercent(value: number): string;
6604
+ export declare function formatPercent(value: number, options?: FormatPercentOptions): string;
6605
+
6606
+ /** How {@link formatPercent} renders a fraction. */
6607
+ export declare interface FormatPercentOptions {
6608
+ /**
6609
+ * Decimal places to show, fixed (padded as well as truncated).
6610
+ *
6611
+ * Defaults to `1`, which is what the function always did. Two is the case
6612
+ * that produced this option: a confidence of 98,74% and one of 98,7% are the
6613
+ * same number on a summary card and two different readings on a detail
6614
+ * screen, so the precision belongs to the call site.
6615
+ */
6616
+ decimals?: number;
6617
+ }
6340
6618
 
6341
6619
  /**
6342
6620
  * Apply the Brazilian phone mask `(XX) XXXXX-XXXX` or `(XX) XXXX-XXXX`.
@@ -7075,11 +7353,15 @@ export declare interface InstallBackgroundSyncOptions {
7075
7353
  }
7076
7354
 
7077
7355
  /**
7078
- * Dismissible bottom banner that invites the user to install the PWA. Wired to
7079
- * {@link useBeforeInstallPrompt}: it renders only when the browser captured an
7080
- * install prompt and the app is not already running standalone — so on
7081
- * platforms that never fire `beforeinstallprompt` (e.g. iOS Safari) it stays
7082
- * hidden and you can surface manual instructions elsewhere.
7356
+ * Dismissible bottom banner that invites the user to install the PWA, wired to
7357
+ * {@link useInstallPrompt}.
7358
+ *
7359
+ * Where the browser fires `beforeinstallprompt`, the button installs. Where it
7360
+ * does not — iOS Safari, and the Android Chromium forks that strip the API — the
7361
+ * banner shows that platform's instruction instead of disappearing. It used to
7362
+ * disappear: built on `useBeforeInstallPrompt`, it rendered `null` exactly where
7363
+ * the user has no other way to find the install entry, which is why every app
7364
+ * kept a local copy of this component just to add the two missing paths.
7083
7365
  *
7084
7366
  * @tempest-limits empty-catch — persisting the dismissal is a courtesy, not the
7085
7367
  * feature. When `localStorage` refuses the write (quota, private mode) the banner
@@ -7093,7 +7375,7 @@ export declare interface InstallBackgroundSyncOptions {
7093
7375
  * storageKey="famacha:install-dismissed"
7094
7376
  * />
7095
7377
  */
7096
- export declare function InstallBanner({ title, description, installLabel, dismissLabel, icon, storageKey, onResult, className, }: InstallBannerProps): JSX.Element | null;
7378
+ export declare function InstallBanner({ title, description, installLabel, hintLabel, dismissLabel, icon, storageKey, declineCooldownMs, renderHint, manualFallbackDelayMs, onResult, className, }: InstallBannerProps): JSX.Element | null;
7097
7379
 
7098
7380
  export declare interface InstallBannerProps {
7099
7381
  /** Headline. Default `"Instale o app"`. */
@@ -7111,29 +7393,95 @@ export declare interface InstallBannerProps {
7111
7393
  * make dismissal last only for the current session (component state).
7112
7394
  */
7113
7395
  storageKey?: string;
7396
+ /**
7397
+ * How long, in ms, a dismissal lasts before the banner may return.
7398
+ *
7399
+ * Omitted, a dismissal written to `storageKey` is permanent, which is what
7400
+ * the component always did and what a stored `"1"` keeps meaning. With a
7401
+ * cooldown the dismissal is stored as a timestamp instead, so the banner
7402
+ * comes back after the window — the same shape `useInstallPrompt` uses for
7403
+ * its own decline.
7404
+ */
7405
+ declineCooldownMs?: number;
7406
+ /**
7407
+ * Install button label used when the browser cannot be prompted and the
7408
+ * banner shows the manual instruction. Default `"Como instalar"`.
7409
+ */
7410
+ hintLabel?: string;
7411
+ /**
7412
+ * Replaces the instruction shown for the `"ios"` and `"manual"` methods.
7413
+ *
7414
+ * The default copy is in PT-BR and names the real menu entries.
7415
+ */
7416
+ renderHint?: RenderInstallHint;
7417
+ /**
7418
+ * How long, in ms, to wait for `beforeinstallprompt` before falling back to
7419
+ * the manual instruction. Forwarded to {@link useInstallPrompt}; defaults to
7420
+ * 3000 there.
7421
+ */
7422
+ manualFallbackDelayMs?: number;
7114
7423
  /** Called with the user's choice after the install prompt resolves. */
7115
7424
  onResult?: (outcome: InstallOutcome) => void;
7116
7425
  className?: string;
7117
7426
  }
7118
7427
 
7119
7428
  /**
7120
- * Button wired to the PWA install prompt ({@link useBeforeInstallPrompt}).
7121
- * Renders nothing when the app can't be installed — no prompt captured yet,
7122
- * already installed, or running standalone — so you can drop it anywhere
7123
- * without guarding visibility yourself.
7429
+ * Button wired to PWA installation, through {@link useInstallPrompt}.
7430
+ *
7431
+ * Where the browser fires `beforeinstallprompt`, the click installs. Where it
7432
+ * does not — iOS Safari, and the Android Chromium forks (Mi Browser, UC, Opera
7433
+ * Mini, Huawei) that strip the API — the click reveals the instruction for that
7434
+ * platform instead of the button disappearing.
7435
+ *
7436
+ * That disappearance was the defect: the component was built on
7437
+ * `useBeforeInstallPrompt`, so it rendered `null` exactly where the user needs
7438
+ * the most help, and every app re-implemented the iOS and manual paths locally.
7439
+ * Nothing renders only when there is genuinely nothing to offer — already
7440
+ * installed, running standalone, or inside the decline cooldown.
7124
7441
  *
7125
7442
  * Inherits every {@link Button} prop (`variant`, `size`, `leftIcon`, …).
7126
7443
  *
7127
7444
  * @example
7128
7445
  * <InstallButton variant="primary" leftIcon={<Download />} />
7446
+ *
7447
+ * @example
7448
+ * <InstallButton renderHint={({ method }) => (method === "ios" ? <IOSSteps /> : <MenuSteps />)} />
7129
7449
  */
7130
- export declare function InstallButton({ label, onResult, ...props }: InstallButtonProps): JSX.Element | null;
7450
+ export declare function InstallButton({ label, hintLabel, onResult, renderHint, manualFallbackDelayMs, wrapperClassName, ...props }: InstallButtonProps): JSX.Element | null;
7131
7451
 
7132
7452
  export declare interface InstallButtonProps extends Omit<ButtonProps, "onClick" | "children"> {
7133
7453
  /** Button label. Default `"Instalar app"`. */
7134
7454
  label?: ReactNode;
7455
+ /**
7456
+ * Label used when the browser cannot be prompted and the button reveals the
7457
+ * manual instruction instead. Default `"Como instalar"`.
7458
+ */
7459
+ hintLabel?: ReactNode;
7135
7460
  /** Called with the user's choice after the install prompt resolves. */
7136
7461
  onResult?: (outcome: InstallOutcome) => void;
7462
+ /**
7463
+ * Replaces the instruction shown for the `"ios"` and `"manual"` methods.
7464
+ *
7465
+ * The default copy is in PT-BR and names the real menu entries. Pass this to
7466
+ * translate it, to shorten it, or to point at a screenshot of your own.
7467
+ */
7468
+ renderHint?: RenderInstallHint;
7469
+ /**
7470
+ * How long, in ms, to wait for `beforeinstallprompt` before falling back to
7471
+ * the manual instruction. Forwarded to {@link useInstallPrompt}; defaults to
7472
+ * 3000 there.
7473
+ */
7474
+ manualFallbackDelayMs?: number;
7475
+ /** Class applied to the wrapper that holds the button and the instruction. */
7476
+ wrapperClassName?: string;
7477
+ }
7478
+
7479
+ /** What an install affordance needs to render the instruction for a method. */
7480
+ export declare interface InstallHintInput {
7481
+ /** The strategy `useInstallPrompt` resolved. */
7482
+ method: InstallMethod;
7483
+ /** An `intent://` URL that re-opens the page in Chrome on Android, if any. */
7484
+ openInChromeIntent: string | null;
7137
7485
  }
7138
7486
 
7139
7487
  /**
@@ -7546,6 +7894,10 @@ export declare interface JsonStorage {
7546
7894
  /**
7547
7895
  * Read and decode a key.
7548
7896
  *
7897
+ * The value is decoded by the store's codec — JSON for {@link storage} —
7898
+ * which is what makes a key written as a bare string unreadable here: it is
7899
+ * not the shape this method writes. {@link JsonStorage.getRaw} reads those.
7900
+ *
7549
7901
  * @typeParam T - The expected value shape.
7550
7902
  * @param key - Storage key.
7551
7903
  * @param fallback - Returned when the key is absent, unreadable, or corrupt.
@@ -7555,11 +7907,42 @@ export declare interface JsonStorage {
7555
7907
  /**
7556
7908
  * Encode and write a key.
7557
7909
  *
7910
+ * The value is **encoded** — `set("theme", "dark")` stores `"dark"` with the
7911
+ * quotes, not `dark`. That matters when an existing key is being adopted:
7912
+ * whatever read it before will no longer recognise what this writes, and the
7913
+ * old value will no longer parse here. {@link JsonStorage.setRaw} is the
7914
+ * passthrough.
7915
+ *
7558
7916
  * @typeParam T - The value being stored.
7559
7917
  * @param key - Storage key.
7560
7918
  * @param value - Any value the codec can represent.
7561
7919
  */
7562
7920
  set<T>(key: string, value: T): void;
7921
+ /**
7922
+ * Read a key exactly as `localStorage` holds it, with no decoding.
7923
+ *
7924
+ * The escape hatch {@link JsonStorage.get} cannot be: `get` runs the codec,
7925
+ * so a key holding a bare `dark` — written by another library, by the app
7926
+ * before it adopted this wrapper, or by the SDK's own `ThemeProvider` —
7927
+ * fails to parse and comes back as the fallback. Reading that key is a
7928
+ * different operation, not a variant of the same one.
7929
+ *
7930
+ * @param key - Storage key.
7931
+ * @returns The stored string, or `null` when the key is absent or storage is
7932
+ * unavailable.
7933
+ */
7934
+ getRaw(key: string): string | null;
7935
+ /**
7936
+ * Write a string exactly as given, with no encoding.
7937
+ *
7938
+ * The counterpart of {@link JsonStorage.getRaw}, and what keeps adoption from
7939
+ * being a silent migration: `set("theme", "dark")` stores `"dark"` **with the
7940
+ * quotes**, which the app's previous reader no longer recognises.
7941
+ *
7942
+ * @param key - Storage key.
7943
+ * @param value - The exact string to store.
7944
+ */
7945
+ setRaw(key: string, value: string): void;
7563
7946
  /**
7564
7947
  * Delete a key.
7565
7948
  *
@@ -11057,6 +11440,24 @@ export declare function readRoundTripMs(report: RTCStatsReport): number | null;
11057
11440
  */
11058
11441
  export declare function readThemeToken(name: string, element?: Element | null): string;
11059
11442
 
11443
+ /**
11444
+ * The sentence a reader gets for a receipt, for the tooltip and the screen reader.
11445
+ *
11446
+ * Partial counts are spelled out rather than rounded to a state: "delivered to 3
11447
+ * of 9" is the information the sender actually wants when the ticks are not blue
11448
+ * yet, and it is the part a glyph cannot carry.
11449
+ *
11450
+ * @param receipt - Per-recipient counts.
11451
+ * @param strings - Locale strings for the thread.
11452
+ * @returns One sentence describing delivery and reading.
11453
+ */
11454
+ export declare function receiptLabel(receipt: ChatReceipt, strings: {
11455
+ deliveredAll: string;
11456
+ deliveredSome: (delivered: number, total: number) => string;
11457
+ readAll: string;
11458
+ readSome: (read: number, total: number) => string;
11459
+ }): string;
11460
+
11060
11461
  export { redirect }
11061
11462
 
11062
11463
  /** Recommended refetch intervals (milliseconds). */
@@ -11249,6 +11650,9 @@ export declare interface RelativeTimeProps extends HTMLAttributes<HTMLTimeElemen
11249
11650
  */
11250
11651
  export declare function removeById<T, V extends Partial<Record<keyof T, unknown>> = T>(idField?: keyof T): (current: T[] | undefined, item: V) => T[];
11251
11652
 
11653
+ /** Renders the instruction shown when the browser has no install prompt. */
11654
+ export declare type RenderInstallHint = (input: InstallHintInput) => ReactNode;
11655
+
11252
11656
  export declare interface ReorderPayload {
11253
11657
  from: number;
11254
11658
  to: number;
@@ -11340,6 +11744,29 @@ export declare function resolveDegradation(quality: MeshQuality, effective: Mesh
11340
11744
 
11341
11745
  export declare type ResolvedTheme = "light" | "dark";
11342
11746
 
11747
+ /**
11748
+ * Resolve the tick state of an outgoing message.
11749
+ *
11750
+ * A group needs three states where a single message has two: sent to the server,
11751
+ * delivered to the recipients, read by the recipients. A boolean `status` collapses
11752
+ * the middle one, so an app that has per-recipient counts either invents its own
11753
+ * glyph or shows a message as read when one person out of nine opened it.
11754
+ *
11755
+ * The counts win when they are there, and the thresholds are deliberately "all":
11756
+ * blue ticks in every messenger mean the whole room, and reporting "read" for a
11757
+ * partial count is the failure that makes people distrust the indicator entirely.
11758
+ * A `totalRecipients` of zero — a note to self, a thread whose membership the app
11759
+ * does not track — falls back to `status`, because "read by all of nobody" is not
11760
+ * a claim worth making.
11761
+ *
11762
+ * `"failed"` and `"sending"` come from `status` regardless: they are about this
11763
+ * side of the wire, and no recipient count exists yet.
11764
+ *
11765
+ * @param message - The message being rendered.
11766
+ * @returns The tick state, or `null` when there is nothing to show.
11767
+ */
11768
+ export declare function resolveReceiptState(message: ChatMessage): ChatReceiptState | null;
11769
+
11343
11770
  /**
11344
11771
  * Responsive value — either a single value applied at all breakpoints, or
11345
11772
  * an object with `mobile` / `tablet` / `desktop` overrides. Apps can mix
@@ -11920,26 +12347,63 @@ export declare interface SegmentedControlProps<TValue extends string = string> e
11920
12347
  }
11921
12348
 
11922
12349
  /**
11923
- * Native `<select>` wrapper with label/helper/error slots. Either provide
11924
- * `options` for a quick render, or pass `<option>` children directly.
12350
+ * Native `<select>` wrapper. Either provide `options` for a quick render, or
12351
+ * pass `<option>` children directly.
12352
+ *
12353
+ * Two variants:
12354
+ *
12355
+ * - `"field"` (default) — label, control, helper text and error slot, for a form.
12356
+ * - `"chip"` — the control alone, sized to its content, for a settings row where
12357
+ * the icon and the label are already on the left and the control belongs on the
12358
+ * right. Rendering the full field there stacks two labelled fields inside one
12359
+ * list item, which is why every app ended up hand-rolling a native `<select>`
12360
+ * with `appearance: none` — and hand-rolling it loses the focus ring, the
12361
+ * disabled styling and the caret this component already solves.
12362
+ *
12363
+ * @example
12364
+ * <Select label="Estado" options={states} placeholder="Selecione" />
12365
+ *
12366
+ * @example
12367
+ * <ListTile title="Idioma" trailing={<Select variant="chip" aria-label="Idioma" options={langs} />} />
11925
12368
  */
11926
12369
  export declare const Select: ForwardRefExoticComponent<SelectProps & RefAttributes<HTMLSelectElement>>;
11927
12370
 
11928
- export declare interface SelectOption {
11929
- value: string | number;
11930
- label: string;
11931
- disabled?: boolean;
12371
+ /** What both variants of {@link Select} accept. */
12372
+ declare interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {
12373
+ options?: SelectOption[];
12374
+ placeholder?: string;
12375
+ wrapperClassName?: string;
11932
12376
  }
11933
12377
 
11934
- export declare interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
12378
+ /**
12379
+ * The bare control, sized to its content.
12380
+ *
12381
+ * `aria-label` is required rather than optional, because the variant removes the
12382
+ * only visible label the component had. A settings row names the control in the
12383
+ * row's own text, which the `<select>` has no relationship to — so without this
12384
+ * the screen reader announces a combobox with no name at all.
12385
+ */
12386
+ declare interface SelectChipProps extends SelectBaseProps {
12387
+ variant: "chip";
12388
+ "aria-label": string;
12389
+ }
12390
+
12391
+ /** The full form field: label, control, helper text and error slot. */
12392
+ declare interface SelectFieldProps extends SelectBaseProps {
12393
+ variant?: "field";
11935
12394
  label?: string;
11936
12395
  helperText?: string;
11937
12396
  error?: string;
11938
- options?: SelectOption[];
11939
- placeholder?: string;
11940
- wrapperClassName?: string;
11941
12397
  }
11942
12398
 
12399
+ export declare interface SelectOption {
12400
+ value: string | number;
12401
+ label: string;
12402
+ disabled?: boolean;
12403
+ }
12404
+
12405
+ export declare type SelectProps = SelectFieldProps | SelectChipProps;
12406
+
11943
12407
  /**
11944
12408
  * Minimal subset of `@sentry/browser` used by the adapter. Lets you pass
11945
12409
  * either the real SDK module (`import * as Sentry from "@sentry/browser"`)
@@ -12855,6 +13319,11 @@ export declare function stopAudio(): void;
12855
13319
  /**
12856
13320
  * Typed wrapper around `localStorage` that JSON-encodes values and silently
12857
13321
  * handles environments where storage is unavailable (SSR, private mode).
13322
+ *
13323
+ * `get`/`set` encode; `getRaw`/`setRaw` do not. Reach for the raw pair when the
13324
+ * key is shared with something that does not speak JSON — an existing key in an
13325
+ * app adopting this wrapper, a key another library owns, or the theme
13326
+ * preference the SDK's own `ThemeProvider` stores as a bare string.
12858
13327
  */
12859
13328
  export declare const storage: JsonStorage;
12860
13329
 
@@ -14986,6 +15455,12 @@ export declare function useIdle(timeout?: number): boolean;
14986
15455
  * mode, and applies a decline cooldown persisted in `localStorage`. The
14987
15456
  * resulting `method` tells the UI which install affordance to render.
14988
15457
  *
15458
+ * Both sides of that persistence are best-effort: `localStorage` throws in
15459
+ * Safari private mode and in a cross-origin frame with storage blocked, and an
15460
+ * exception raised while resolving an install CTA would take the page down with
15461
+ * it. A refused read means "no decline recorded"; a refused write means the CTA
15462
+ * may come back next visit.
15463
+ *
14989
15464
  * The decline persistence is pluggable through
14990
15465
  * {@link UseInstallPromptOptions.declineStorageKey} and
14991
15466
  * {@link UseInstallPromptOptions.declineCooldownMs} — no app-specific storage