@oxyhq/bloom 0.30.10 → 0.31.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.
@@ -5,16 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.createAlertDialog = createAlertDialog;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
- var _reactNative = require("react-native");
9
- var _button = require("../button");
10
- var _useTheme = require("../theme/use-theme.js");
11
- var _index = require("../typography/index.js");
12
- var _tokens = require("../styles/tokens.js");
13
8
  var _jsxRuntime = require("react/jsx-runtime");
14
9
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
15
- /** Centered alert-card max width (px). Matches the legacy AlertDialog card. */
16
- const ALERT_DIALOG_MAX_WIDTH = 440;
17
-
18
10
  /**
19
11
  * Build the `AlertDialog` component bound to a platform `Dialog`.
20
12
  *
@@ -24,10 +16,16 @@ const ALERT_DIALOG_MAX_WIDTH = 440;
24
16
  * (`index.ts` / `index.web.ts`) inject the correct `Dialog` here. The body
25
17
  * below is single-source, no duplication.
26
18
  *
27
- * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that lays
28
- * out a title, description and a confirm/cancel action row. Reach for the
19
+ * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that maps
20
+ * the confirm/cancel props onto the Dialog's declarative `actions` row the
21
+ * SAME battle-tested `ActionRow`/`ActionButton` primitive every other bloom
22
+ * confirm surface uses (e.g. Mention's `ConfirmPrompt`), so the button
23
+ * component, layout, and palette stay identical across apps. Reach for the
29
24
  * imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
30
25
  * from an event handler without owning visible state.
26
+ *
27
+ * The centered card uses the Dialog default `maxWidth` (480px) for full visual
28
+ * parity with the shared confirm surface.
31
29
  */
32
30
  function createAlertDialog(Dialog) {
33
31
  const AlertDialogComponent = function AlertDialog({
@@ -45,73 +43,51 @@ function createAlertDialog(Dialog) {
45
43
  cardStyle,
46
44
  testID
47
45
  }) {
48
- const theme = (0, _useTheme.useTheme)();
49
- const handleCancel = (0, _react.useCallback)(() => {
50
- onCancel?.();
51
- onClose();
52
- }, [onCancel, onClose]);
46
+ // Confirm resolves + closes SYNCHRONOUSLY, in this order, and must NOT go
47
+ // through the Dialog's auto-close (`shouldCloseOnPress: false`). In
48
+ // controlled mode `close()` fires `onClose` FIRST, and the `confirm()` host
49
+ // wires `onClose` to resolve the promise as *cancelled* (`false`). Running
50
+ // `onConfirm` (resolve `true`, which removes the queue entry) before
51
+ // `onClose` (now a no-op for that entry) is what makes the confirm button
52
+ // resolve `true`.
53
53
  const handleConfirm = (0, _react.useCallback)(() => {
54
54
  onConfirm?.();
55
55
  onClose();
56
56
  }, [onConfirm, onClose]);
57
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(Dialog, {
57
+ const actions = [{
58
+ label: confirmLabel,
59
+ onPress: handleConfirm,
60
+ color: destructive ? 'destructive' : 'default',
61
+ shouldCloseOnPress: false
62
+ }];
63
+ if (!hideCancel) {
64
+ // The `cancel` color auto-dismisses via the Dialog's `close()`, which in
65
+ // controlled mode fires `onClose` (the host resolves `false`). `onCancel`
66
+ // runs after the surface finishes closing.
67
+ actions.push({
68
+ label: cancelLabel,
69
+ onPress: onCancel,
70
+ color: 'cancel'
71
+ });
72
+ }
73
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(Dialog, {
58
74
  open: visible,
59
75
  onClose: onClose,
60
76
  placement: "center"
61
- // Alert dialogs are blocking by default — the backdrop only dismisses
62
- // when the caller opts in via `dismissible`.
77
+ // Alert dialogs are blocking by default — the backdrop / Escape only
78
+ // dismiss when the caller opts in via `dismissible`.
63
79
  ,
64
80
  dismissOnBackdrop: dismissible,
65
- maxWidth: ALERT_DIALOG_MAX_WIDTH,
66
81
  title: title,
82
+ description: description,
83
+ actions: actions,
67
84
  label: title,
68
85
  style: cardStyle,
69
- testID: testID,
70
- children: [description ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Text, {
71
- style: {
72
- fontSize: _tokens.fontSize.md,
73
- lineHeight: _tokens.fontSize.md * 1.4,
74
- color: theme.colors.textSecondary,
75
- paddingBottom: _tokens.space.lg
76
- },
77
- children: description
78
- }) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
79
- style: styles.actions,
80
- children: [!hideCancel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
81
- variant: "secondary",
82
- size: "medium",
83
- onPress: handleCancel,
84
- style: styles.action,
85
- accessibilityLabel: cancelLabel,
86
- children: cancelLabel
87
- }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
88
- variant: "primary",
89
- size: "medium",
90
- onPress: handleConfirm,
91
- style: _reactNative.StyleSheet.flatten([styles.action, destructive && {
92
- backgroundColor: theme.colors.negative
93
- }]),
94
- textStyle: destructive ? {
95
- color: theme.colors.negativeForeground
96
- } : undefined,
97
- accessibilityLabel: confirmLabel,
98
- children: confirmLabel
99
- })]
100
- })]
86
+ testID: testID
101
87
  });
102
88
  };
103
89
  const AlertDialog = /*#__PURE__*/(0, _react.memo)(AlertDialogComponent);
104
90
  AlertDialog.displayName = 'AlertDialog';
105
91
  return AlertDialog;
106
92
  }
107
- const styles = _reactNative.StyleSheet.create({
108
- actions: {
109
- flexDirection: 'row',
110
- justifyContent: 'flex-end',
111
- gap: _tokens.space.sm
112
- },
113
- action: {
114
- minWidth: 96
115
- }
116
- });
117
93
  //# sourceMappingURL=AlertDialog.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_button","_useTheme","_index","_tokens","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","ALERT_DIALOG_MAX_WIDTH","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","theme","useTheme","handleCancel","useCallback","handleConfirm","jsxs","open","placement","dismissOnBackdrop","maxWidth","label","style","children","jsx","Text","fontSize","md","lineHeight","color","colors","textSecondary","paddingBottom","space","lg","View","styles","actions","Button","variant","size","onPress","action","accessibilityLabel","StyleSheet","flatten","backgroundColor","negative","textStyle","negativeForeground","undefined","memo","displayName","create","flexDirection","justifyContent","gap","sm","minWidth"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireWildcard","require","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","handleConfirm","useCallback","actions","label","onPress","color","shouldCloseOnPress","push","jsx","open","placement","dismissOnBackdrop","style","memo","displayName"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":"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","ignoreList":[]}
@@ -1,15 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { memo, useCallback } from 'react';
4
- import { StyleSheet, View } from 'react-native';
5
- import { Button } from '../button';
6
- import { useTheme } from "../theme/use-theme.js";
7
- import { Text } from "../typography/index.js";
8
- import { fontSize, space } from "../styles/tokens.js";
9
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
- /** Centered alert-card max width (px). Matches the legacy AlertDialog card. */
11
- const ALERT_DIALOG_MAX_WIDTH = 440;
12
-
4
+ import { jsx as _jsx } from "react/jsx-runtime";
13
5
  /**
14
6
  * Build the `AlertDialog` component bound to a platform `Dialog`.
15
7
  *
@@ -19,10 +11,16 @@ const ALERT_DIALOG_MAX_WIDTH = 440;
19
11
  * (`index.ts` / `index.web.ts`) inject the correct `Dialog` here. The body
20
12
  * below is single-source, no duplication.
21
13
  *
22
- * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that lays
23
- * out a title, description and a confirm/cancel action row. Reach for the
14
+ * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that maps
15
+ * the confirm/cancel props onto the Dialog's declarative `actions` row the
16
+ * SAME battle-tested `ActionRow`/`ActionButton` primitive every other bloom
17
+ * confirm surface uses (e.g. Mention's `ConfirmPrompt`), so the button
18
+ * component, layout, and palette stay identical across apps. Reach for the
24
19
  * imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
25
20
  * from an event handler without owning visible state.
21
+ *
22
+ * The centered card uses the Dialog default `maxWidth` (480px) for full visual
23
+ * parity with the shared confirm surface.
26
24
  */
27
25
  export function createAlertDialog(Dialog) {
28
26
  const AlertDialogComponent = function AlertDialog({
@@ -40,73 +38,51 @@ export function createAlertDialog(Dialog) {
40
38
  cardStyle,
41
39
  testID
42
40
  }) {
43
- const theme = useTheme();
44
- const handleCancel = useCallback(() => {
45
- onCancel?.();
46
- onClose();
47
- }, [onCancel, onClose]);
41
+ // Confirm resolves + closes SYNCHRONOUSLY, in this order, and must NOT go
42
+ // through the Dialog's auto-close (`shouldCloseOnPress: false`). In
43
+ // controlled mode `close()` fires `onClose` FIRST, and the `confirm()` host
44
+ // wires `onClose` to resolve the promise as *cancelled* (`false`). Running
45
+ // `onConfirm` (resolve `true`, which removes the queue entry) before
46
+ // `onClose` (now a no-op for that entry) is what makes the confirm button
47
+ // resolve `true`.
48
48
  const handleConfirm = useCallback(() => {
49
49
  onConfirm?.();
50
50
  onClose();
51
51
  }, [onConfirm, onClose]);
52
- return /*#__PURE__*/_jsxs(Dialog, {
52
+ const actions = [{
53
+ label: confirmLabel,
54
+ onPress: handleConfirm,
55
+ color: destructive ? 'destructive' : 'default',
56
+ shouldCloseOnPress: false
57
+ }];
58
+ if (!hideCancel) {
59
+ // The `cancel` color auto-dismisses via the Dialog's `close()`, which in
60
+ // controlled mode fires `onClose` (the host resolves `false`). `onCancel`
61
+ // runs after the surface finishes closing.
62
+ actions.push({
63
+ label: cancelLabel,
64
+ onPress: onCancel,
65
+ color: 'cancel'
66
+ });
67
+ }
68
+ return /*#__PURE__*/_jsx(Dialog, {
53
69
  open: visible,
54
70
  onClose: onClose,
55
71
  placement: "center"
56
- // Alert dialogs are blocking by default — the backdrop only dismisses
57
- // when the caller opts in via `dismissible`.
72
+ // Alert dialogs are blocking by default — the backdrop / Escape only
73
+ // dismiss when the caller opts in via `dismissible`.
58
74
  ,
59
75
  dismissOnBackdrop: dismissible,
60
- maxWidth: ALERT_DIALOG_MAX_WIDTH,
61
76
  title: title,
77
+ description: description,
78
+ actions: actions,
62
79
  label: title,
63
80
  style: cardStyle,
64
- testID: testID,
65
- children: [description ? /*#__PURE__*/_jsx(Text, {
66
- style: {
67
- fontSize: fontSize.md,
68
- lineHeight: fontSize.md * 1.4,
69
- color: theme.colors.textSecondary,
70
- paddingBottom: space.lg
71
- },
72
- children: description
73
- }) : null, /*#__PURE__*/_jsxs(View, {
74
- style: styles.actions,
75
- children: [!hideCancel ? /*#__PURE__*/_jsx(Button, {
76
- variant: "secondary",
77
- size: "medium",
78
- onPress: handleCancel,
79
- style: styles.action,
80
- accessibilityLabel: cancelLabel,
81
- children: cancelLabel
82
- }) : null, /*#__PURE__*/_jsx(Button, {
83
- variant: "primary",
84
- size: "medium",
85
- onPress: handleConfirm,
86
- style: StyleSheet.flatten([styles.action, destructive && {
87
- backgroundColor: theme.colors.negative
88
- }]),
89
- textStyle: destructive ? {
90
- color: theme.colors.negativeForeground
91
- } : undefined,
92
- accessibilityLabel: confirmLabel,
93
- children: confirmLabel
94
- })]
95
- })]
81
+ testID: testID
96
82
  });
97
83
  };
98
84
  const AlertDialog = /*#__PURE__*/memo(AlertDialogComponent);
99
85
  AlertDialog.displayName = 'AlertDialog';
100
86
  return AlertDialog;
101
87
  }
102
- const styles = StyleSheet.create({
103
- actions: {
104
- flexDirection: 'row',
105
- justifyContent: 'flex-end',
106
- gap: space.sm
107
- },
108
- action: {
109
- minWidth: 96
110
- }
111
- });
112
88
  //# sourceMappingURL=AlertDialog.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["React","memo","useCallback","StyleSheet","View","Button","useTheme","Text","fontSize","space","jsx","_jsx","jsxs","_jsxs","ALERT_DIALOG_MAX_WIDTH","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","theme","handleCancel","handleConfirm","open","placement","dismissOnBackdrop","maxWidth","label","style","children","md","lineHeight","color","colors","textSecondary","paddingBottom","lg","styles","actions","variant","size","onPress","action","accessibilityLabel","flatten","backgroundColor","negative","textStyle","negativeForeground","undefined","displayName","create","flexDirection","justifyContent","gap","sm","minWidth"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["React","memo","useCallback","jsx","_jsx","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","handleConfirm","actions","label","onPress","color","shouldCloseOnPress","push","open","placement","dismissOnBackdrop","style","displayName"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,IAAI,EAAEC,WAAW,QAAQ,OAAO;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAOjD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,MAAuB,EAAE;EACzD,MAAMC,oBAAoB,GAAG,SAASC,WAAWA,CAAC;IAChDC,OAAO;IACPC,OAAO;IACPC,KAAK;IACLC,WAAW;IACXC,YAAY,GAAG,SAAS;IACxBC,WAAW,GAAG,QAAQ;IACtBC,SAAS;IACTC,QAAQ;IACRC,WAAW,GAAG,KAAK;IACnBC,UAAU,GAAG,KAAK;IAClBC,WAAW,GAAG,KAAK;IACnBC,SAAS;IACTC;EACgB,CAAC,EAAE;IACnB;IACA;IACA;IACA;IACA;IACA;IACA;IACA,MAAMC,aAAa,GAAGpB,WAAW,CAAC,MAAM;MACtCa,SAAS,GAAG,CAAC;MACbL,OAAO,CAAC,CAAC;IACX,CAAC,EAAE,CAACK,SAAS,EAAEL,OAAO,CAAC,CAAC;IAExB,MAAMa,OAAuB,GAAG,CAC9B;MACEC,KAAK,EAAEX,YAAY;MACnBY,OAAO,EAAEH,aAAa;MACtBI,KAAK,EAAET,WAAW,GAAG,aAAa,GAAG,SAAS;MAC9CU,kBAAkB,EAAE;IACtB,CAAC,CACF;IACD,IAAI,CAACT,UAAU,EAAE;MACf;MACA;MACA;MACAK,OAAO,CAACK,IAAI,CAAC;QACXJ,KAAK,EAAEV,WAAW;QAClBW,OAAO,EAAET,QAAQ;QACjBU,KAAK,EAAE;MACT,CAAC,CAAC;IACJ;IAEA,oBACEtB,IAAA,CAACE,MAAM;MACLuB,IAAI,EAAEpB,OAAQ;MACdC,OAAO,EAAEA,OAAQ;MACjBoB,SAAS,EAAC;MACV;MACA;MAAA;MACAC,iBAAiB,EAAEZ,WAAY;MAC/BR,KAAK,EAAEA,KAAM;MACbC,WAAW,EAAEA,WAAY;MACzBW,OAAO,EAAEA,OAAQ;MACjBC,KAAK,EAAEb,KAAM;MACbqB,KAAK,EAAEZ,SAAU;MACjBC,MAAM,EAAEA;IAAO,CAChB,CAAC;EAEN,CAAC;EAED,MAAMb,WAAW,gBAAGP,IAAI,CAACM,oBAAoB,CAAC;EAC9CC,WAAW,CAACyB,WAAW,GAAG,aAAa;EACvC,OAAOzB,WAAW;AACpB","ignoreList":[]}
@@ -11,10 +11,16 @@ type DialogComponent = React.ComponentType<DialogProps>;
11
11
  * (`index.ts` / `index.web.ts`) inject the correct `Dialog` here. The body
12
12
  * below is single-source, no duplication.
13
13
  *
14
- * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that lays
15
- * out a title, description and a confirm/cancel action row. Reach for the
14
+ * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that maps
15
+ * the confirm/cancel props onto the Dialog's declarative `actions` row the
16
+ * SAME battle-tested `ActionRow`/`ActionButton` primitive every other bloom
17
+ * confirm surface uses (e.g. Mention's `ConfirmPrompt`), so the button
18
+ * component, layout, and palette stay identical across apps. Reach for the
16
19
  * imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
17
20
  * from an event handler without owning visible state.
21
+ *
22
+ * The centered card uses the Dialog default `maxWidth` (480px) for full visual
23
+ * parity with the shared confirm surface.
18
24
  */
19
25
  export declare function createAlertDialog(Dialog: DialogComponent): React.MemoExoticComponent<({ visible, onClose, title, description, confirmLabel, cancelLabel, onConfirm, onCancel, destructive, hideCancel, dismissible, cardStyle, testID, }: AlertDialogProps) => import("react/jsx-runtime").JSX.Element>;
20
26
  export type AlertDialogType = ReturnType<typeof createAlertDialog>;
@@ -1 +1 @@
1
- {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAIjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAI7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAKxD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAwEpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
1
+ {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAqDpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
@@ -11,10 +11,16 @@ type DialogComponent = React.ComponentType<DialogProps>;
11
11
  * (`index.ts` / `index.web.ts`) inject the correct `Dialog` here. The body
12
12
  * below is single-source, no duplication.
13
13
  *
14
- * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that lays
15
- * out a title, description and a confirm/cancel action row. Reach for the
14
+ * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that maps
15
+ * the confirm/cancel props onto the Dialog's declarative `actions` row the
16
+ * SAME battle-tested `ActionRow`/`ActionButton` primitive every other bloom
17
+ * confirm surface uses (e.g. Mention's `ConfirmPrompt`), so the button
18
+ * component, layout, and palette stay identical across apps. Reach for the
16
19
  * imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
17
20
  * from an event handler without owning visible state.
21
+ *
22
+ * The centered card uses the Dialog default `maxWidth` (480px) for full visual
23
+ * parity with the shared confirm surface.
18
24
  */
19
25
  export declare function createAlertDialog(Dialog: DialogComponent): React.MemoExoticComponent<({ visible, onClose, title, description, confirmLabel, cancelLabel, onConfirm, onCancel, destructive, hideCancel, dismissible, cardStyle, testID, }: AlertDialogProps) => import("react/jsx-runtime").JSX.Element>;
20
26
  export type AlertDialogType = ReturnType<typeof createAlertDialog>;
@@ -1 +1 @@
1
- {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAIjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAI7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAKxD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAwEpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
1
+ {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAqDpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.30.10",
3
+ "version": "0.31.0",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -1,7 +1,8 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import { act, fireEvent, render } from '@testing-library/react-native';
3
3
 
4
- import { AlertDialog } from '../alert-dialog';
4
+ import { AlertDialog, AlertDialogHost, confirm } from '../alert-dialog';
5
+ import { getConfirmQueue, resolveConfirm } from '../alert-dialog/confirm-store';
5
6
  import { BloomThemeProvider } from '../theme/BloomThemeProvider';
6
7
 
7
8
  function renderWithTheme(ui: React.ReactElement) {
@@ -12,11 +13,23 @@ function renderWithTheme(ui: React.ReactElement) {
12
13
  );
13
14
  }
14
15
 
15
- // `AlertDialog` is now a thin wrapper over `<Dialog placement="center">`. These
16
- // tests assert the public-API contract it owns (controlled visibility, the
17
- // title/description copy, the confirm/cancel action row, and the
18
- // confirm/cancel/dismiss callbacks) holds on the native Dialog surface.
16
+ // `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that maps
17
+ // its confirm/cancel props onto the Dialog's declarative `actions` row (the
18
+ // shared `ActionRow`/`ActionButton` primitive). These tests assert the
19
+ // public-API contract it owns controlled visibility, the title/description
20
+ // copy, the confirm/cancel actions, and — most importantly — the exact
21
+ // `confirm()` promise-resolution contract every ecosystem call site relies on:
22
+ // the confirm button resolves `true`; the cancel button (or Escape / backdrop,
23
+ // which route through the Dialog's `onClose`) resolves `false`.
19
24
  describe('AlertDialog (built on Dialog)', () => {
25
+ afterEach(() => {
26
+ // Drain any confirm() entries a test left pending so the module-scoped
27
+ // queue never bleeds across tests.
28
+ for (const entry of [...getConfirmQueue()]) {
29
+ resolveConfirm(entry.id, false);
30
+ }
31
+ });
32
+
20
33
  it('renders nothing when not visible', () => {
21
34
  const { queryByText } = renderWithTheme(
22
35
  <AlertDialog
@@ -48,8 +61,9 @@ describe('AlertDialog (built on Dialog)', () => {
48
61
  });
49
62
 
50
63
  it('runs onConfirm then onClose when the confirm button is pressed', () => {
51
- const onConfirm = jest.fn();
52
- const onClose = jest.fn();
64
+ const calls: string[] = [];
65
+ const onConfirm = jest.fn(() => calls.push('confirm'));
66
+ const onClose = jest.fn(() => calls.push('close'));
53
67
  const { getByText } = renderWithTheme(
54
68
  <AlertDialog
55
69
  visible
@@ -64,10 +78,11 @@ describe('AlertDialog (built on Dialog)', () => {
64
78
  });
65
79
  expect(onConfirm).toHaveBeenCalledTimes(1);
66
80
  expect(onClose).toHaveBeenCalledTimes(1);
81
+ // Order matters: onConfirm must resolve the promise BEFORE onClose runs.
82
+ expect(calls).toEqual(['confirm', 'close']);
67
83
  });
68
84
 
69
- it('runs onCancel then onClose when the cancel button is pressed', () => {
70
- const onCancel = jest.fn();
85
+ it('requests close (resolves cancel) when the cancel button is pressed', () => {
71
86
  const onClose = jest.fn();
72
87
  const { getByText } = renderWithTheme(
73
88
  <AlertDialog
@@ -75,16 +90,37 @@ describe('AlertDialog (built on Dialog)', () => {
75
90
  onClose={onClose}
76
91
  title="Confirm?"
77
92
  cancelLabel="No"
78
- onCancel={onCancel}
79
93
  />,
80
94
  );
81
95
  act(() => {
82
96
  fireEvent.press(getByText('No'));
83
97
  });
84
- expect(onCancel).toHaveBeenCalledTimes(1);
98
+ // The `cancel` action dismisses via the Dialog's `close()`, which in
99
+ // controlled mode fires `onClose` — the confirm() host's resolve-false.
85
100
  expect(onClose).toHaveBeenCalledTimes(1);
86
101
  });
87
102
 
103
+ it('invokes onCancel after the cancel button dismisses the dialog', () => {
104
+ const onCancel = jest.fn();
105
+ function Harness() {
106
+ const [open, setOpen] = useState(true);
107
+ return (
108
+ <AlertDialog
109
+ visible={open}
110
+ onClose={() => setOpen(false)}
111
+ title="Confirm?"
112
+ cancelLabel="No"
113
+ onCancel={onCancel}
114
+ />
115
+ );
116
+ }
117
+ const { getByText } = renderWithTheme(<Harness />);
118
+ act(() => {
119
+ fireEvent.press(getByText('No'));
120
+ });
121
+ expect(onCancel).toHaveBeenCalledTimes(1);
122
+ });
123
+
88
124
  it('hides the cancel button when hideCancel is set', () => {
89
125
  const { queryByText, getByText } = renderWithTheme(
90
126
  <AlertDialog
@@ -112,11 +148,65 @@ describe('AlertDialog (built on Dialog)', () => {
112
148
  onConfirm={onConfirm}
113
149
  />,
114
150
  );
115
- const confirm = getByText('Sign out');
116
- expect(confirm).toBeTruthy();
151
+ const confirmButton = getByText('Sign out');
152
+ expect(confirmButton).toBeTruthy();
117
153
  act(() => {
118
- fireEvent.press(confirm);
154
+ fireEvent.press(confirmButton);
119
155
  });
120
156
  expect(onConfirm).toHaveBeenCalledTimes(1);
121
157
  });
158
+
159
+ // The authoritative ecosystem contract: `confirm()` + `<AlertDialogHost />`.
160
+ // Every consuming call site does `const ok = await confirm({...})`, so these
161
+ // assert the exact promise resolution against the REAL rendered action row.
162
+ describe('confirm() + AlertDialogHost promise contract', () => {
163
+ it('resolves true when the confirm button is pressed', async () => {
164
+ const { getByText } = renderWithTheme(<AlertDialogHost />);
165
+ let result!: Promise<boolean>;
166
+ act(() => {
167
+ result = confirm({
168
+ title: 'Delete app?',
169
+ confirmLabel: 'Delete',
170
+ cancelLabel: 'Keep',
171
+ });
172
+ });
173
+ act(() => {
174
+ fireEvent.press(getByText('Delete'));
175
+ });
176
+ await expect(result).resolves.toBe(true);
177
+ expect(getConfirmQueue()).toHaveLength(0);
178
+ });
179
+
180
+ it('resolves false when the cancel button is pressed', async () => {
181
+ const { getByText } = renderWithTheme(<AlertDialogHost />);
182
+ let result!: Promise<boolean>;
183
+ act(() => {
184
+ result = confirm({
185
+ title: 'Discard changes?',
186
+ confirmLabel: 'Discard',
187
+ cancelLabel: 'Keep editing',
188
+ });
189
+ });
190
+ act(() => {
191
+ fireEvent.press(getByText('Keep editing'));
192
+ });
193
+ await expect(result).resolves.toBe(false);
194
+ expect(getConfirmQueue()).toHaveLength(0);
195
+ });
196
+
197
+ it('resolves true even though confirm fires both onConfirm and onClose', async () => {
198
+ // The confirm button runs onConfirm (resolve true, drains the entry) then
199
+ // onClose (resolve false — a no-op once the entry is gone). The net must
200
+ // be `true`; this guards the double-resolution path explicitly.
201
+ const { getByText } = renderWithTheme(<AlertDialogHost />);
202
+ let result!: Promise<boolean>;
203
+ act(() => {
204
+ result = confirm({ title: 'Proceed?', confirmLabel: 'Proceed' });
205
+ });
206
+ act(() => {
207
+ fireEvent.press(getByText('Proceed'));
208
+ });
209
+ await expect(result).resolves.toBe(true);
210
+ });
211
+ });
122
212
  });
@@ -42,6 +42,22 @@ describe('confirm-store', () => {
42
42
  await expect(promise).resolves.toBe(false);
43
43
  });
44
44
 
45
+ it('resolves once — a second resolve for the same id is a no-op', async () => {
46
+ // The AlertDialog confirm path calls onConfirm (resolve true) THEN onClose
47
+ // (resolve false) for the SAME entry. The first resolve drains the entry,
48
+ // so the second is ignored and the promise keeps its `true` value. This is
49
+ // the guard the whole confirm() button contract relies on.
50
+ const promise = confirm({ title: 'Proceed?' });
51
+ const id = getConfirmQueue()[0]?.id;
52
+ expect(id).toBeTruthy();
53
+ if (id) {
54
+ resolveConfirm(id, true);
55
+ resolveConfirm(id, false);
56
+ }
57
+ await expect(promise).resolves.toBe(true);
58
+ expect(getConfirmQueue()).toHaveLength(0);
59
+ });
60
+
45
61
  it('drains queued entries to a late subscriber', () => {
46
62
  void confirm({ title: 'Queued before subscribe' });
47
63
  let delivered: number | null = null;
@@ -1,18 +1,10 @@
1
1
  import React, { memo, useCallback } from 'react';
2
- import { StyleSheet, View } from 'react-native';
3
2
 
4
- import { Button } from '../button';
5
- import type { DialogProps } from '../dialog';
6
- import { useTheme } from '../theme/use-theme';
7
- import { Text } from '../typography';
8
- import { fontSize, space } from '../styles/tokens';
3
+ import type { DialogAction, DialogProps } from '../dialog';
9
4
  import type { AlertDialogProps } from './types';
10
5
 
11
6
  type DialogComponent = React.ComponentType<DialogProps>;
12
7
 
13
- /** Centered alert-card max width (px). Matches the legacy AlertDialog card. */
14
- const ALERT_DIALOG_MAX_WIDTH = 440;
15
-
16
8
  /**
17
9
  * Build the `AlertDialog` component bound to a platform `Dialog`.
18
10
  *
@@ -22,10 +14,16 @@ const ALERT_DIALOG_MAX_WIDTH = 440;
22
14
  * (`index.ts` / `index.web.ts`) inject the correct `Dialog` here. The body
23
15
  * below is single-source, no duplication.
24
16
  *
25
- * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that lays
26
- * out a title, description and a confirm/cancel action row. Reach for the
17
+ * `AlertDialog` is a thin wrapper over `<Dialog placement="center">` that maps
18
+ * the confirm/cancel props onto the Dialog's declarative `actions` row the
19
+ * SAME battle-tested `ActionRow`/`ActionButton` primitive every other bloom
20
+ * confirm surface uses (e.g. Mention's `ConfirmPrompt`), so the button
21
+ * component, layout, and palette stay identical across apps. Reach for the
27
22
  * imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
28
23
  * from an event handler without owning visible state.
24
+ *
25
+ * The centered card uses the Dialog default `maxWidth` (480px) for full visual
26
+ * parity with the shared confirm surface.
29
27
  */
30
28
  export function createAlertDialog(Dialog: DialogComponent) {
31
29
  const AlertDialogComponent = function AlertDialog({
@@ -43,71 +41,52 @@ export function createAlertDialog(Dialog: DialogComponent) {
43
41
  cardStyle,
44
42
  testID,
45
43
  }: AlertDialogProps) {
46
- const theme = useTheme();
47
-
48
- const handleCancel = useCallback(() => {
49
- onCancel?.();
50
- onClose();
51
- }, [onCancel, onClose]);
52
-
44
+ // Confirm resolves + closes SYNCHRONOUSLY, in this order, and must NOT go
45
+ // through the Dialog's auto-close (`shouldCloseOnPress: false`). In
46
+ // controlled mode `close()` fires `onClose` FIRST, and the `confirm()` host
47
+ // wires `onClose` to resolve the promise as *cancelled* (`false`). Running
48
+ // `onConfirm` (resolve `true`, which removes the queue entry) before
49
+ // `onClose` (now a no-op for that entry) is what makes the confirm button
50
+ // resolve `true`.
53
51
  const handleConfirm = useCallback(() => {
54
52
  onConfirm?.();
55
53
  onClose();
56
54
  }, [onConfirm, onClose]);
57
55
 
56
+ const actions: DialogAction[] = [
57
+ {
58
+ label: confirmLabel,
59
+ onPress: handleConfirm,
60
+ color: destructive ? 'destructive' : 'default',
61
+ shouldCloseOnPress: false,
62
+ },
63
+ ];
64
+ if (!hideCancel) {
65
+ // The `cancel` color auto-dismisses via the Dialog's `close()`, which in
66
+ // controlled mode fires `onClose` (the host resolves `false`). `onCancel`
67
+ // runs after the surface finishes closing.
68
+ actions.push({
69
+ label: cancelLabel,
70
+ onPress: onCancel,
71
+ color: 'cancel',
72
+ });
73
+ }
74
+
58
75
  return (
59
76
  <Dialog
60
77
  open={visible}
61
78
  onClose={onClose}
62
79
  placement="center"
63
- // Alert dialogs are blocking by default — the backdrop only dismisses
64
- // when the caller opts in via `dismissible`.
80
+ // Alert dialogs are blocking by default — the backdrop / Escape only
81
+ // dismiss when the caller opts in via `dismissible`.
65
82
  dismissOnBackdrop={dismissible}
66
- maxWidth={ALERT_DIALOG_MAX_WIDTH}
67
83
  title={title}
84
+ description={description}
85
+ actions={actions}
68
86
  label={title}
69
87
  style={cardStyle}
70
88
  testID={testID}
71
- >
72
- {description ? (
73
- <Text
74
- style={{
75
- fontSize: fontSize.md,
76
- lineHeight: fontSize.md * 1.4,
77
- color: theme.colors.textSecondary,
78
- paddingBottom: space.lg,
79
- }}
80
- >
81
- {description}
82
- </Text>
83
- ) : null}
84
- <View style={styles.actions}>
85
- {!hideCancel ? (
86
- <Button
87
- variant="secondary"
88
- size="medium"
89
- onPress={handleCancel}
90
- style={styles.action}
91
- accessibilityLabel={cancelLabel}
92
- >
93
- {cancelLabel}
94
- </Button>
95
- ) : null}
96
- <Button
97
- variant="primary"
98
- size="medium"
99
- onPress={handleConfirm}
100
- style={StyleSheet.flatten([
101
- styles.action,
102
- destructive && { backgroundColor: theme.colors.negative },
103
- ])}
104
- textStyle={destructive ? { color: theme.colors.negativeForeground } : undefined}
105
- accessibilityLabel={confirmLabel}
106
- >
107
- {confirmLabel}
108
- </Button>
109
- </View>
110
- </Dialog>
89
+ />
111
90
  );
112
91
  };
113
92
 
@@ -117,14 +96,3 @@ export function createAlertDialog(Dialog: DialogComponent) {
117
96
  }
118
97
 
119
98
  export type AlertDialogType = ReturnType<typeof createAlertDialog>;
120
-
121
- const styles = StyleSheet.create({
122
- actions: {
123
- flexDirection: 'row',
124
- justifyContent: 'flex-end',
125
- gap: space.sm,
126
- },
127
- action: {
128
- minWidth: 96,
129
- },
130
- });