@unif/react-native-design 0.4.33 → 0.4.35

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 (33) hide show
  1. package/lib/module/components/ui/Pulse/Pulse.js +1 -1
  2. package/lib/module/components/ui/Pulse/Pulse.js.map +1 -1
  3. package/lib/module/components/ui/Pulse/PulseDot.js +1 -1
  4. package/lib/module/components/ui/Pulse/PulseDot.js.map +1 -1
  5. package/lib/module/components/ui/Pulse/index.js +1 -1
  6. package/lib/module/components/ui/Pulse/index.js.map +1 -1
  7. package/lib/module/components/ui/Pulse/usePulse.web.js +55 -0
  8. package/lib/module/components/ui/Pulse/usePulse.web.js.map +1 -0
  9. package/lib/module/components/ui/Spinner/Spinner.web.js +79 -0
  10. package/lib/module/components/ui/Spinner/Spinner.web.js.map +1 -0
  11. package/lib/module/components/ui/Spinner/index.js +1 -1
  12. package/lib/module/components/ui/Spinner/index.js.map +1 -1
  13. package/lib/module/components/ui/Switch/Switch.web.js +73 -0
  14. package/lib/module/components/ui/Switch/Switch.web.js.map +1 -0
  15. package/lib/module/components/ui/Switch/index.js +1 -1
  16. package/lib/module/components/ui/Switch/index.js.map +1 -1
  17. package/lib/module/components/ui/Toast/ToastHost.web.js +102 -0
  18. package/lib/module/components/ui/Toast/ToastHost.web.js.map +1 -0
  19. package/lib/module/components/ui/Toast/index.js +1 -1
  20. package/lib/module/components/ui/Toast/index.js.map +1 -1
  21. package/lib/typescript/src/components/ui/Pulse/usePulse.web.d.ts +17 -0
  22. package/lib/typescript/src/components/ui/Pulse/usePulse.web.d.ts.map +1 -0
  23. package/lib/typescript/src/components/ui/Spinner/Spinner.web.d.ts +4 -0
  24. package/lib/typescript/src/components/ui/Spinner/Spinner.web.d.ts.map +1 -0
  25. package/lib/typescript/src/components/ui/Switch/Switch.web.d.ts +14 -0
  26. package/lib/typescript/src/components/ui/Switch/Switch.web.d.ts.map +1 -0
  27. package/lib/typescript/src/components/ui/Toast/ToastHost.web.d.ts +14 -0
  28. package/lib/typescript/src/components/ui/Toast/ToastHost.web.d.ts.map +1 -0
  29. package/package.json +4 -3
  30. package/src/components/ui/Pulse/usePulse.web.ts +57 -0
  31. package/src/components/ui/Spinner/Spinner.web.tsx +87 -0
  32. package/src/components/ui/Switch/Switch.web.tsx +67 -0
  33. package/src/components/ui/Toast/ToastHost.web.tsx +104 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React from 'react';
4
4
  import Animated from 'react-native-reanimated';
5
- import { usePulse } from "./usePulse.js";
5
+ import { usePulse } from './usePulse';
6
6
  import { jsx as _jsx } from "react/jsx-runtime";
7
7
  export function Pulse({
8
8
  children,
@@ -1 +1 @@
1
- {"version":3,"names":["React","Animated","usePulse","jsx","_jsx","Pulse","children","testID","opts","animatedStyle","View","style"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/Pulse.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,QAAQ,MAAM,yBAAyB;AAC9C,SAASC,QAAQ,QAAQ,eAAY;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGtC,OAAO,SAASC,KAAKA,CAAC;EACpBC,QAAQ;EACRC,MAAM;EACN,GAAGC;AACuC,CAAC,EAAqB;EAChE,MAAMC,aAAa,GAAGP,QAAQ,CAACM,IAAI,CAAC;EACpC,oBACEJ,IAAA,CAACH,QAAQ,CAACS,IAAI;IAACC,KAAK,EAAEF,aAAc;IAACF,MAAM,EAAEA,MAAO;IAAAD,QAAA,EACjDA;EAAQ,CACI,CAAC;AAEpB","ignoreList":[]}
1
+ {"version":3,"names":["React","Animated","usePulse","jsx","_jsx","Pulse","children","testID","opts","animatedStyle","View","style"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/Pulse.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,QAAQ,MAAM,yBAAyB;AAC9C,SAASC,QAAQ,QAAQ,YAAY;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGtC,OAAO,SAASC,KAAKA,CAAC;EACpBC,QAAQ;EACRC,MAAM;EACN,GAAGC;AACuC,CAAC,EAAqB;EAChE,MAAMC,aAAa,GAAGP,QAAQ,CAACM,IAAI,CAAC;EACpC,oBACEJ,IAAA,CAACH,QAAQ,CAACS,IAAI;IAACC,KAAK,EAAEF,aAAc;IAACF,MAAM,EAAEA,MAAO;IAAAD,QAAA,EACjDA;EAAQ,CACI,CAAC;AAEpB","ignoreList":[]}
@@ -3,7 +3,7 @@
3
3
  import React from 'react';
4
4
  import Animated from 'react-native-reanimated';
5
5
  import { useColors } from "../../../theme/index.js";
6
- import { usePulse } from "./usePulse.js";
6
+ import { usePulse } from './usePulse';
7
7
  import { jsx as _jsx } from "react/jsx-runtime";
8
8
  export function PulseDot({
9
9
  size = 6,
@@ -1 +1 @@
1
- {"version":3,"names":["React","Animated","useColors","usePulse","jsx","_jsx","PulseDot","size","color","from","to","duration","delay","testID","c","fill","primary","animatedStyle","View","style","width","height","borderRadius","backgroundColor","accessibilityElementsHidden","importantForAccessibility"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/PulseDot.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,QAAQ,MAAM,yBAAyB;AAC9C,SAASC,SAAS,QAAQ,yBAAgB;AAC1C,SAASC,QAAQ,QAAQ,eAAY;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGtC,OAAO,SAASC,QAAQA,CAAC;EACvBC,IAAI,GAAG,CAAC;EACRC,KAAK;EACLC,IAAI,GAAG,GAAG;EACVC,EAAE,GAAG,CAAC;EACNC,QAAQ,GAAG,GAAG;EACdC,KAAK,GAAG,CAAC;EACTC;AACa,CAAC,GAAG,CAAC,CAAC,EAAqB;EACxC,MAAMC,CAAC,GAAGZ,SAAS,CAAC,CAAC;EACrB,MAAMa,IAAI,GAAGP,KAAK,IAAIM,CAAC,CAACE,OAAO;EAC/B,MAAMC,aAAa,GAAGd,QAAQ,CAAC;IAAEM,IAAI;IAAEC,EAAE;IAAEC,QAAQ;IAAEC;EAAM,CAAC,CAAC;EAC7D,oBACEP,IAAA,CAACJ,QAAQ,CAACiB,IAAI;IACZC,KAAK,EAAE,CACL;MACEC,KAAK,EAAEb,IAAI;MACXc,MAAM,EAAEd,IAAI;MACZe,YAAY,EAAEf,IAAI,GAAG,CAAC;MACtBgB,eAAe,EAAER;IACnB,CAAC,EACDE,aAAa,CACb;IACFJ,MAAM,EAAEA,MAAO;IACfW,2BAA2B;IAC3BC,yBAAyB,EAAC;EAAqB,CAChD,CAAC;AAEN","ignoreList":[]}
1
+ {"version":3,"names":["React","Animated","useColors","usePulse","jsx","_jsx","PulseDot","size","color","from","to","duration","delay","testID","c","fill","primary","animatedStyle","View","style","width","height","borderRadius","backgroundColor","accessibilityElementsHidden","importantForAccessibility"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/PulseDot.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,QAAQ,MAAM,yBAAyB;AAC9C,SAASC,SAAS,QAAQ,yBAAgB;AAC1C,SAASC,QAAQ,QAAQ,YAAY;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGtC,OAAO,SAASC,QAAQA,CAAC;EACvBC,IAAI,GAAG,CAAC;EACRC,KAAK;EACLC,IAAI,GAAG,GAAG;EACVC,EAAE,GAAG,CAAC;EACNC,QAAQ,GAAG,GAAG;EACdC,KAAK,GAAG,CAAC;EACTC;AACa,CAAC,GAAG,CAAC,CAAC,EAAqB;EACxC,MAAMC,CAAC,GAAGZ,SAAS,CAAC,CAAC;EACrB,MAAMa,IAAI,GAAGP,KAAK,IAAIM,CAAC,CAACE,OAAO;EAC/B,MAAMC,aAAa,GAAGd,QAAQ,CAAC;IAAEM,IAAI;IAAEC,EAAE;IAAEC,QAAQ;IAAEC;EAAM,CAAC,CAAC;EAC7D,oBACEP,IAAA,CAACJ,QAAQ,CAACiB,IAAI;IACZC,KAAK,EAAE,CACL;MACEC,KAAK,EAAEb,IAAI;MACXc,MAAM,EAAEd,IAAI;MACZe,YAAY,EAAEf,IAAI,GAAG,CAAC;MACtBgB,eAAe,EAAER;IACnB,CAAC,EACDE,aAAa,CACb;IACFJ,MAAM,EAAEA,MAAO;IACfW,2BAA2B;IAC3BC,yBAAyB,EAAC;EAAqB,CAChD,CAAC;AAEN","ignoreList":[]}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- export { usePulse } from "./usePulse.js";
3
+ export { usePulse } from './usePulse';
4
4
  export { Pulse } from "./Pulse.js";
5
5
  export { PulseDot } from "./PulseDot.js";
6
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["usePulse","Pulse","PulseDot"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/index.ts"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,eAAY;AACrC,SAASC,KAAK,QAAQ,YAAS;AAC/B,SAASC,QAAQ,QAAQ,eAAY","ignoreList":[]}
1
+ {"version":3,"names":["usePulse","Pulse","PulseDot"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/index.ts"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,YAAY;AACrC,SAASC,KAAK,QAAQ,YAAS;AAC/B,SAASC,QAAQ,QAAQ,eAAY","ignoreList":[]}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useState } from 'react';
4
+ /**
5
+ * Web 端 usePulse —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
6
+ * 链路会抛 `Object.keys(undefined)` TypeError(animation frame 每帧打印),pulse 卡死
7
+ * 在初始值。这里用 React state + setInterval + CSS 等价于在两个 opacity 之间循环,
8
+ * 返回 RN style({ opacity }),保持 native API 兼容。
9
+ *
10
+ * native 端走 usePulse.ts(reanimated worklet) 不动。
11
+ *
12
+ * 注意:实现走 React state 而非 CSS transition——因为 RN style 不支持 transition,
13
+ * 而 setInterval 切换 opacity 加上一个外层 div 的 CSS transition 视觉上等价
14
+ * (transition 由 ToastHost.web.tsx 那个模式接管,这里直接用 React 切值)。
15
+ */
16
+ export function usePulse({
17
+ from = 0.6,
18
+ to = 1,
19
+ duration = 700,
20
+ delay = 0
21
+ } = {}) {
22
+ const [opacity, setOpacity] = useState(from);
23
+ useEffect(() => {
24
+ let cancelled = false;
25
+ let direction = 'up';
26
+ let start = Date.now() + delay;
27
+ let raf = null;
28
+ const tick = () => {
29
+ if (cancelled) return;
30
+ const now = Date.now();
31
+ if (now < start) {
32
+ raf = requestAnimationFrame(tick);
33
+ return;
34
+ }
35
+ const elapsed = (now - start) % duration;
36
+ const t = elapsed / duration; // 0→1
37
+ const next = direction === 'up' ? from + (to - from) * t : to - (to - from) * t;
38
+ setOpacity(next);
39
+ if (elapsed + 16 >= duration) {
40
+ direction = direction === 'up' ? 'down' : 'up';
41
+ start = now;
42
+ }
43
+ raf = requestAnimationFrame(tick);
44
+ };
45
+ raf = requestAnimationFrame(tick);
46
+ return () => {
47
+ cancelled = true;
48
+ if (raf != null) cancelAnimationFrame(raf);
49
+ };
50
+ }, [from, to, duration, delay]);
51
+ return {
52
+ opacity
53
+ };
54
+ }
55
+ //# sourceMappingURL=usePulse.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useEffect","useState","usePulse","from","to","duration","delay","opacity","setOpacity","cancelled","direction","start","Date","now","raf","tick","requestAnimationFrame","elapsed","t","next","cancelAnimationFrame"],"sourceRoot":"../../../../../src","sources":["components/ui/Pulse/usePulse.web.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAG3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAAC;EACvBC,IAAI,GAAG,GAAG;EACVC,EAAE,GAAG,CAAC;EACNC,QAAQ,GAAG,GAAG;EACdC,KAAK,GAAG;AACI,CAAC,GAAG,CAAC,CAAC,EAAuB;EACzC,MAAM,CAACC,OAAO,EAAEC,UAAU,CAAC,GAAGP,QAAQ,CAACE,IAAI,CAAC;EAE5CH,SAAS,CAAC,MAAM;IACd,IAAIS,SAAS,GAAG,KAAK;IACrB,IAAIC,SAAwB,GAAG,IAAI;IACnC,IAAIC,KAAK,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGP,KAAK;IAC9B,IAAIQ,GAAkB,GAAG,IAAI;IAE7B,MAAMC,IAAI,GAAGA,CAAA,KAAM;MACjB,IAAIN,SAAS,EAAE;MACf,MAAMI,GAAG,GAAGD,IAAI,CAACC,GAAG,CAAC,CAAC;MACtB,IAAIA,GAAG,GAAGF,KAAK,EAAE;QACfG,GAAG,GAAGE,qBAAqB,CAACD,IAAI,CAAC;QACjC;MACF;MACA,MAAME,OAAO,GAAG,CAACJ,GAAG,GAAGF,KAAK,IAAIN,QAAQ;MACxC,MAAMa,CAAC,GAAGD,OAAO,GAAGZ,QAAQ,CAAC,CAAC;MAC9B,MAAMc,IAAI,GACRT,SAAS,KAAK,IAAI,GAAGP,IAAI,GAAG,CAACC,EAAE,GAAGD,IAAI,IAAIe,CAAC,GAAGd,EAAE,GAAG,CAACA,EAAE,GAAGD,IAAI,IAAIe,CAAC;MACpEV,UAAU,CAACW,IAAI,CAAC;MAChB,IAAIF,OAAO,GAAG,EAAE,IAAIZ,QAAQ,EAAE;QAC5BK,SAAS,GAAGA,SAAS,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI;QAC9CC,KAAK,GAAGE,GAAG;MACb;MACAC,GAAG,GAAGE,qBAAqB,CAACD,IAAI,CAAC;IACnC,CAAC;IACDD,GAAG,GAAGE,qBAAqB,CAACD,IAAI,CAAC;IAEjC,OAAO,MAAM;MACXN,SAAS,GAAG,IAAI;MAChB,IAAIK,GAAG,IAAI,IAAI,EAAEM,oBAAoB,CAACN,GAAG,CAAC;IAC5C,CAAC;EACH,CAAC,EAAE,CAACX,IAAI,EAAEC,EAAE,EAAEC,QAAQ,EAAEC,KAAK,CAAC,CAAC;EAE/B,OAAO;IAAEC;EAAQ,CAAC;AACpB","ignoreList":[]}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ import React, { useEffect, useRef } from 'react';
4
+ import { View } from 'react-native';
5
+ import { useColors } from "../../../theme/index.js";
6
+ import { createLogger } from "../../../utils/logger/index.js";
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const log = createLogger('Spinner');
9
+
10
+ // React-Native-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle 链路在
11
+ // web bundle 里跑会触发 `_updatePropsJS` 里 `Object.keys(updates)` 的 worklet
12
+ // 回值打包 edge case(animation frame 每帧抛 TypeError),旋转停滞。
13
+ // 该 .web.tsx 用纯 CSS @keyframes 替代,native 端仍走 Spinner.tsx 的 reanimated 实现,
14
+ // 走 webpack `resolve.extensions = ['.web.tsx', ...]`,bundle 命中 .web 版本。
15
+ const SPIN_KEYFRAMES_ID = 'unif-spinner-spin-keyframes';
16
+
17
+ // tsconfig 不带 DOM lib(库本体是 RN),用 globalThis 兜底访问浏览器 API,
18
+ // 类型用 any 通过 strict 检查(实际运行只在 web bundle 命中本文件,document 一定有)。
19
+
20
+ const win = globalThis;
21
+ function ensureKeyframes() {
22
+ if (typeof win.document === 'undefined') return;
23
+ if (win.document.getElementById(SPIN_KEYFRAMES_ID)) return;
24
+ const style = win.document.createElement('style');
25
+ style.id = SPIN_KEYFRAMES_ID;
26
+ // ⚠️ 安全:注入 <style> 的内容会被浏览器当 CSS 解析。本字符串必须保持「静态
27
+ // 硬编码」,严禁拼接任何外部 / 用户 / props 输入(size / color 等),否则 CSS
28
+ // 注入。每实例的尺寸 / 颜色 / 时长走 RN style + node.style.animation(见下),
29
+ // 不进 keyframes —— 本函数永远只注册这一条固定 0→360deg 规则。
30
+ style.textContent = '@keyframes unif-spinner-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
31
+ win.document.head.appendChild(style);
32
+ }
33
+ export function Spinner({
34
+ size = 18,
35
+ color,
36
+ thickness = 2,
37
+ style,
38
+ testID
39
+ }) {
40
+ const c = useColors();
41
+ // ref 用 any —— RN View ref 在 web bundle 实际拿到 HTMLDivElement,
42
+ // RN 端拿到 View 实例,统一 any 兼容两端。
43
+
44
+ const ref = useRef(null);
45
+ const stroke = color ?? c.primary;
46
+ if (!Number.isFinite(size) || size < 8) {
47
+ log.warn(`size 应为 ≥8 的有限数,传入 ${size},已钳到 8`);
48
+ }
49
+ if (Number.isFinite(thickness) && thickness <= 0) {
50
+ log.warn(`thickness 应为正数,传入 ${thickness},已 fallback 为 2`);
51
+ }
52
+ const safeSize = Number.isFinite(size) && size > 8 ? size : 8;
53
+ const safeThickness = Number.isFinite(thickness) && thickness > 0 ? thickness : 2;
54
+
55
+ // RN-Web 把 View 渲染成 div,我们用 ref 拿到底层 DOM 节点直接挂 CSS animation
56
+ // (RN style 不支持 animation 字段;只能事后注入)。
57
+ useEffect(() => {
58
+ ensureKeyframes();
59
+ const node = ref.current;
60
+ if (node && node.style) {
61
+ node.style.animation = 'unif-spinner-spin 900ms linear infinite';
62
+ }
63
+ }, []);
64
+ return /*#__PURE__*/_jsx(View, {
65
+ ref: ref,
66
+ style: [{
67
+ width: safeSize,
68
+ height: safeSize,
69
+ borderRadius: safeSize / 2,
70
+ borderWidth: safeThickness,
71
+ borderColor: c.outline,
72
+ borderTopColor: stroke
73
+ }, style],
74
+ testID: testID,
75
+ accessibilityElementsHidden: true,
76
+ importantForAccessibility: "no-hide-descendants"
77
+ });
78
+ }
79
+ //# sourceMappingURL=Spinner.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","useEffect","useRef","View","useColors","createLogger","jsx","_jsx","log","SPIN_KEYFRAMES_ID","win","globalThis","ensureKeyframes","document","getElementById","style","createElement","id","textContent","head","appendChild","Spinner","size","color","thickness","testID","c","ref","stroke","primary","Number","isFinite","warn","safeSize","safeThickness","node","current","animation","width","height","borderRadius","borderWidth","borderColor","outline","borderTopColor","accessibilityElementsHidden","importantForAccessibility"],"sourceRoot":"../../../../../src","sources":["components/ui/Spinner/Spinner.web.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,SAAS,EAAEC,MAAM,QAAQ,OAAO;AAChD,SAASC,IAAI,QAAQ,cAAc;AACnC,SAASC,SAAS,QAAQ,yBAAgB;AAC1C,SAASC,YAAY,QAAQ,gCAAuB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGrD,MAAMC,GAAG,GAAGH,YAAY,CAAC,SAAS,CAAC;;AAEnC;AACA;AACA;AACA;AACA;AACA,MAAMI,iBAAiB,GAAG,6BAA6B;;AAEvD;AACA;;AAEA,MAAMC,GAAQ,GAAGC,UAAiB;AAElC,SAASC,eAAeA,CAAA,EAAG;EACzB,IAAI,OAAOF,GAAG,CAACG,QAAQ,KAAK,WAAW,EAAE;EACzC,IAAIH,GAAG,CAACG,QAAQ,CAACC,cAAc,CAACL,iBAAiB,CAAC,EAAE;EACpD,MAAMM,KAAK,GAAGL,GAAG,CAACG,QAAQ,CAACG,aAAa,CAAC,OAAO,CAAC;EACjDD,KAAK,CAACE,EAAE,GAAGR,iBAAiB;EAC5B;EACA;EACA;EACA;EACAM,KAAK,CAACG,WAAW,GACf,sGAAsG;EACxGR,GAAG,CAACG,QAAQ,CAACM,IAAI,CAACC,WAAW,CAACL,KAAK,CAAC;AACtC;AAEA,OAAO,SAASM,OAAOA,CAAC;EACtBC,IAAI,GAAG,EAAE;EACTC,KAAK;EACLC,SAAS,GAAG,CAAC;EACbT,KAAK;EACLU;AACY,CAAC,EAAqB;EAClC,MAAMC,CAAC,GAAGtB,SAAS,CAAC,CAAC;EACrB;EACA;;EAEA,MAAMuB,GAAG,GAAGzB,MAAM,CAAM,IAAI,CAAC;EAC7B,MAAM0B,MAAM,GAAGL,KAAK,IAAIG,CAAC,CAACG,OAAO;EACjC,IAAI,CAACC,MAAM,CAACC,QAAQ,CAACT,IAAI,CAAC,IAAKA,IAAI,GAAc,CAAC,EAAE;IAClDd,GAAG,CAACwB,IAAI,CAAC,sBAAsBV,IAAI,QAAQ,CAAC;EAC9C;EACA,IAAIQ,MAAM,CAACC,QAAQ,CAACP,SAAS,CAAC,IAAKA,SAAS,IAAe,CAAC,EAAE;IAC5DhB,GAAG,CAACwB,IAAI,CAAC,qBAAqBR,SAAS,iBAAiB,CAAC;EAC3D;EACA,MAAMS,QAAQ,GAAGH,MAAM,CAACC,QAAQ,CAACT,IAAI,CAAC,IAAIA,IAAI,GAAG,CAAC,GAAGA,IAAI,GAAG,CAAC;EAC7D,MAAMY,aAAa,GACjBJ,MAAM,CAACC,QAAQ,CAACP,SAAS,CAAC,IAAIA,SAAS,GAAG,CAAC,GAAGA,SAAS,GAAG,CAAC;;EAE7D;EACA;EACAvB,SAAS,CAAC,MAAM;IACdW,eAAe,CAAC,CAAC;IACjB,MAAMuB,IAAI,GAAGR,GAAG,CAACS,OAAO;IACxB,IAAID,IAAI,IAAIA,IAAI,CAACpB,KAAK,EAAE;MACtBoB,IAAI,CAACpB,KAAK,CAACsB,SAAS,GAAG,yCAAyC;IAClE;EACF,CAAC,EAAE,EAAE,CAAC;EAEN,oBACE9B,IAAA,CAACJ,IAAI;IACHwB,GAAG,EAAEA,GAAI;IACTZ,KAAK,EAAE,CACL;MACEuB,KAAK,EAAEL,QAAQ;MACfM,MAAM,EAAEN,QAAQ;MAChBO,YAAY,EAAEP,QAAQ,GAAG,CAAC;MAC1BQ,WAAW,EAAEP,aAAa;MAC1BQ,WAAW,EAAEhB,CAAC,CAACiB,OAAO;MACtBC,cAAc,EAAEhB;IAClB,CAAC,EACDb,KAAK,CACL;IACFU,MAAM,EAAEA,MAAO;IACfoB,2BAA2B;IAC3BC,yBAAyB,EAAC;EAAqB,CAChD,CAAC;AAEN","ignoreList":[]}
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export { Spinner } from "./Spinner.js";
3
+ export { Spinner } from './Spinner';
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["Spinner"],"sourceRoot":"../../../../../src","sources":["components/ui/Spinner/index.ts"],"mappings":";;AAAA,SAASA,OAAO,QAAQ,cAAW","ignoreList":[]}
1
+ {"version":3,"names":["Spinner"],"sourceRoot":"../../../../../src","sources":["components/ui/Spinner/index.ts"],"mappings":";;AAAA,SAASA,OAAO,QAAQ,WAAW","ignoreList":[]}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { Pressable } from 'react-native-gesture-handler';
5
+ import { View } from 'react-native';
6
+ import { motion, r, useColors, useThemedStyles } from "../../../theme/index.js";
7
+ import { INSET, makeStyles, THUMB, TRACK_W } from "./styles.js";
8
+ import { jsx as _jsx } from "react/jsx-runtime";
9
+ // ─── 动画派生常量(组件私有,非 style)─────────────────────────────────────
10
+ // 把手 translateX 的 off / on 端点:左右各留 INSET 间距,
11
+ // off = INSET(2),on = trackW - INSET - thumbW(32-2-16=14)。
12
+ const THUMB_OFF_X = r(INSET);
13
+ const THUMB_ON_X = r(TRACK_W - INSET - THUMB);
14
+
15
+ /**
16
+ * Web 端 Switch —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
17
+ * 链路会抛 `Object.keys(undefined)` TypeError(切换时打印,thumb 卡住),走 CSS
18
+ * transition 实现 backgroundColor + translateX,native 端仍走 Switch.tsx
19
+ * 的 reanimated 实现。
20
+ *
21
+ * 实现:track / thumb 仍是 RN View(尺寸 / 圆角走 RN style),把 web-only
22
+ * backgroundColor transition 通过 `style` 行内 CSS prop 注入(RN-Web 把
23
+ * style 数组合并到 element 的 inline style,我们补 `transition` 字段)。
24
+ */
25
+ export function Switch({
26
+ value,
27
+ onChange,
28
+ disabled,
29
+ accessibilityLabel,
30
+ testID
31
+ }) {
32
+ const c = useColors();
33
+ const styles = useThemedStyles(makeStyles);
34
+
35
+ // 给 RN style 数组追加 web-only 字段:transition + 切换值
36
+ const trackWebStyle = {
37
+ ...styles.track,
38
+ backgroundColor: value ? c.primary : c.surfaceContainerHighest,
39
+ // RN style 不识 transition 但 RN-Web 会把它当 inline style 透传给 DOM
40
+ transitionProperty: 'background-color',
41
+ transitionDuration: `${motion.base}ms`,
42
+ transitionTimingFunction: 'ease-out'
43
+ };
44
+ const thumbWebStyle = {
45
+ ...styles.thumb,
46
+ transform: [{
47
+ translateX: value ? THUMB_ON_X : THUMB_OFF_X
48
+ }],
49
+ transitionProperty: 'transform',
50
+ transitionDuration: `${motion.base}ms`,
51
+ transitionTimingFunction: 'ease-out'
52
+ };
53
+ return /*#__PURE__*/_jsx(Pressable, {
54
+ onPress: () => !disabled && onChange(!value),
55
+ disabled: disabled,
56
+ hitSlop: 6,
57
+ style: disabled ? styles.pressableDisabled : styles.pressableEnabled,
58
+ accessibilityRole: "switch",
59
+ accessibilityState: {
60
+ checked: value,
61
+ disabled: !!disabled
62
+ },
63
+ accessibilityLabel: accessibilityLabel,
64
+ testID: testID,
65
+ children: /*#__PURE__*/_jsx(View, {
66
+ style: trackWebStyle,
67
+ children: /*#__PURE__*/_jsx(View, {
68
+ style: thumbWebStyle
69
+ })
70
+ })
71
+ });
72
+ }
73
+ //# sourceMappingURL=Switch.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","Pressable","View","motion","r","useColors","useThemedStyles","INSET","makeStyles","THUMB","TRACK_W","jsx","_jsx","THUMB_OFF_X","THUMB_ON_X","Switch","value","onChange","disabled","accessibilityLabel","testID","c","styles","trackWebStyle","track","backgroundColor","primary","surfaceContainerHighest","transitionProperty","transitionDuration","base","transitionTimingFunction","thumbWebStyle","thumb","transform","translateX","onPress","hitSlop","style","pressableDisabled","pressableEnabled","accessibilityRole","accessibilityState","checked","children"],"sourceRoot":"../../../../../src","sources":["components/ui/Switch/Switch.web.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,SAAS,QAAQ,8BAA8B;AACxD,SAASC,IAAI,QAAQ,cAAc;AACnC,SAASC,MAAM,EAAEC,CAAC,EAAEC,SAAS,EAAEC,eAAe,QAAQ,yBAAgB;AACtE,SAASC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,OAAO,QAAQ,aAAU;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAG7D;AACA;AACA;AACA,MAAMC,WAAW,GAAGT,CAAC,CAACG,KAAK,CAAC;AAC5B,MAAMO,UAAU,GAAGV,CAAC,CAACM,OAAO,GAAGH,KAAK,GAAGE,KAAK,CAAC;;AAE7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASM,MAAMA,CAAC;EACrBC,KAAK;EACLC,QAAQ;EACRC,QAAQ;EACRC,kBAAkB;EAClBC;AACW,CAAC,EAAqB;EACjC,MAAMC,CAAC,GAAGhB,SAAS,CAAC,CAAC;EACrB,MAAMiB,MAAM,GAAGhB,eAAe,CAACE,UAAU,CAAC;;EAE1C;EACA,MAAMe,aAAa,GAAG;IACpB,GAAGD,MAAM,CAACE,KAAK;IACfC,eAAe,EAAET,KAAK,GAAGK,CAAC,CAACK,OAAO,GAAGL,CAAC,CAACM,uBAAuB;IAC9D;IACAC,kBAAkB,EAAE,kBAAkB;IACtCC,kBAAkB,EAAE,GAAG1B,MAAM,CAAC2B,IAAI,IAAI;IACtCC,wBAAwB,EAAE;EAC5B,CAAsB;EACtB,MAAMC,aAAa,GAAG;IACpB,GAAGV,MAAM,CAACW,KAAK;IACfC,SAAS,EAAE,CAAC;MAAEC,UAAU,EAAEnB,KAAK,GAAGF,UAAU,GAAGD;IAAY,CAAC,CAAC;IAC7De,kBAAkB,EAAE,WAAW;IAC/BC,kBAAkB,EAAE,GAAG1B,MAAM,CAAC2B,IAAI,IAAI;IACtCC,wBAAwB,EAAE;EAC5B,CAAsB;EAEtB,oBACEnB,IAAA,CAACX,SAAS;IACRmC,OAAO,EAAEA,CAAA,KAAM,CAAClB,QAAQ,IAAID,QAAQ,CAAC,CAACD,KAAK,CAAE;IAC7CE,QAAQ,EAAEA,QAAS;IACnBmB,OAAO,EAAE,CAAE;IACXC,KAAK,EAAEpB,QAAQ,GAAGI,MAAM,CAACiB,iBAAiB,GAAGjB,MAAM,CAACkB,gBAAiB;IACrEC,iBAAiB,EAAC,QAAQ;IAC1BC,kBAAkB,EAAE;MAAEC,OAAO,EAAE3B,KAAK;MAAEE,QAAQ,EAAE,CAAC,CAACA;IAAS,CAAE;IAC7DC,kBAAkB,EAAEA,kBAAmB;IACvCC,MAAM,EAAEA,MAAO;IAAAwB,QAAA,eAEfhC,IAAA,CAACV,IAAI;MAACoC,KAAK,EAAEf,aAAc;MAAAqB,QAAA,eACzBhC,IAAA,CAACV,IAAI;QAACoC,KAAK,EAAEN;MAAc,CAAE;IAAC,CAC1B;EAAC,CACE,CAAC;AAEhB","ignoreList":[]}
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export { Switch } from "./Switch.js";
3
+ export { Switch } from './Switch';
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["Switch"],"sourceRoot":"../../../../../src","sources":["components/ui/Switch/index.ts"],"mappings":";;AAAA,SAASA,MAAM,QAAQ,aAAU","ignoreList":[]}
1
+ {"version":3,"names":["Switch"],"sourceRoot":"../../../../../src","sources":["components/ui/Switch/index.ts"],"mappings":";;AAAA,SAASA,MAAM,QAAQ,UAAU","ignoreList":[]}
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { Text, View } from 'react-native';
5
+ import { useColors, useThemedStyles, motion } from "../../../theme/index.js";
6
+ import { dotColorFor, makeStyles } from "./styles.js";
7
+ import { _subs } from "./toast.js";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ /**
10
+ * Web 端 ToastHost —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
11
+ * 链路会在 _updatePropsJS 里 Object.keys 抛 TypeError(点击就崩),走纯 React + CSS
12
+ * transition 实现 fade + slide,native 端仍走 ToastHost.tsx 的 reanimated 实现。
13
+ *
14
+ * 行为复刻 native 版:
15
+ * - opacity 0→1 + translateY 8→0 入场(motion.base ms)
16
+ * - 停留 entry.duration ms
17
+ * - opacity 1→0 + translateY 0→8 退场后 unmount
18
+ */
19
+ export function ToastHost({
20
+ testID
21
+ } = {}) {
22
+ const c = useColors();
23
+ const styles = useThemedStyles(makeStyles);
24
+ const [entry, setEntry] = useState(null);
25
+ const [visible, setVisible] = useState(false);
26
+ const dismissTimerRef = useRef(null);
27
+ const exitTimerRef = useRef(null);
28
+ const enterRafRef = useRef(null);
29
+ useEffect(() => {
30
+ const sub = next => {
31
+ setEntry(next);
32
+ };
33
+ _subs.add(sub);
34
+ return () => {
35
+ _subs.delete(sub);
36
+ };
37
+ }, []);
38
+ useEffect(() => {
39
+ // entry=null:上一轮 effect 的 cleanup 已清 timer/RAF,这里无需重复。
40
+ if (!entry) return;
41
+
42
+ // cancelled 标志:entry 快速连续切换 / 组件卸载时,cleanup 置 true。RAF 与
43
+ // timer 回调执行前都检查它 —— cancelAnimationFrame 在部分浏览器不保证取消
44
+ // 已排队回调,且双 RAF 的外层回调可能在 cleanup 后仍跑并 queue 内层,光靠
45
+ // cancel 防不住。cancelled 兜底,杜绝 unmount 后 setState 警告 + 状态错位。
46
+ let cancelled = false;
47
+ // 进场:每次 entry 切换都从 visible=false 开始,确保 CSS transition 有起点
48
+ setVisible(false);
49
+ // 双 RAF 才能稳过 React commit/paint 一帧 —— 否则同一 microtask 内 visible
50
+ // 还是 true(未刷),CSS transition 没起点。
51
+ enterRafRef.current = requestAnimationFrame(() => {
52
+ if (cancelled) return;
53
+ enterRafRef.current = requestAnimationFrame(() => {
54
+ if (cancelled) return;
55
+ setVisible(true);
56
+ });
57
+ });
58
+ // 停留时长:从 setEntry 到关闭 = entry.duration ms。
59
+ dismissTimerRef.current = setTimeout(() => {
60
+ if (cancelled) return;
61
+ setVisible(false);
62
+ exitTimerRef.current = setTimeout(() => {
63
+ if (cancelled) return;
64
+ setEntry(null);
65
+ }, motion.base);
66
+ }, entry.duration);
67
+ return () => {
68
+ cancelled = true;
69
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
70
+ if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
71
+ if (enterRafRef.current != null) cancelAnimationFrame(enterRafRef.current);
72
+ };
73
+ }, [entry]);
74
+ if (!entry) return null;
75
+ const dotColor = dotColorFor(entry.kind, c);
76
+ const animatedWebStyle = {
77
+ opacity: visible ? 1 : 0,
78
+ transform: visible ? 'translateY(0px)' : 'translateY(8px)',
79
+ transition: `opacity ${motion.base}ms ease-out, transform ${motion.base}ms ease-out`
80
+ };
81
+ return /*#__PURE__*/_jsx(View, {
82
+ style: styles.host,
83
+ pointerEvents: "none",
84
+ testID: testID,
85
+ children: /*#__PURE__*/_jsx("div", {
86
+ style: animatedWebStyle,
87
+ children: /*#__PURE__*/_jsxs(View, {
88
+ style: styles.toast,
89
+ children: [dotColor ? /*#__PURE__*/_jsx(View, {
90
+ style: [styles.dot, {
91
+ backgroundColor: dotColor
92
+ }]
93
+ }) : null, /*#__PURE__*/_jsx(Text, {
94
+ style: styles.text,
95
+ testID: testID ? `${testID}-text` : undefined,
96
+ children: entry.message
97
+ })]
98
+ })
99
+ })
100
+ });
101
+ }
102
+ //# sourceMappingURL=ToastHost.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","useEffect","useRef","useState","Text","View","useColors","useThemedStyles","motion","dotColorFor","makeStyles","_subs","jsx","_jsx","jsxs","_jsxs","ToastHost","testID","c","styles","entry","setEntry","visible","setVisible","dismissTimerRef","exitTimerRef","enterRafRef","sub","next","add","delete","cancelled","current","requestAnimationFrame","setTimeout","base","duration","clearTimeout","cancelAnimationFrame","dotColor","kind","animatedWebStyle","opacity","transform","transition","style","host","pointerEvents","children","toast","dot","backgroundColor","text","undefined","message"],"sourceRoot":"../../../../../src","sources":["components/ui/Toast/ToastHost.web.tsx"],"mappings":"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","ignoreList":[]}
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  export { toast } from "./toast.js";
4
- export { ToastHost } from "./ToastHost.js";
4
+ export { ToastHost } from './ToastHost';
5
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["toast","ToastHost"],"sourceRoot":"../../../../../src","sources":["components/ui/Toast/index.ts"],"mappings":";;AAAA,SAASA,KAAK,QAAQ,YAAS;AAC/B,SAASC,SAAS,QAAQ,gBAAa","ignoreList":[]}
1
+ {"version":3,"names":["toast","ToastHost"],"sourceRoot":"../../../../../src","sources":["components/ui/Toast/index.ts"],"mappings":";;AAAA,SAASA,KAAK,QAAQ,YAAS;AAC/B,SAASC,SAAS,QAAQ,aAAa","ignoreList":[]}
@@ -0,0 +1,17 @@
1
+ import type { PulseOptions } from './types';
2
+ /**
3
+ * Web 端 usePulse —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
4
+ * 链路会抛 `Object.keys(undefined)` TypeError(animation frame 每帧打印),pulse 卡死
5
+ * 在初始值。这里用 React state + setInterval + CSS 等价于在两个 opacity 之间循环,
6
+ * 返回 RN style({ opacity }),保持 native API 兼容。
7
+ *
8
+ * native 端走 usePulse.ts(reanimated worklet) 不动。
9
+ *
10
+ * 注意:实现走 React state 而非 CSS transition——因为 RN style 不支持 transition,
11
+ * 而 setInterval 切换 opacity 加上一个外层 div 的 CSS transition 视觉上等价
12
+ * (transition 由 ToastHost.web.tsx 那个模式接管,这里直接用 React 切值)。
13
+ */
14
+ export declare function usePulse({ from, to, duration, delay, }?: PulseOptions): {
15
+ opacity: number;
16
+ };
17
+ //# sourceMappingURL=usePulse.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePulse.web.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Pulse/usePulse.web.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,EACvB,IAAU,EACV,EAAM,EACN,QAAc,EACd,KAAS,GACV,GAAE,YAAiB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAoCzC"}
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { SpinnerProps } from './types';
3
+ export declare function Spinner({ size, color, thickness, style, testID, }: SpinnerProps): React.JSX.Element;
4
+ //# sourceMappingURL=Spinner.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Spinner.web.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Spinner/Spinner.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAIjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA8B5C,wBAAgB,OAAO,CAAC,EACtB,IAAS,EACT,KAAK,EACL,SAAa,EACb,KAAK,EACL,MAAM,GACP,EAAE,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA8ClC"}
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import type { SwitchProps } from './types';
3
+ /**
4
+ * Web 端 Switch —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
5
+ * 链路会抛 `Object.keys(undefined)` TypeError(切换时打印,thumb 卡住),走 CSS
6
+ * transition 实现 backgroundColor + translateX,native 端仍走 Switch.tsx
7
+ * 的 reanimated 实现。
8
+ *
9
+ * 实现:track / thumb 仍是 RN View(尺寸 / 圆角走 RN style),把 web-only
10
+ * backgroundColor transition 通过 `style` 行内 CSS prop 注入(RN-Web 把
11
+ * style 数组合并到 element 的 inline style,我们补 `transition` 字段)。
12
+ */
13
+ export declare function Switch({ value, onChange, disabled, accessibilityLabel, testID, }: SwitchProps): React.JSX.Element;
14
+ //# sourceMappingURL=Switch.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Switch.web.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Switch/Switch.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAQ3C;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,EACrB,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,kBAAkB,EAClB,MAAM,GACP,EAAE,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAqCjC"}
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import type { ToastHostProps } from './types';
3
+ /**
4
+ * Web 端 ToastHost —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
5
+ * 链路会在 _updatePropsJS 里 Object.keys 抛 TypeError(点击就崩),走纯 React + CSS
6
+ * transition 实现 fade + slide,native 端仍走 ToastHost.tsx 的 reanimated 实现。
7
+ *
8
+ * 行为复刻 native 版:
9
+ * - opacity 0→1 + translateY 8→0 入场(motion.base ms)
10
+ * - 停留 entry.duration ms
11
+ * - opacity 1→0 + translateY 0→8 退场后 unmount
12
+ */
13
+ export declare function ToastHost({ testID, }?: ToastHostProps): React.JSX.Element | null;
14
+ //# sourceMappingURL=ToastHost.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToastHost.web.d.ts","sourceRoot":"","sources":["../../../../../../src/components/ui/Toast/ToastHost.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAK3D,OAAO,KAAK,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAEtE;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,EACxB,MAAM,GACP,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAoFhD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unif/react-native-design",
3
- "version": "0.4.33",
3
+ "version": "0.4.35",
4
4
  "description": "Unif 设计系统:theme + icons + utils + components",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -151,8 +151,9 @@
151
151
  },
152
152
  "release-it": {
153
153
  "git": {
154
- "commitMessage": "chore: release ${version}",
155
- "tagName": "v${version}"
154
+ "commitMessage": "chore: release ${version} [skip ci]",
155
+ "tagName": "v${version}",
156
+ "requireCommits": true
156
157
  },
157
158
  "npm": {
158
159
  "publish": true,
@@ -0,0 +1,57 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { PulseOptions } from './types';
3
+
4
+ /**
5
+ * Web 端 usePulse —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
6
+ * 链路会抛 `Object.keys(undefined)` TypeError(animation frame 每帧打印),pulse 卡死
7
+ * 在初始值。这里用 React state + setInterval + CSS 等价于在两个 opacity 之间循环,
8
+ * 返回 RN style({ opacity }),保持 native API 兼容。
9
+ *
10
+ * native 端走 usePulse.ts(reanimated worklet) 不动。
11
+ *
12
+ * 注意:实现走 React state 而非 CSS transition——因为 RN style 不支持 transition,
13
+ * 而 setInterval 切换 opacity 加上一个外层 div 的 CSS transition 视觉上等价
14
+ * (transition 由 ToastHost.web.tsx 那个模式接管,这里直接用 React 切值)。
15
+ */
16
+ export function usePulse({
17
+ from = 0.6,
18
+ to = 1,
19
+ duration = 700,
20
+ delay = 0,
21
+ }: PulseOptions = {}): { opacity: number } {
22
+ const [opacity, setOpacity] = useState(from);
23
+
24
+ useEffect(() => {
25
+ let cancelled = false;
26
+ let direction: 'up' | 'down' = 'up';
27
+ let start = Date.now() + delay;
28
+ let raf: number | null = null;
29
+
30
+ const tick = () => {
31
+ if (cancelled) return;
32
+ const now = Date.now();
33
+ if (now < start) {
34
+ raf = requestAnimationFrame(tick);
35
+ return;
36
+ }
37
+ const elapsed = (now - start) % duration;
38
+ const t = elapsed / duration; // 0→1
39
+ const next =
40
+ direction === 'up' ? from + (to - from) * t : to - (to - from) * t;
41
+ setOpacity(next);
42
+ if (elapsed + 16 >= duration) {
43
+ direction = direction === 'up' ? 'down' : 'up';
44
+ start = now;
45
+ }
46
+ raf = requestAnimationFrame(tick);
47
+ };
48
+ raf = requestAnimationFrame(tick);
49
+
50
+ return () => {
51
+ cancelled = true;
52
+ if (raf != null) cancelAnimationFrame(raf);
53
+ };
54
+ }, [from, to, duration, delay]);
55
+
56
+ return { opacity };
57
+ }
@@ -0,0 +1,87 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View } from 'react-native';
3
+ import { useColors } from '../../../theme';
4
+ import { createLogger } from '../../../utils/logger';
5
+ import type { SpinnerProps } from './types';
6
+
7
+ const log = createLogger('Spinner');
8
+
9
+ // React-Native-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle 链路在
10
+ // web bundle 里跑会触发 `_updatePropsJS` 里 `Object.keys(updates)` 的 worklet
11
+ // 回值打包 edge case(animation frame 每帧抛 TypeError),旋转停滞。
12
+ // 该 .web.tsx 用纯 CSS @keyframes 替代,native 端仍走 Spinner.tsx 的 reanimated 实现,
13
+ // 走 webpack `resolve.extensions = ['.web.tsx', ...]`,bundle 命中 .web 版本。
14
+ const SPIN_KEYFRAMES_ID = 'unif-spinner-spin-keyframes';
15
+
16
+ // tsconfig 不带 DOM lib(库本体是 RN),用 globalThis 兜底访问浏览器 API,
17
+ // 类型用 any 通过 strict 检查(实际运行只在 web bundle 命中本文件,document 一定有)。
18
+
19
+ const win: any = globalThis as any;
20
+
21
+ function ensureKeyframes() {
22
+ if (typeof win.document === 'undefined') return;
23
+ if (win.document.getElementById(SPIN_KEYFRAMES_ID)) return;
24
+ const style = win.document.createElement('style');
25
+ style.id = SPIN_KEYFRAMES_ID;
26
+ // ⚠️ 安全:注入 <style> 的内容会被浏览器当 CSS 解析。本字符串必须保持「静态
27
+ // 硬编码」,严禁拼接任何外部 / 用户 / props 输入(size / color 等),否则 CSS
28
+ // 注入。每实例的尺寸 / 颜色 / 时长走 RN style + node.style.animation(见下),
29
+ // 不进 keyframes —— 本函数永远只注册这一条固定 0→360deg 规则。
30
+ style.textContent =
31
+ '@keyframes unif-spinner-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
32
+ win.document.head.appendChild(style);
33
+ }
34
+
35
+ export function Spinner({
36
+ size = 18,
37
+ color,
38
+ thickness = 2,
39
+ style,
40
+ testID,
41
+ }: SpinnerProps): React.JSX.Element {
42
+ const c = useColors();
43
+ // ref 用 any —— RN View ref 在 web bundle 实际拿到 HTMLDivElement,
44
+ // RN 端拿到 View 实例,统一 any 兼容两端。
45
+
46
+ const ref = useRef<any>(null);
47
+ const stroke = color ?? c.primary;
48
+ if (!Number.isFinite(size) || (size as number) < 8) {
49
+ log.warn(`size 应为 ≥8 的有限数,传入 ${size},已钳到 8`);
50
+ }
51
+ if (Number.isFinite(thickness) && (thickness as number) <= 0) {
52
+ log.warn(`thickness 应为正数,传入 ${thickness},已 fallback 为 2`);
53
+ }
54
+ const safeSize = Number.isFinite(size) && size > 8 ? size : 8;
55
+ const safeThickness =
56
+ Number.isFinite(thickness) && thickness > 0 ? thickness : 2;
57
+
58
+ // RN-Web 把 View 渲染成 div,我们用 ref 拿到底层 DOM 节点直接挂 CSS animation
59
+ // (RN style 不支持 animation 字段;只能事后注入)。
60
+ useEffect(() => {
61
+ ensureKeyframes();
62
+ const node = ref.current;
63
+ if (node && node.style) {
64
+ node.style.animation = 'unif-spinner-spin 900ms linear infinite';
65
+ }
66
+ }, []);
67
+
68
+ return (
69
+ <View
70
+ ref={ref}
71
+ style={[
72
+ {
73
+ width: safeSize,
74
+ height: safeSize,
75
+ borderRadius: safeSize / 2,
76
+ borderWidth: safeThickness,
77
+ borderColor: c.outline,
78
+ borderTopColor: stroke,
79
+ },
80
+ style,
81
+ ]}
82
+ testID={testID}
83
+ accessibilityElementsHidden
84
+ importantForAccessibility="no-hide-descendants"
85
+ />
86
+ );
87
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { Pressable } from 'react-native-gesture-handler';
3
+ import { View } from 'react-native';
4
+ import { motion, r, useColors, useThemedStyles } from '../../../theme';
5
+ import { INSET, makeStyles, THUMB, TRACK_W } from './styles';
6
+ import type { SwitchProps } from './types';
7
+
8
+ // ─── 动画派生常量(组件私有,非 style)─────────────────────────────────────
9
+ // 把手 translateX 的 off / on 端点:左右各留 INSET 间距,
10
+ // off = INSET(2),on = trackW - INSET - thumbW(32-2-16=14)。
11
+ const THUMB_OFF_X = r(INSET);
12
+ const THUMB_ON_X = r(TRACK_W - INSET - THUMB);
13
+
14
+ /**
15
+ * Web 端 Switch —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
16
+ * 链路会抛 `Object.keys(undefined)` TypeError(切换时打印,thumb 卡住),走 CSS
17
+ * transition 实现 backgroundColor + translateX,native 端仍走 Switch.tsx
18
+ * 的 reanimated 实现。
19
+ *
20
+ * 实现:track / thumb 仍是 RN View(尺寸 / 圆角走 RN style),把 web-only
21
+ * backgroundColor transition 通过 `style` 行内 CSS prop 注入(RN-Web 把
22
+ * style 数组合并到 element 的 inline style,我们补 `transition` 字段)。
23
+ */
24
+ export function Switch({
25
+ value,
26
+ onChange,
27
+ disabled,
28
+ accessibilityLabel,
29
+ testID,
30
+ }: SwitchProps): React.JSX.Element {
31
+ const c = useColors();
32
+ const styles = useThemedStyles(makeStyles);
33
+
34
+ // 给 RN style 数组追加 web-only 字段:transition + 切换值
35
+ const trackWebStyle = {
36
+ ...styles.track,
37
+ backgroundColor: value ? c.primary : c.surfaceContainerHighest,
38
+ // RN style 不识 transition 但 RN-Web 会把它当 inline style 透传给 DOM
39
+ transitionProperty: 'background-color',
40
+ transitionDuration: `${motion.base}ms`,
41
+ transitionTimingFunction: 'ease-out',
42
+ } as unknown as object;
43
+ const thumbWebStyle = {
44
+ ...styles.thumb,
45
+ transform: [{ translateX: value ? THUMB_ON_X : THUMB_OFF_X }],
46
+ transitionProperty: 'transform',
47
+ transitionDuration: `${motion.base}ms`,
48
+ transitionTimingFunction: 'ease-out',
49
+ } as unknown as object;
50
+
51
+ return (
52
+ <Pressable
53
+ onPress={() => !disabled && onChange(!value)}
54
+ disabled={disabled}
55
+ hitSlop={6}
56
+ style={disabled ? styles.pressableDisabled : styles.pressableEnabled}
57
+ accessibilityRole="switch"
58
+ accessibilityState={{ checked: value, disabled: !!disabled }}
59
+ accessibilityLabel={accessibilityLabel}
60
+ testID={testID}
61
+ >
62
+ <View style={trackWebStyle}>
63
+ <View style={thumbWebStyle} />
64
+ </View>
65
+ </Pressable>
66
+ );
67
+ }
@@ -0,0 +1,104 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import { useColors, useThemedStyles, motion } from '../../../theme';
4
+ import { dotColorFor, makeStyles } from './styles';
5
+ import { _subs } from './toast';
6
+ import type { Subscriber, ToastEntry, ToastHostProps } from './types';
7
+
8
+ /**
9
+ * Web 端 ToastHost —— RN-Web 上 reanimated 4 + worklets 0.9.x 的 useAnimatedStyle
10
+ * 链路会在 _updatePropsJS 里 Object.keys 抛 TypeError(点击就崩),走纯 React + CSS
11
+ * transition 实现 fade + slide,native 端仍走 ToastHost.tsx 的 reanimated 实现。
12
+ *
13
+ * 行为复刻 native 版:
14
+ * - opacity 0→1 + translateY 8→0 入场(motion.base ms)
15
+ * - 停留 entry.duration ms
16
+ * - opacity 1→0 + translateY 0→8 退场后 unmount
17
+ */
18
+ export function ToastHost({
19
+ testID,
20
+ }: ToastHostProps = {}): React.JSX.Element | null {
21
+ const c = useColors();
22
+ const styles = useThemedStyles(makeStyles);
23
+ const [entry, setEntry] = useState<ToastEntry | null>(null);
24
+ const [visible, setVisible] = useState(false);
25
+ const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26
+ const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
27
+ const enterRafRef = useRef<number | null>(null);
28
+
29
+ useEffect(() => {
30
+ const sub: Subscriber = (next) => {
31
+ setEntry(next);
32
+ };
33
+ _subs.add(sub);
34
+ return () => {
35
+ _subs.delete(sub);
36
+ };
37
+ }, []);
38
+
39
+ useEffect(() => {
40
+ // entry=null:上一轮 effect 的 cleanup 已清 timer/RAF,这里无需重复。
41
+ if (!entry) return;
42
+
43
+ // cancelled 标志:entry 快速连续切换 / 组件卸载时,cleanup 置 true。RAF 与
44
+ // timer 回调执行前都检查它 —— cancelAnimationFrame 在部分浏览器不保证取消
45
+ // 已排队回调,且双 RAF 的外层回调可能在 cleanup 后仍跑并 queue 内层,光靠
46
+ // cancel 防不住。cancelled 兜底,杜绝 unmount 后 setState 警告 + 状态错位。
47
+ let cancelled = false;
48
+ // 进场:每次 entry 切换都从 visible=false 开始,确保 CSS transition 有起点
49
+ setVisible(false);
50
+ // 双 RAF 才能稳过 React commit/paint 一帧 —— 否则同一 microtask 内 visible
51
+ // 还是 true(未刷),CSS transition 没起点。
52
+ enterRafRef.current = requestAnimationFrame(() => {
53
+ if (cancelled) return;
54
+ enterRafRef.current = requestAnimationFrame(() => {
55
+ if (cancelled) return;
56
+ setVisible(true);
57
+ });
58
+ });
59
+ // 停留时长:从 setEntry 到关闭 = entry.duration ms。
60
+ dismissTimerRef.current = setTimeout(() => {
61
+ if (cancelled) return;
62
+ setVisible(false);
63
+ exitTimerRef.current = setTimeout(() => {
64
+ if (cancelled) return;
65
+ setEntry(null);
66
+ }, motion.base);
67
+ }, entry.duration);
68
+
69
+ return () => {
70
+ cancelled = true;
71
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
72
+ if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
73
+ if (enterRafRef.current != null)
74
+ cancelAnimationFrame(enterRafRef.current);
75
+ };
76
+ }, [entry]);
77
+
78
+ if (!entry) return null;
79
+
80
+ const dotColor = dotColorFor(entry.kind, c);
81
+ const animatedWebStyle: React.CSSProperties = {
82
+ opacity: visible ? 1 : 0,
83
+ transform: visible ? 'translateY(0px)' : 'translateY(8px)',
84
+ transition: `opacity ${motion.base}ms ease-out, transform ${motion.base}ms ease-out`,
85
+ };
86
+
87
+ return (
88
+ <View style={styles.host} pointerEvents="none" testID={testID}>
89
+ <div style={animatedWebStyle}>
90
+ <View style={styles.toast}>
91
+ {dotColor ? (
92
+ <View style={[styles.dot, { backgroundColor: dotColor }]} />
93
+ ) : null}
94
+ <Text
95
+ style={styles.text}
96
+ testID={testID ? `${testID}-text` : undefined}
97
+ >
98
+ {entry.message}
99
+ </Text>
100
+ </View>
101
+ </div>
102
+ </View>
103
+ );
104
+ }