@oxyhq/bloom 0.1.1 → 0.1.3

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 (69) hide show
  1. package/lib/commonjs/dialog/Dialog.web.js +5 -4
  2. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  3. package/lib/commonjs/index.js +12 -0
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/loading/Loading.js +9 -8
  6. package/lib/commonjs/loading/Loading.js.map +1 -1
  7. package/lib/commonjs/loading/SpinnerIcon.js +185 -0
  8. package/lib/commonjs/loading/SpinnerIcon.js.map +1 -0
  9. package/lib/commonjs/loading/index.js +7 -0
  10. package/lib/commonjs/loading/index.js.map +1 -1
  11. package/lib/commonjs/switch/Switch.js +134 -0
  12. package/lib/commonjs/switch/Switch.js.map +1 -0
  13. package/lib/commonjs/switch/index.js +13 -0
  14. package/lib/commonjs/switch/index.js.map +1 -0
  15. package/lib/commonjs/switch/types.js +6 -0
  16. package/lib/commonjs/switch/types.js.map +1 -0
  17. package/lib/module/dialog/Dialog.web.js +5 -4
  18. package/lib/module/dialog/Dialog.web.js.map +1 -1
  19. package/lib/module/index.js +1 -0
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/loading/Loading.js +10 -9
  22. package/lib/module/loading/Loading.js.map +1 -1
  23. package/lib/module/loading/SpinnerIcon.js +179 -0
  24. package/lib/module/loading/SpinnerIcon.js.map +1 -0
  25. package/lib/module/loading/index.js +1 -0
  26. package/lib/module/loading/index.js.map +1 -1
  27. package/lib/module/switch/Switch.js +129 -0
  28. package/lib/module/switch/Switch.js.map +1 -0
  29. package/lib/module/switch/index.js +4 -0
  30. package/lib/module/switch/index.js.map +1 -0
  31. package/lib/module/switch/types.js +4 -0
  32. package/lib/module/switch/types.js.map +1 -0
  33. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/index.d.ts +1 -0
  35. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts +15 -0
  38. package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/loading/index.d.ts +1 -0
  40. package/lib/typescript/commonjs/loading/index.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/switch/Switch.d.ts +4 -0
  42. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/switch/index.d.ts +3 -0
  44. package/lib/typescript/commonjs/switch/index.d.ts.map +1 -0
  45. package/lib/typescript/commonjs/switch/types.d.ts +15 -0
  46. package/lib/typescript/commonjs/switch/types.d.ts.map +1 -0
  47. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  48. package/lib/typescript/module/index.d.ts +1 -0
  49. package/lib/typescript/module/index.d.ts.map +1 -1
  50. package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
  51. package/lib/typescript/module/loading/SpinnerIcon.d.ts +15 -0
  52. package/lib/typescript/module/loading/SpinnerIcon.d.ts.map +1 -0
  53. package/lib/typescript/module/loading/index.d.ts +1 -0
  54. package/lib/typescript/module/loading/index.d.ts.map +1 -1
  55. package/lib/typescript/module/switch/Switch.d.ts +4 -0
  56. package/lib/typescript/module/switch/Switch.d.ts.map +1 -0
  57. package/lib/typescript/module/switch/index.d.ts +3 -0
  58. package/lib/typescript/module/switch/index.d.ts.map +1 -0
  59. package/lib/typescript/module/switch/types.d.ts +15 -0
  60. package/lib/typescript/module/switch/types.d.ts.map +1 -0
  61. package/package.json +15 -1
  62. package/src/dialog/Dialog.web.tsx +3 -1
  63. package/src/index.ts +1 -0
  64. package/src/loading/Loading.tsx +6 -5
  65. package/src/loading/SpinnerIcon.tsx +115 -0
  66. package/src/loading/index.ts +1 -0
  67. package/src/switch/Switch.tsx +117 -0
  68. package/src/switch/index.ts +2 -0
  69. package/src/switch/types.ts +15 -0
@@ -0,0 +1,15 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ export interface SwitchProps {
3
+ /** Current on/off state */
4
+ value: boolean;
5
+ /** Called when the user toggles the switch */
6
+ onValueChange: (value: boolean) => void;
7
+ /** Whether the switch is disabled */
8
+ disabled?: boolean;
9
+ /** Container style */
10
+ style?: StyleProp<ViewStyle>;
11
+ /** Size variant */
12
+ size?: 'default' | 'sm';
13
+ testID?: string;
14
+ }
15
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/switch/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,2BAA2B;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,8CAA8C;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,mBAAmB;IACnB,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4G,MAAM,OAAO,CAAC;AAMjI,OAAO,KAAK,EAAsB,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEtF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQtF,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,GACX,EAAE,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,kDAuF3C;AAED,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,KAAK,EACL,KAAK,EACL,MAAM,EACN,qBAAqB,GACtB,EAAE,gBAAgB,2CAsClB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,2CAEtD;AAED,wBAAgB,MAAM,SAErB;AAED,wBAAgB,KAAK,4CA+BpB;AAoBD,wBAAgB,QAAQ,SAEvB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
1
+ {"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4G,MAAM,OAAO,CAAC;AAOjI,OAAO,KAAK,EAAsB,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEtF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQtF,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,GACX,EAAE,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,kDAwF3C;AAED,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,KAAK,EACL,KAAK,EACL,MAAM,EACN,qBAAqB,GACtB,EAAE,gBAAgB,2CAsClB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,2CAEtD;AAED,wBAAgB,MAAM,SAErB;AAED,wBAAgB,KAAK,4CA+BpB;AAoBD,wBAAgB,QAAQ,SAEvB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
@@ -12,4 +12,5 @@ export type { ErrorBoundaryProps } from './error-boundary';
12
12
  export * from './avatar';
13
13
  export * from './loading';
14
14
  export * as PromptInput from './prompt-input';
15
+ export * from './switch';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,KAAK,WAAW,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,KAAK,WAAW,MAAM,gBAAgB,CAAC;AAC9C,cAAc,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Loading.d.ts","sourceRoot":"","sources":["../../../../src/loading/Loading.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmC,MAAM,OAAO,CAAC;AAIxD,OAAO,KAAK,EACV,YAAY,EAKb,MAAM,SAAS,CAAC;AAmNjB,eAAO,MAAM,OAAO,0CAAyB,CAAC"}
1
+ {"version":3,"file":"Loading.d.ts","sourceRoot":"","sources":["../../../../src/loading/Loading.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmC,MAAM,OAAO,CAAC;AAKxD,OAAO,KAAK,EACV,YAAY,EAKb,MAAM,SAAS,CAAC;AAmNjB,eAAO,MAAM,OAAO,0CAAyB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { type ViewStyle } from 'react-native';
3
+ interface SpinnerIconProps {
4
+ size?: number;
5
+ color?: string;
6
+ style?: ViewStyle;
7
+ }
8
+ /**
9
+ * iOS-style SVG spinner with 8 rotating rectangles and an opacity gradient trail.
10
+ * Requires react-native-svg and react-native-reanimated as peer dependencies.
11
+ * Falls back to ActivityIndicator if either is missing.
12
+ */
13
+ export declare const SpinnerIcon: React.FC<SpinnerIconProps>;
14
+ export {};
15
+ //# sourceMappingURL=SpinnerIcon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpinnerIcon.d.ts","sourceRoot":"","sources":["../../../../src/loading/SpinnerIcon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAoCvE,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAgElD,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export { Loading } from './Loading';
2
+ export { SpinnerIcon } from './SpinnerIcon';
2
3
  export type { LoadingProps, LoadingVariant, LoadingSize, SpinnerLoadingProps, TopLoadingProps, SkeletonLoadingProps, InlineLoadingProps, } from './types';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/loading/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/loading/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { SwitchProps } from './types';
3
+ export declare const Switch: React.NamedExoticComponent<SwitchProps & React.RefAttributes<import("react-native").View>>;
4
+ //# sourceMappingURL=Switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../../src/switch/Switch.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkC,MAAM,OAAO,CAAC;AAIvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgH3C,eAAO,MAAM,MAAM,4FAAwB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { Switch } from './Switch';
2
+ export type { SwitchProps } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/switch/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ export interface SwitchProps {
3
+ /** Current on/off state */
4
+ value: boolean;
5
+ /** Called when the user toggles the switch */
6
+ onValueChange: (value: boolean) => void;
7
+ /** Whether the switch is disabled */
8
+ disabled?: boolean;
9
+ /** Container style */
10
+ style?: StyleProp<ViewStyle>;
11
+ /** Size variant */
12
+ size?: 'default' | 'sm';
13
+ testID?: string;
14
+ }
15
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/switch/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,2BAA2B;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,8CAA8C;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,mBAAmB;IACnB,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
@@ -155,6 +155,17 @@
155
155
  "default": "./lib/commonjs/loading/index.js"
156
156
  }
157
157
  },
158
+ "./switch": {
159
+ "react-native": "./src/switch/index.ts",
160
+ "import": {
161
+ "types": "./lib/typescript/module/switch/index.d.ts",
162
+ "default": "./lib/module/switch/index.js"
163
+ },
164
+ "require": {
165
+ "types": "./lib/typescript/commonjs/switch/index.d.ts",
166
+ "default": "./lib/commonjs/switch/index.js"
167
+ }
168
+ },
158
169
  "./prompt-input": {
159
170
  "react-native": "./src/prompt-input/index.ts",
160
171
  "import": {
@@ -256,5 +267,8 @@
256
267
  ],
257
268
  "typescript"
258
269
  ]
270
+ },
271
+ "dependencies": {
272
+ "react-remove-scroll-bar": "^2.3.8"
259
273
  }
260
274
  }
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
2
2
  import { Pressable, Text, View, type ViewStyle } from 'react-native';
3
+ import { RemoveScrollBar } from 'react-remove-scroll-bar';
3
4
 
4
5
  import { useTheme } from '../theme/use-theme';
5
6
  import { Portal } from '../portal';
@@ -75,6 +76,7 @@ export function Outer({
75
76
  <Portal>
76
77
  <Context.Provider value={context}>
77
78
  <ClosingContext.Provider value={isClosing}>
79
+ <RemoveScrollBar />
78
80
  <Pressable
79
81
  onPress={() => close()}
80
82
  style={{
@@ -88,7 +90,7 @@ export function Outer({
88
90
  justifyContent: webOptions?.alignCenter ? 'center' : undefined,
89
91
  paddingHorizontal: 20,
90
92
  paddingVertical: '10vh' as unknown as number,
91
- overflow: 'scroll',
93
+ overflow: 'auto' as ViewStyle['overflow'],
92
94
  }}
93
95
  >
94
96
  <DialogBackdrop isClosing={isClosing} />
package/src/index.ts CHANGED
@@ -12,3 +12,4 @@ export type { ErrorBoundaryProps } from './error-boundary';
12
12
  export * from './avatar';
13
13
  export * from './loading';
14
14
  export * as PromptInput from './prompt-input';
15
+ export * from './switch';
@@ -1,7 +1,8 @@
1
1
  import React, { memo, useEffect, useMemo } from 'react';
2
- import { View, Text, ActivityIndicator, StyleSheet, type DimensionValue } from 'react-native';
2
+ import { View, Text, StyleSheet, type DimensionValue } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
+ import { SpinnerIcon } from './SpinnerIcon';
5
6
  import type {
6
7
  LoadingProps,
7
8
  SpinnerLoadingProps,
@@ -52,7 +53,7 @@ const SpinnerLoading: React.FC<SpinnerLoadingProps> = ({
52
53
 
53
54
  return (
54
55
  <View style={[styles.container, style]} testID={testID}>
55
- {spinnerIcon ?? <ActivityIndicator size={effectiveIconSize > 30 ? 'large' : 'small'} color={spinnerColor} />}
56
+ {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
56
57
  {showText && text && (
57
58
  <Text
58
59
  style={[
@@ -92,7 +93,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
92
93
  return (
93
94
  <View style={[styles.topContainer, { height: targetHeight }, style]} testID={testID}>
94
95
  <View style={[styles.topLoadingView, { height: targetHeight }]}>
95
- {spinnerIcon ?? <ActivityIndicator size={effectiveIconSize > 30 ? 'large' : 'small'} color={spinnerColor} />}
96
+ {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
96
97
  </View>
97
98
  </View>
98
99
  );
@@ -130,7 +131,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
130
131
  return (
131
132
  <Animated.View style={[styles.topContainer, containerAnimated]} testID={testID}>
132
133
  <Animated.View style={[styles.topLoadingView, { height: targetHeight }, innerAnimated, style]}>
133
- {spinnerIcon ?? <ActivityIndicator size={effectiveIconSize > 30 ? 'large' : 'small'} color={spinnerColor} />}
134
+ {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
134
135
  </Animated.View>
135
136
  </Animated.View>
136
137
  );
@@ -188,7 +189,7 @@ const InlineLoading: React.FC<InlineLoadingProps> = ({
188
189
 
189
190
  return (
190
191
  <View style={[styles.inlineContainer, style]} testID={testID}>
191
- {spinnerIcon ?? <ActivityIndicator size="small" color={spinnerColor} />}
192
+ {spinnerIcon ?? <SpinnerIcon size={SIZE_CONFIG.small.spinner} color={spinnerColor} />}
192
193
  {text && (
193
194
  <Text
194
195
  style={[
@@ -0,0 +1,115 @@
1
+ import React, { useEffect } from 'react';
2
+ import { ActivityIndicator, View, type ViewStyle } from 'react-native';
3
+
4
+ // Lazy-loaded dependencies for the SVG spinner.
5
+ // Falls back to ActivityIndicator if react-native-svg or react-native-reanimated are not installed.
6
+ type SvgModuleType = typeof import('react-native-svg');
7
+ type ReanimatedType = typeof import('react-native-reanimated');
8
+
9
+ let svgModule: SvgModuleType | null = null;
10
+ let svgModuleResolved = false;
11
+ let reanimatedModule: ReanimatedType | null = null;
12
+ let reanimatedResolved = false;
13
+
14
+ function getSvgModule(): SvgModuleType | null {
15
+ if (!svgModuleResolved) {
16
+ svgModuleResolved = true;
17
+ try {
18
+ svgModule = require('react-native-svg');
19
+ } catch {
20
+ svgModule = null;
21
+ }
22
+ }
23
+ return svgModule;
24
+ }
25
+
26
+ function getReanimated(): ReanimatedType | null {
27
+ if (!reanimatedResolved) {
28
+ reanimatedResolved = true;
29
+ try {
30
+ reanimatedModule = require('react-native-reanimated');
31
+ } catch {
32
+ reanimatedModule = null;
33
+ }
34
+ }
35
+ return reanimatedModule;
36
+ }
37
+
38
+ interface SpinnerIconProps {
39
+ size?: number;
40
+ color?: string;
41
+ style?: ViewStyle;
42
+ }
43
+
44
+ /**
45
+ * iOS-style SVG spinner with 8 rotating rectangles and an opacity gradient trail.
46
+ * Requires react-native-svg and react-native-reanimated as peer dependencies.
47
+ * Falls back to ActivityIndicator if either is missing.
48
+ */
49
+ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
50
+ color = '#005c67',
51
+ size = 26,
52
+ style,
53
+ }) => {
54
+ const svg = getSvgModule();
55
+ const reanimated = getReanimated();
56
+
57
+ if (!svg || !reanimated) {
58
+ return <ActivityIndicator size={size > 30 ? 'large' : 'small'} color={color} />;
59
+ }
60
+
61
+ const { default: Svg, Rect } = svg;
62
+ const {
63
+ default: Animated,
64
+ useAnimatedStyle,
65
+ useSharedValue,
66
+ withRepeat,
67
+ withTiming,
68
+ Easing,
69
+ } = reanimated;
70
+
71
+ // eslint-disable-next-line react-hooks/rules-of-hooks
72
+ const rotation = useSharedValue(0);
73
+
74
+ // eslint-disable-next-line react-hooks/rules-of-hooks
75
+ useEffect(() => {
76
+ rotation.value = withRepeat(
77
+ withTiming(360, { duration: 400, easing: Easing.linear }),
78
+ -1,
79
+ false
80
+ );
81
+ }, [rotation, withRepeat, withTiming, Easing]);
82
+
83
+ // eslint-disable-next-line react-hooks/rules-of-hooks
84
+ const animatedStyle = useAnimatedStyle(() => ({
85
+ transform: [{ rotate: `${rotation.value}deg` }],
86
+ }));
87
+
88
+ return (
89
+ <Animated.View
90
+ style={[
91
+ {
92
+ width: size,
93
+ height: size,
94
+ alignItems: 'center',
95
+ justifyContent: 'center',
96
+ },
97
+ animatedStyle,
98
+ style,
99
+ ]}
100
+ >
101
+ <Svg viewBox="0 0 100 100" width={size} height={size}>
102
+ <Rect fill={color} height="10" opacity="0" rx="5" ry="5" transform="rotate(-90 50 50)" width="28" x="67" y="45" />
103
+ <Rect fill={color} height="10" opacity="0.125" rx="5" ry="5" transform="rotate(-45 50 50)" width="28" x="67" y="45" />
104
+ <Rect fill={color} height="10" opacity="0.25" rx="5" ry="5" transform="rotate(0 50 50)" width="28" x="67" y="45" />
105
+ <Rect fill={color} height="10" opacity="0.375" rx="5" ry="5" transform="rotate(45 50 50)" width="28" x="67" y="45" />
106
+ <Rect fill={color} height="10" opacity="0.5" rx="5" ry="5" transform="rotate(90 50 50)" width="28" x="67" y="45" />
107
+ <Rect fill={color} height="10" opacity="0.625" rx="5" ry="5" transform="rotate(135 50 50)" width="28" x="67" y="45" />
108
+ <Rect fill={color} height="10" opacity="0.75" rx="5" ry="5" transform="rotate(180 50 50)" width="28" x="67" y="45" />
109
+ <Rect fill={color} height="10" opacity="0.875" rx="5" ry="5" transform="rotate(225 50 50)" width="28" x="67" y="45" />
110
+ </Svg>
111
+ </Animated.View>
112
+ );
113
+ };
114
+
115
+ SpinnerIcon.displayName = 'SpinnerIcon';
@@ -1,4 +1,5 @@
1
1
  export { Loading } from './Loading';
2
+ export { SpinnerIcon } from './SpinnerIcon';
2
3
  export type {
3
4
  LoadingProps,
4
5
  LoadingVariant,
@@ -0,0 +1,117 @@
1
+ import React, { memo, useEffect, useRef } from 'react';
2
+ import { Pressable, Animated } from 'react-native';
3
+
4
+ import { useTheme } from '../theme/use-theme';
5
+ import type { SwitchProps } from './types';
6
+
7
+ const TRACK = { default: { w: 44, h: 26 }, sm: { w: 36, h: 22 } } as const;
8
+ const THUMB = { default: 22, sm: 18 } as const;
9
+ const PADDING = 2;
10
+ const SQUEEZE_RATIO = 0.75; // thumb height shrinks to 75% when pressed
11
+
12
+ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, SwitchProps>(
13
+ ({ value, onValueChange, disabled, style, size = 'default', testID }, ref) => {
14
+ const theme = useTheme();
15
+ const anim = useRef(new Animated.Value(value ? 1 : 0)).current;
16
+ const pressAnim = useRef(new Animated.Value(0)).current;
17
+
18
+ useEffect(() => {
19
+ Animated.spring(anim, {
20
+ toValue: value ? 1 : 0,
21
+ useNativeDriver: false,
22
+ friction: 8,
23
+ tension: 60,
24
+ }).start();
25
+ }, [value, anim]);
26
+
27
+ const onPressIn = () => {
28
+ if (disabled) return;
29
+ Animated.spring(pressAnim, {
30
+ toValue: 1,
31
+ useNativeDriver: false,
32
+ friction: 8,
33
+ tension: 100,
34
+ }).start();
35
+ };
36
+
37
+ const onPressOut = () => {
38
+ Animated.spring(pressAnim, {
39
+ toValue: 0,
40
+ useNativeDriver: false,
41
+ friction: 8,
42
+ tension: 60,
43
+ }).start();
44
+ };
45
+
46
+ const track = TRACK[size];
47
+ const thumb = THUMB[size];
48
+ const travel = track.w - thumb - PADDING * 2;
49
+
50
+ const trackBg = anim.interpolate({
51
+ inputRange: [0, 1],
52
+ outputRange: [theme.colors.border, theme.colors.primary],
53
+ });
54
+
55
+ const thumbX = anim.interpolate({
56
+ inputRange: [0, 1],
57
+ outputRange: [PADDING, PADDING + travel],
58
+ });
59
+
60
+ const squeezedHeight = thumb * SQUEEZE_RATIO;
61
+
62
+ const thumbHeight = pressAnim.interpolate({
63
+ inputRange: [0, 1],
64
+ outputRange: [thumb, squeezedHeight],
65
+ });
66
+
67
+ const thumbRadius = pressAnim.interpolate({
68
+ inputRange: [0, 1],
69
+ outputRange: [thumb / 2, squeezedHeight / 2],
70
+ });
71
+
72
+ return (
73
+ <Pressable
74
+ ref={ref}
75
+ role="switch"
76
+ aria-checked={value}
77
+ accessibilityState={{ checked: value, disabled }}
78
+ onPress={() => !disabled && onValueChange(!value)}
79
+ onPressIn={onPressIn}
80
+ onPressOut={onPressOut}
81
+ style={[{ opacity: disabled ? 0.4 : 1 }, style]}
82
+ hitSlop={4}
83
+ testID={testID}
84
+ >
85
+ <Animated.View
86
+ style={{
87
+ width: track.w,
88
+ height: track.h,
89
+ borderRadius: track.h / 2,
90
+ backgroundColor: trackBg,
91
+ justifyContent: 'center',
92
+ alignItems: 'flex-start',
93
+ }}
94
+ >
95
+ <Animated.View
96
+ style={{
97
+ width: thumb,
98
+ height: thumbHeight,
99
+ borderRadius: thumbRadius,
100
+ backgroundColor: '#fff',
101
+ transform: [{ translateX: thumbX }],
102
+ shadowColor: '#000',
103
+ shadowOffset: { width: 0, height: 2 },
104
+ shadowOpacity: 0.15,
105
+ shadowRadius: 3,
106
+ elevation: 3,
107
+ }}
108
+ />
109
+ </Animated.View>
110
+ </Pressable>
111
+ );
112
+ }
113
+ );
114
+
115
+ SwitchComponent.displayName = 'Switch';
116
+
117
+ export const Switch = memo(SwitchComponent);
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch';
2
+ export type { SwitchProps } from './types';
@@ -0,0 +1,15 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export interface SwitchProps {
4
+ /** Current on/off state */
5
+ value: boolean;
6
+ /** Called when the user toggles the switch */
7
+ onValueChange: (value: boolean) => void;
8
+ /** Whether the switch is disabled */
9
+ disabled?: boolean;
10
+ /** Container style */
11
+ style?: StyleProp<ViewStyle>;
12
+ /** Size variant */
13
+ size?: 'default' | 'sm';
14
+ testID?: string;
15
+ }