@unif/react-native-design 0.24.0 → 0.25.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 (37) hide show
  1. package/jest-preset.js +9 -1
  2. package/lib/module/components/ui/Confirm/ConfirmHost.js +7 -12
  3. package/lib/module/components/ui/Confirm/ConfirmHost.js.map +1 -1
  4. package/lib/module/components/ui/Confirm/confirm.js +3 -2
  5. package/lib/module/components/ui/Confirm/confirm.js.map +1 -1
  6. package/lib/module/components/ui/Confirm/store.js +58 -8
  7. package/lib/module/components/ui/Confirm/store.js.map +1 -1
  8. package/lib/module/components/ui/Toast/ToastHost.js +20 -2
  9. package/lib/module/components/ui/Toast/ToastHost.js.map +1 -1
  10. package/lib/module/components/ui/Toast/ToastHost.web.js +11 -1
  11. package/lib/module/components/ui/Toast/ToastHost.web.js.map +1 -1
  12. package/lib/module/components/ui/Toast/store.js +95 -12
  13. package/lib/module/components/ui/Toast/store.js.map +1 -1
  14. package/lib/module/components/ui/Toast/toast.js +3 -2
  15. package/lib/module/components/ui/Toast/toast.js.map +1 -1
  16. package/lib/typescript/src/components/ui/Confirm/ConfirmHost.d.ts +7 -5
  17. package/lib/typescript/src/components/ui/Confirm/ConfirmHost.d.ts.map +1 -1
  18. package/lib/typescript/src/components/ui/Confirm/confirm.d.ts +4 -3
  19. package/lib/typescript/src/components/ui/Confirm/confirm.d.ts.map +1 -1
  20. package/lib/typescript/src/components/ui/Confirm/store.d.ts +13 -4
  21. package/lib/typescript/src/components/ui/Confirm/store.d.ts.map +1 -1
  22. package/lib/typescript/src/components/ui/Toast/ToastHost.d.ts +5 -1
  23. package/lib/typescript/src/components/ui/Toast/ToastHost.d.ts.map +1 -1
  24. package/lib/typescript/src/components/ui/Toast/ToastHost.web.d.ts +1 -0
  25. package/lib/typescript/src/components/ui/Toast/ToastHost.web.d.ts.map +1 -1
  26. package/lib/typescript/src/components/ui/Toast/store.d.ts +25 -1
  27. package/lib/typescript/src/components/ui/Toast/store.d.ts.map +1 -1
  28. package/lib/typescript/src/components/ui/Toast/toast.d.ts +4 -3
  29. package/lib/typescript/src/components/ui/Toast/toast.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/ui/Confirm/ConfirmHost.tsx +8 -14
  32. package/src/components/ui/Confirm/confirm.ts +3 -2
  33. package/src/components/ui/Confirm/store.ts +61 -13
  34. package/src/components/ui/Toast/ToastHost.tsx +20 -2
  35. package/src/components/ui/Toast/ToastHost.web.tsx +11 -1
  36. package/src/components/ui/Toast/store.ts +112 -16
  37. package/src/components/ui/Toast/toast.ts +3 -2
@@ -1,13 +1,15 @@
1
1
  import React from 'react';
2
2
  /**
3
- * Confirm 对话框宿主 —— App 根挂**一次**,监听 `confirm()` 调用并渲染。
3
+ * Confirm 对话框宿主 —— App 根挂一个,监听 `confirm()` 调用并渲染。
4
4
  *
5
5
  * 原生 RN `Modal`(transparent + animationType slide)从底部滑入,backdrop 点击
6
6
  * 取消、内层 sheet 拦截点击不冒泡。**不依赖 @gorhom**。
7
7
  *
8
- * 唯一 owner:重复挂载的实例拿到 `null` lease,永久惰性、不渲染 —— 不会出现两个
9
- * Host 争抢同一个对话框。所有关闭路径统一走 `settleConfirm(entry, result)`,
10
- * 组件内不直接调用 entry resolver。
8
+ * 栈式 owner:后挂载的实例接管事件,前任入栈挂起并收到 `clear`(对话框就地关闭、
9
+ * Promise resolve(false));接管者卸载后自动归还。因此**允许**在自己的 `Modal` 里再挂
10
+ * 一个 —— RN `Modal` 是独立 native window,根 Host 的对话框会被它物理盖住,内层
11
+ * 自挂才能显示。所有关闭路径统一走 `settleConfirm(entry, result)`,组件内不直接调用
12
+ * entry 的 resolver。
11
13
  */
12
- export declare function ConfirmHost(): React.JSX.Element | null;
14
+ export declare function ConfirmHost(): React.JSX.Element;
13
15
  //# sourceMappingURL=ConfirmHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfirmHost.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Confirm/ConfirmHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAsBxE;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAwItD"}
1
+ {"version":3,"file":"ConfirmHost.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Confirm/ConfirmHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAsBxE;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAgI/C"}
@@ -3,7 +3,8 @@ import type { ConfirmOptions } from './types';
3
3
  * 弹出确认对话框,返回 Promise<boolean>。
4
4
  * - 用户点确认 → resolve(true)
5
5
  * - 用户点取消 / 点 backdrop / 系统返回 → resolve(false)
6
- * - 未挂 `<ConfirmHost />`、已有对话框在显示、Host 渲染抛错、Host 卸载 → resolve(false)
6
+ * - 未挂 `<ConfirmHost />`、已有对话框在显示、Host 渲染抛错、Host 卸载、
7
+ * 被后挂载的 Host 接管 → resolve(false)
7
8
  *
8
9
  * 任何路径都必然 settle,不会悬挂。
9
10
  *
@@ -19,8 +20,8 @@ import type { ConfirmOptions } from './types';
19
20
  * ```
20
21
  */
21
22
  export declare const confirm: (options: ConfirmOptions) => Promise<boolean>;
22
- /** 内部:ConfirmHost 挂载时注册唯一 owner;重复挂载得到 null。 */
23
- export declare const registerConfirmHost: (subscriber: import("./store").ConfirmSubscriber) => import("./store").ConfirmHostLease | null;
23
+ /** 内部:ConfirmHost 挂载时接管 owner;前任入栈挂起,本 lease 释放时归还。 */
24
+ export declare const registerConfirmHost: (subscriber: import("./store").ConfirmSubscriber) => import("./store").ConfirmHostLease;
24
25
  /** 内部:所有关闭路径的唯一出口(identity-guarded + 幂等)。 */
25
26
  export declare const settleConfirm: (entry: import("./store").ConfirmEntry, result: boolean) => boolean;
26
27
  //# sourceMappingURL=confirm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Confirm/confirm.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAM9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,cAAc,KAAG,OAAO,CAAC,OAAO,CACzC,CAAC;AAEzB,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,gGAAqB,CAAC;AAEtD,6CAA6C;AAC7C,eAAO,MAAM,aAAa,qEAAe,CAAC"}
1
+ {"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Confirm/confirm.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAM9C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,cAAc,KAAG,OAAO,CAAC,OAAO,CACzC,CAAC;AAEzB,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,yFAAqB,CAAC;AAEtD,6CAA6C;AAC7C,eAAO,MAAM,aAAa,qEAAe,CAAC"}
@@ -4,10 +4,17 @@ import type { ConfirmOptions } from './types';
4
4
  * Confirm 的纯状态机 —— 不含任何 React / 渲染依赖,可直接单测。
5
5
  *
6
6
  * 两条不变量:
7
- * 1. **唯一 owner** —— 同一时间只有一个 `<ConfirmHost />` 持有订阅。重复挂载拿到 `null`
8
- * lease、永久惰性,不会出现两个 Host 争抢同一个 entry。
7
+ * 1. **单一活跃 owner + 栈式接管** —— 同一时间只有一个 `<ConfirmHost />` 收事件,但
8
+ * 后挂载者**接管**、前任入栈挂起,卸载时自动归还。RN `Modal` 是独立 native window,
9
+ * 根 Host 渲染的对话框会被 Modal 物理盖住(iOS 兄弟 Modal 更是不叠放),所以
10
+ * 「Modal 内自挂一个 Host」是唯一正解、必须是合法用法 —— 旧的 first-wins 把它判成
11
+ * 恒惰性死码,才是要修的东西。
9
12
  * 2. **唯一 active entry** —— 同一时间只有一个未决对话框。所有关闭路径(确认 / 取消 /
10
- * backdrop / 系统返回 / Host 卸载 / subscriber 抛错)都汇聚到 `settle()`。
13
+ * backdrop / 系统返回 / Host 卸载 / owner 切换 / subscriber 抛错)都汇聚到 `settle()`。
14
+ *
15
+ * 由 1 派生出一条关键不变量:**active entry 永远属于当前 owner**。owner 每次切换
16
+ * (接管或归还)前都把 active 排空为 `false`,因此挂起中的 owner 手里不可能有未决
17
+ * 对话框,恢复时也不会继承一个自己没见过 `show` 的槽位。
11
18
  *
12
19
  * `settle()` 用 **identity guard**(`active !== entry` 直接返回 false)而非 id 比较:
13
20
  * 旧 entry 的迟到回调拿着的是旧对象引用,永远匹配不上新 active,因此不可能误关新对话框。
@@ -31,12 +38,14 @@ export type ConfirmHostLease = {
31
38
  /**
32
39
  * Host 卸载。`heldEntry` 是 Host 当时手里那个未决 entry(没有则传 `null`)——
33
40
  * 只有它仍是 active 时才会被 settle 成 `false`,避免误伤后来的对话框。
41
+ * 幂等:重复调用、或本 owner 早已被接管者顶下去,都是无副作用的 no-op。
34
42
  */
35
43
  release(heldEntry: ConfirmEntry | null): void;
36
44
  };
37
45
  export type ConfirmStore = {
38
46
  request(options: ConfirmOptions): Promise<boolean>;
39
- registerHost(subscriber: ConfirmSubscriber): ConfirmHostLease | null;
47
+ /** 挂载 Host。恒返回可用 lease —— 已有 owner 时走接管,不再拒绝。 */
48
+ registerHost(subscriber: ConfirmSubscriber): ConfirmHostLease;
40
49
  settle(entry: ConfirmEntry, result: boolean): boolean;
41
50
  activeEntry(): ConfirmEntry | null;
42
51
  };
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Confirm/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,YAAY,CAAC,UAAU,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,CAAC;IACrE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IACtD,WAAW,IAAI,YAAY,GAAG,IAAI,CAAC;CACpC,CAAC;AAIF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAmG5D"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Confirm/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,iDAAiD;IACjD,YAAY,CAAC,UAAU,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAC9D,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IACtD,WAAW,IAAI,YAAY,GAAG,IAAI,CAAC;CACpC,CAAC;AAIF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CA0I5D"}
@@ -1,9 +1,13 @@
1
1
  import React from 'react';
2
2
  import type { ToastHostProps } from './types';
3
3
  /**
4
- * 在根附近挂**一次**。监听 toast() 调用并渲染当前 toast。
4
+ * 在根附近挂一个。监听 toast() 调用并渲染当前 toast。
5
5
  * 同一时间只显示一条 —— 新的会替换旧的(latest-wins)。
6
6
  *
7
+ * 栈式 owner:后挂载的实例接管投递,前任挂起并收起当前 toast;接管者卸载后自动归还。
8
+ * 因此**允许**在自己的 `Modal` 里再挂一个 —— RN `Modal` 是独立 native window,根 Host
9
+ * 的 toast 会被它物理盖住,内层自挂才看得见。
10
+ *
7
11
  * 位置由 `entry.position`('top' / 'bottom' / 'center')决定,top/bottom 自动避让
8
12
  * safe-area;进入动画方向随位置(top 从上滑、bottom/center 从下滑)。
9
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ToastHost.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/ToastHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAmBxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAS9C;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,EACxB,MAAM,GACP,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CA+HhD"}
1
+ {"version":3,"file":"ToastHost.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/ToastHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAmBxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAS9C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,MAAM,GACP,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CA6IhD"}
@@ -9,6 +9,7 @@ import type { ToastHostProps } from './types';
9
9
  * - opacity 0→1 + translateY 8→0 入场(motion.base ms)
10
10
  * - 停留 entry.duration ms
11
11
  * - opacity 1→0 + translateY 0→8 退场后 unmount
12
+ * - 栈式 owner:后挂载者接管、前任挂起并收起 toast,接管者卸载后自动归还
12
13
  *
13
14
  * 竞态纪律与 native 版一致:timer / RAF 在改 UI 或上报完成前,都要同时通过同步 ref
14
15
  * 与 `isCurrentToastDelivery(delivery)` 两道校验。
@@ -1 +1 @@
1
- {"version":3,"file":"ToastHost.web.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/ToastHost.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAkBxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAS9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,EACxB,MAAM,GACP,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAiJhD"}
1
+ {"version":3,"file":"ToastHost.web.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/ToastHost.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAkBxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAS9C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,MAAM,GACP,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CA0JhD"}
@@ -16,6 +16,18 @@ import type { ToastEntry } from './types';
16
16
  *
17
17
  * `leaseId` 单调递增,同一 entry 被重新投递(Host 重挂)也会拿到新的 leaseId ——
18
18
  * 仅比较 entry.id 不足以区分「同一条消息的两次投递」。
19
+ *
20
+ * **栈式 owner**(与 Confirm 同构):后挂载的 Host 接管投递,前任入栈挂起并收到 clear,
21
+ * 接管者卸载后自动归还。RN `Modal` 是独立 native window,根 Host 渲染的 toast 会被
22
+ * Modal 物理盖住,只有「Modal 内自挂一个 Host」能显示 —— 旧的 first-wins 把这条唯一
23
+ * 正解判成恒惰性死码。
24
+ *
25
+ * 两个方向对在途 toast 的处理**刻意不同**:
26
+ * - **接管**(有人挂到我上面)→ 在途 toast **丢弃**。它属于「已经被盖住的过去」,
27
+ * 搬到接管者身上重放没有意义。
28
+ * - **归还**(接管者卸载、我恢复)→ 在途 toast **整条交回、立即重投**。
29
+ * 「Modal 内操作成功 → toast → 关窗」是最常见的一条路径,那条 toast 刚发出、用户
30
+ * 一眼都还没看到;丢掉的话比不挂内层 Host 还糟。不追剩余时长,重投 = 完整重播一遍。
19
31
  */
20
32
  export type ToastDelivery = {
21
33
  entry: ToastEntry;
@@ -23,13 +35,25 @@ export type ToastDelivery = {
23
35
  leaseId: number;
24
36
  };
25
37
  export type ToastSubscriber = (delivery: ToastDelivery) => void;
38
+ /**
39
+ * owner 被接管 / 归还时的撤回通知 —— 对应 Confirm 的 `clear` 事件。
40
+ *
41
+ * 没有它,挂起中的 Host 会把最后一条 toast **冻在屏幕上**:store 已丢弃那次投递,
42
+ * Host 自己的退场 timer 过不了 `isCurrent` CAS,永远走不到清 UI 那一步。
43
+ */
44
+ export type ToastClearSubscriber = () => void;
26
45
  export type ToastHostLease = {
27
46
  readonly ownerToken: symbol;
47
+ /** Host 卸载。幂等:重复调用、或本 owner 已被摘掉,都是无副作用的 no-op。 */
28
48
  release(): void;
29
49
  };
30
50
  export type ToastStore = {
31
51
  publish(entry: ToastEntry): void;
32
- registerHost(subscriber: ToastSubscriber): ToastHostLease | null;
52
+ /**
53
+ * 挂载 Host。已有 owner 时走接管(不再拒绝);返回 `null` 只剩一种含义 ——
54
+ * 补投 pending 时 subscriber 抛错,这个 owner 当场就被作废了。
55
+ */
56
+ registerHost(subscriber: ToastSubscriber, onClear?: ToastClearSubscriber): ToastHostLease | null;
33
57
  complete(ownerToken: symbol, leaseId: number, entryId: number): boolean;
34
58
  isCurrent(delivery: ToastDelivery): boolean;
35
59
  pendingEntry(): ToastEntry | null;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,UAAU,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;AAEhE,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,YAAY,CAAC,UAAU,EAAE,eAAe,GAAG,cAAc,GAAG,IAAI,CAAC;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACxE,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC;IAC5C,YAAY,IAAI,UAAU,GAAG,IAAI,CAAC;IAClC,eAAe,IAAI,aAAa,GAAG,IAAI,CAAC;CACzC,CAAC;AAIF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAsGxD"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,UAAU,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,IAAI,CAAC;AAE9C,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,mDAAmD;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC;;;OAGG;IACH,YAAY,CACV,UAAU,EAAE,eAAe,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACxE,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC;IAC5C,YAAY,IAAI,UAAU,GAAG,IAAI,CAAC;IAClC,eAAe,IAAI,aAAa,GAAG,IAAI,CAAC;CACzC,CAAC;AAQF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAsKxD"}
@@ -1,6 +1,7 @@
1
1
  import type { ToastInput } from './types';
2
2
  /**
3
- * 命令式 toast API。在 app 根附近挂一次 `<ToastHost />` 即可。
3
+ * 命令式 toast API。在 app 根附近挂一个 `<ToastHost />` 即可;需要在 `Modal` 里显示
4
+ * 时可在 Modal 内再挂一个,它会接管、关闭后自动归还。
4
5
  *
5
6
  * toast('已切换到日报');
6
7
  * toast.success('订单提交成功');
@@ -12,8 +13,8 @@ export declare const toast: ((input: ToastInput) => void) & {
12
13
  success: (input: ToastInput) => void;
13
14
  error: (input: ToastInput) => void;
14
15
  };
15
- /** 内部:ToastHost 挂载时注册唯一 owner;重复挂载得到 null。 */
16
- export declare const registerToastHost: (subscriber: import("./store").ToastSubscriber) => import("./store").ToastHostLease | null;
16
+ /** 内部:ToastHost 挂载时接管 owner;前任入栈挂起,本 lease 释放时归还。 */
17
+ export declare const registerToastHost: (subscriber: import("./store").ToastSubscriber, onClear?: import("./store").ToastClearSubscriber) => import("./store").ToastHostLease | null;
17
18
  /** 内部:三重身份 CAS —— owner token + leaseId + entry id 全对才算完成。 */
18
19
  export declare const completeToast: (ownerToken: symbol, leaseId: number, entryId: number) => boolean;
19
20
  /** 内部:timer / RAF / 动画回调动 UI 前的守卫。 */
@@ -1 +1 @@
1
- {"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/toast.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAc,UAAU,EAAa,MAAM,SAAS,CAAC;AA+CjE;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,WAAyB,UAAU;kBACrC,UAAU;qBACP,UAAU;mBACZ,UAAU;CACzB,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,iBAAiB,4FAAqB,CAAC;AAEpD,8DAA8D;AAC9D,eAAO,MAAM,aAAa,mEAAiB,CAAC;AAE5C,sCAAsC;AACtC,eAAO,MAAM,sBAAsB,wDAAkB,CAAC"}
1
+ {"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/toast.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAc,UAAU,EAAa,MAAM,SAAS,CAAC;AA+CjE;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,WAAyB,UAAU;kBACrC,UAAU;qBACP,UAAU;mBACZ,UAAU;CACzB,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,iBAAiB,8IAAqB,CAAC;AAEpD,8DAA8D;AAC9D,eAAO,MAAM,aAAa,mEAAiB,CAAC;AAE5C,sCAAsC;AACtC,eAAO,MAAM,sBAAsB,wDAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unif/react-native-design",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Unif 设计系统:theme + icons + utils + components",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -21,16 +21,18 @@ const SCRIM_IN_MS = 220;
21
21
  const SCRIM_OUT_MS = 260;
22
22
 
23
23
  /**
24
- * Confirm 对话框宿主 —— App 根挂**一次**,监听 `confirm()` 调用并渲染。
24
+ * Confirm 对话框宿主 —— App 根挂一个,监听 `confirm()` 调用并渲染。
25
25
  *
26
26
  * 原生 RN `Modal`(transparent + animationType slide)从底部滑入,backdrop 点击
27
27
  * 取消、内层 sheet 拦截点击不冒泡。**不依赖 @gorhom**。
28
28
  *
29
- * 唯一 owner:重复挂载的实例拿到 `null` lease,永久惰性、不渲染 —— 不会出现两个
30
- * Host 争抢同一个对话框。所有关闭路径统一走 `settleConfirm(entry, result)`,
31
- * 组件内不直接调用 entry resolver。
29
+ * 栈式 owner:后挂载的实例接管事件,前任入栈挂起并收到 `clear`(对话框就地关闭、
30
+ * Promise resolve(false));接管者卸载后自动归还。因此**允许**在自己的 `Modal` 里再挂
31
+ * 一个 —— RN `Modal` 是独立 native window,根 Host 的对话框会被它物理盖住,内层
32
+ * 自挂才能显示。所有关闭路径统一走 `settleConfirm(entry, result)`,组件内不直接调用
33
+ * entry 的 resolver。
32
34
  */
33
- export function ConfirmHost(): React.JSX.Element | null {
35
+ export function ConfirmHost(): React.JSX.Element {
34
36
  const styles = useThemedStyles(makeStyles);
35
37
  const insets = useSafeAreaInsets();
36
38
  const { height: screenH } = useWindowDimensions();
@@ -39,9 +41,6 @@ export function ConfirmHost(): React.JSX.Element | null {
39
41
  // 同步权威副本:effect cleanup 与迟到回调都读它,不受 React state 批处理延迟影响。
40
42
  const pendingRef = useRef<ConfirmEntry | null>(null);
41
43
 
42
- // 注册失败(已有 owner)时保持惰性:不订阅、不渲染。
43
- const [inert, setInert] = useState(false);
44
-
45
44
  // 保留最后一次显示的 entry:关闭时 entry 立即置 null,但 slide 退场动画期 Modal 仍可见,
46
45
  // 渲染 lastEntry 才不会滑出一个只剩 padding 的空 sheet([M-17])。
47
46
  const lastEntryRef = useRef<ConfirmEntry | null>(null);
@@ -66,13 +65,10 @@ export function ConfirmHost(): React.JSX.Element | null {
66
65
  }
67
66
  };
68
67
  const lease = registerConfirmHost(subscriber);
69
- if (!lease) {
70
- setInert(true);
71
- return;
72
- }
73
68
  return () => {
74
69
  // 卸载时若对话框仍未决(error boundary 重置 / 根 re-key 切语言主题等),
75
70
  // Store 把它 settle(false) —— 否则 confirm() 的 Promise 永久悬挂并锁死单例([H-5])。
71
+ // 若本实例是接管者,release 顺带把 owner 归还给挂起中的前任。
76
72
  const held = pendingRef.current;
77
73
  pendingRef.current = null;
78
74
  lease.release(held);
@@ -99,8 +95,6 @@ export function ConfirmHost(): React.JSX.Element | null {
99
95
  }).start();
100
96
  }, [entry, scrimOpacity]);
101
97
 
102
- if (inert) return null;
103
-
104
98
  return (
105
99
  <Modal
106
100
  visible={!!entry}
@@ -19,7 +19,8 @@ const store = createConfirmStore(log);
19
19
  * 弹出确认对话框,返回 Promise<boolean>。
20
20
  * - 用户点确认 → resolve(true)
21
21
  * - 用户点取消 / 点 backdrop / 系统返回 → resolve(false)
22
- * - 未挂 `<ConfirmHost />`、已有对话框在显示、Host 渲染抛错、Host 卸载 → resolve(false)
22
+ * - 未挂 `<ConfirmHost />`、已有对话框在显示、Host 渲染抛错、Host 卸载、
23
+ * 被后挂载的 Host 接管 → resolve(false)
23
24
  *
24
25
  * 任何路径都必然 settle,不会悬挂。
25
26
  *
@@ -37,7 +38,7 @@ const store = createConfirmStore(log);
37
38
  export const confirm = (options: ConfirmOptions): Promise<boolean> =>
38
39
  store.request(options);
39
40
 
40
- /** 内部:ConfirmHost 挂载时注册唯一 owner;重复挂载得到 null。 */
41
+ /** 内部:ConfirmHost 挂载时接管 owner;前任入栈挂起,本 lease 释放时归还。 */
41
42
  export const registerConfirmHost = store.registerHost;
42
43
 
43
44
  /** 内部:所有关闭路径的唯一出口(identity-guarded + 幂等)。 */
@@ -5,10 +5,17 @@ import type { ConfirmOptions } from './types';
5
5
  * Confirm 的纯状态机 —— 不含任何 React / 渲染依赖,可直接单测。
6
6
  *
7
7
  * 两条不变量:
8
- * 1. **唯一 owner** —— 同一时间只有一个 `<ConfirmHost />` 持有订阅。重复挂载拿到 `null`
9
- * lease、永久惰性,不会出现两个 Host 争抢同一个 entry。
8
+ * 1. **单一活跃 owner + 栈式接管** —— 同一时间只有一个 `<ConfirmHost />` 收事件,但
9
+ * 后挂载者**接管**、前任入栈挂起,卸载时自动归还。RN `Modal` 是独立 native window,
10
+ * 根 Host 渲染的对话框会被 Modal 物理盖住(iOS 兄弟 Modal 更是不叠放),所以
11
+ * 「Modal 内自挂一个 Host」是唯一正解、必须是合法用法 —— 旧的 first-wins 把它判成
12
+ * 恒惰性死码,才是要修的东西。
10
13
  * 2. **唯一 active entry** —— 同一时间只有一个未决对话框。所有关闭路径(确认 / 取消 /
11
- * backdrop / 系统返回 / Host 卸载 / subscriber 抛错)都汇聚到 `settle()`。
14
+ * backdrop / 系统返回 / Host 卸载 / owner 切换 / subscriber 抛错)都汇聚到 `settle()`。
15
+ *
16
+ * 由 1 派生出一条关键不变量:**active entry 永远属于当前 owner**。owner 每次切换
17
+ * (接管或归还)前都把 active 排空为 `false`,因此挂起中的 owner 手里不可能有未决
18
+ * 对话框,恢复时也不会继承一个自己没见过 `show` 的槽位。
12
19
  *
13
20
  * `settle()` 用 **identity guard**(`active !== entry` 直接返回 false)而非 id 比较:
14
21
  * 旧 entry 的迟到回调拿着的是旧对象引用,永远匹配不上新 active,因此不可能误关新对话框。
@@ -32,13 +39,15 @@ export type ConfirmHostLease = {
32
39
  /**
33
40
  * Host 卸载。`heldEntry` 是 Host 当时手里那个未决 entry(没有则传 `null`)——
34
41
  * 只有它仍是 active 时才会被 settle 成 `false`,避免误伤后来的对话框。
42
+ * 幂等:重复调用、或本 owner 早已被接管者顶下去,都是无副作用的 no-op。
35
43
  */
36
44
  release(heldEntry: ConfirmEntry | null): void;
37
45
  };
38
46
 
39
47
  export type ConfirmStore = {
40
48
  request(options: ConfirmOptions): Promise<boolean>;
41
- registerHost(subscriber: ConfirmSubscriber): ConfirmHostLease | null;
49
+ /** 挂载 Host。恒返回可用 lease —— 已有 owner 时走接管,不再拒绝。 */
50
+ registerHost(subscriber: ConfirmSubscriber): ConfirmHostLease;
42
51
  settle(entry: ConfirmEntry, result: boolean): boolean;
43
52
  activeEntry(): ConfirmEntry | null;
44
53
  };
@@ -49,6 +58,8 @@ export function createConfirmStore(log: Logger): ConfirmStore {
49
58
  let nextId = 0;
50
59
  let active: ConfirmEntry | null = null;
51
60
  let owner: Owner | null = null;
61
+ /** 被接管而挂起的前任,栈顶 = 最近一个。当前 owner 卸载时从这里恢复。 */
62
+ const suspended: Owner[] = [];
52
63
 
53
64
  function settle(entry: ConfirmEntry, result: boolean): boolean {
54
65
  if (entry.settled || active !== entry) return false;
@@ -88,7 +99,7 @@ export function createConfirmStore(log: Logger): ConfirmStore {
88
99
  const current = owner;
89
100
  if (!current?.subscriber) {
90
101
  log.warn(
91
- 'confirm() 调用时未挂载 <ConfirmHost />,对话框无法显示,已 resolve(false)。请在 app 根附近挂一次 <ConfirmHost />。'
102
+ 'confirm() 调用时未挂载 <ConfirmHost />,对话框无法显示,已 resolve(false)。请在 app 根附近挂一个 <ConfirmHost />。'
92
103
  );
93
104
  return Promise.resolve(false);
94
105
  }
@@ -114,26 +125,63 @@ export function createConfirmStore(log: Logger): ConfirmStore {
114
125
  });
115
126
  }
116
127
 
117
- function registerHost(
118
- subscriber: ConfirmSubscriber
119
- ): ConfirmHostLease | null {
120
- if (owner) {
121
- log.warn(
122
- '检测到多个 <ConfirmHost />。只有第一个生效,重复挂载的实例保持惰性 —— 请在 app 根只挂一次。'
123
- );
124
- return null;
128
+ /**
129
+ * owner 切换前把 active 排空为 `false` —— Promise 不悬挂、单例槽不被带走。
130
+ *
131
+ * 每轮 settle 成功即 `active = null`,只有 clear 回调同步再 `request()` 时才会多转
132
+ * 一轮;settle 返回 false(排不动)立即退出,不给死循环留口子。
133
+ */
134
+ function drainActive(): void {
135
+ while (active) {
136
+ if (!settle(active, false)) break;
125
137
  }
138
+ }
139
+
140
+ /** 弹出最近一个仍可用的挂起 owner;抛错被作废的(subscriber 已置空)直接丢弃。 */
141
+ function popSuspended(): Owner | null {
142
+ while (suspended.length > 0) {
143
+ const next = suspended.pop();
144
+ if (next?.subscriber) return next;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ function registerHost(subscriber: ConfirmSubscriber): ConfirmHostLease {
126
150
  const token = Symbol('ConfirmHostOwner');
127
151
  const self: Owner = { token, subscriber };
152
+ if (owner) {
153
+ // 先排空再换人:此刻前任仍是 owner,clear 事件正好发给它,它的对话框就地关闭。
154
+ // 顺序反过来(先换 owner 再 settle)clear 会误投给刚接管的新 Host。
155
+ drainActive();
156
+ // 排空期间 clear 回调若抛错,settle 已把前任作废(owner 置 null)—— 作废的不入栈,
157
+ // 免得恢复出一个死订阅;若回调期间又有人注册,入栈的就是那位,同样不丢。
158
+ const outgoing = owner;
159
+ if (outgoing) suspended.push(outgoing);
160
+ }
128
161
  owner = self;
129
162
  return {
130
163
  ownerToken: token,
131
164
  release(heldEntry: ConfirmEntry | null): void {
165
+ const index = suspended.indexOf(self);
166
+ if (index >= 0) {
167
+ // 乱序卸载(父 Modal 先于内层 Host 卸载):挂起者的 active 在被接管时已结算,
168
+ // 这里只把它摘出栈,绝不能碰当前 owner。
169
+ suspended.splice(index, 1);
170
+ self.subscriber = null;
171
+ return;
172
+ }
132
173
  if (owner?.token !== token) return;
133
174
  // 先摘订阅,再结算:settle 内部发的 clear 事件不应再回到正在卸载的 Host。
134
175
  self.subscriber = null;
135
176
  owner = null;
136
177
  if (heldEntry) settle(heldEntry, false);
178
+ const successor = popSuspended();
179
+ if (successor) {
180
+ // 归还前再排空一次:前任没见过残留 entry 的 show,不能让它继承一个锁死的槽。
181
+ // 此刻 owner 为 null,排空不会发出任何 clear。
182
+ drainActive();
183
+ owner = successor;
184
+ }
137
185
  },
138
186
  };
139
187
  }
@@ -27,9 +27,13 @@ type DeliveryIdentity = {
27
27
  };
28
28
 
29
29
  /**
30
- * 在根附近挂**一次**。监听 toast() 调用并渲染当前 toast。
30
+ * 在根附近挂一个。监听 toast() 调用并渲染当前 toast。
31
31
  * 同一时间只显示一条 —— 新的会替换旧的(latest-wins)。
32
32
  *
33
+ * 栈式 owner:后挂载的实例接管投递,前任挂起并收起当前 toast;接管者卸载后自动归还。
34
+ * 因此**允许**在自己的 `Modal` 里再挂一个 —— RN `Modal` 是独立 native window,根 Host
35
+ * 的 toast 会被它物理盖住,内层自挂才看得见。
36
+ *
33
37
  * 位置由 `entry.position`('top' / 'bottom' / 'center')决定,top/bottom 自动避让
34
38
  * safe-area;进入动画方向随位置(top 从上滑、bottom/center 从下滑)。
35
39
  *
@@ -66,7 +70,21 @@ export function ToastHost({
66
70
  };
67
71
  setDelivery(next);
68
72
  };
69
- const lease = registerToastHost(subscriber);
73
+ // 被后挂载的 Host 接管:立刻收起当前 toast。store 已丢弃这次投递,自身的退场
74
+ // timer 从此过不了 CAS —— 不主动清,这条 toast 会冻在屏幕上直到本 Host 卸载。
75
+ const clearOnSuspend = () => {
76
+ currentDeliveryRef.current = null;
77
+ if (dismissTimer.current) {
78
+ clearTimeout(dismissTimer.current);
79
+ dismissTimer.current = null;
80
+ }
81
+ cancelAnimation(op);
82
+ cancelAnimation(ty);
83
+ setDelivery(null);
84
+ };
85
+ const lease = registerToastHost(subscriber, clearOnSuspend);
86
+ // null lease 只剩一种成因:补投 pending 时 subscriber 抛错,本 owner 当场被作废
87
+ //(不再是「重复挂载」—— 那条现在走接管)。惰性到卸载为止。
70
88
  if (!lease) {
71
89
  setInert(true);
72
90
  return;
@@ -34,6 +34,7 @@ type DeliveryIdentity = {
34
34
  * - opacity 0→1 + translateY 8→0 入场(motion.base ms)
35
35
  * - 停留 entry.duration ms
36
36
  * - opacity 1→0 + translateY 0→8 退场后 unmount
37
+ * - 栈式 owner:后挂载者接管、前任挂起并收起 toast,接管者卸载后自动归还
37
38
  *
38
39
  * 竞态纪律与 native 版一致:timer / RAF 在改 UI 或上报完成前,都要同时通过同步 ref
39
40
  * 与 `isCurrentToastDelivery(delivery)` 两道校验。
@@ -79,7 +80,16 @@ export function ToastHost({
79
80
  };
80
81
  setDelivery(next);
81
82
  };
82
- const lease = registerToastHost(subscriber);
83
+ // 被后挂载的 Host 接管:立刻收起当前 toast。store 已丢弃这次投递,自身的退场
84
+ // timer/RAF 从此过不了 CAS —— 不主动清,这条 toast 会冻在屏幕上直到本 Host 卸载。
85
+ const clearOnSuspend = () => {
86
+ currentDeliveryRef.current = null;
87
+ cancelCallbacks();
88
+ setDelivery(null);
89
+ };
90
+ const lease = registerToastHost(subscriber, clearOnSuspend);
91
+ // null lease 只剩一种成因:补投 pending 时 subscriber 抛错,本 owner 当场被作废
92
+ //(不再是「重复挂载」—— 那条现在走接管)。惰性到卸载为止。
83
93
  if (!lease) {
84
94
  setInert(true);
85
95
  return;