react-native-tinyui 1.1.1 → 3.0.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 (76) hide show
  1. package/README.md +140 -32
  2. package/ios/Core/Tinyui.mm +18 -0
  3. package/ios/Core/TinyuiComponentClasses.mm +7 -0
  4. package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +0 -1
  5. package/ios/SFSymbol/TinyuiSFSymbolEffects.swift +184 -0
  6. package/ios/SFSymbol/TinyuiSFSymbolImage.swift +103 -0
  7. package/ios/SFSymbol/TinyuiSFSymbolProvider.swift +135 -0
  8. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +4 -2
  9. package/ios/SFSymbol/TinyuiSFSymbolView.mm +14 -122
  10. package/ios/TipKit/TinyuiTip.swift +16 -0
  11. package/ios/TipKit/TinyuiTipPopoverProvider.swift +66 -135
  12. package/ios/TipKit/TinyuiTipPopoverView.mm +0 -18
  13. package/ios/TipKit/TinyuiTipView.h +8 -0
  14. package/ios/TipKit/TinyuiTipView.mm +93 -0
  15. package/ios/TipKit/TinyuiTipViewProvider.swift +140 -0
  16. package/ios/TipKit/TinyuiTipViewShadowNode.h +48 -0
  17. package/ios/TipKit/TinyuiTipsRuntime.swift +16 -74
  18. package/lib/module/NativeTinyui.js.map +1 -1
  19. package/lib/module/SFSymbol/index.js +0 -4
  20. package/lib/module/SFSymbol/index.js.map +1 -1
  21. package/lib/module/SFSymbol/options.js +0 -2
  22. package/lib/module/SFSymbol/options.js.map +1 -1
  23. package/lib/module/TipKit/Popover.js +0 -22
  24. package/lib/module/TipKit/Popover.js.map +1 -1
  25. package/lib/module/TipKit/TipPopoverNativeComponent.ts +0 -6
  26. package/lib/module/TipKit/TipViewNativeComponent.ts +28 -0
  27. package/lib/module/TipKit/View.js +62 -0
  28. package/lib/module/TipKit/View.js.map +1 -0
  29. package/lib/module/TipKit/index.js +11 -0
  30. package/lib/module/TipKit/index.js.map +1 -1
  31. package/lib/module/TipKit/validation.js +4 -4
  32. package/lib/module/TipKit/validation.js.map +1 -1
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/typescript/src/LiquidGlassText/types.d.ts +1 -1
  35. package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
  36. package/lib/typescript/src/NativeTinyui.d.ts +1 -0
  37. package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
  38. package/lib/typescript/src/SFSymbol/index.d.ts +1 -1
  39. package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -1
  40. package/lib/typescript/src/SFSymbol/options.d.ts +0 -2
  41. package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -1
  42. package/lib/typescript/src/SFSymbol/types.d.ts +1 -5
  43. package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -1
  44. package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -1
  45. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +0 -9
  46. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -1
  47. package/lib/typescript/src/TipKit/TipViewNativeComponent.d.ts +29 -0
  48. package/lib/typescript/src/TipKit/TipViewNativeComponent.d.ts.map +1 -0
  49. package/lib/typescript/src/TipKit/View.d.ts +3 -0
  50. package/lib/typescript/src/TipKit/View.d.ts.map +1 -0
  51. package/lib/typescript/src/TipKit/index.d.ts +8 -1
  52. package/lib/typescript/src/TipKit/index.d.ts.map +1 -1
  53. package/lib/typescript/src/TipKit/types.d.ts +17 -15
  54. package/lib/typescript/src/TipKit/types.d.ts.map +1 -1
  55. package/lib/typescript/src/TipKit/validation.d.ts +2 -2
  56. package/lib/typescript/src/TipKit/validation.d.ts.map +1 -1
  57. package/lib/typescript/src/index.d.ts +2 -2
  58. package/lib/typescript/src/index.d.ts.map +1 -1
  59. package/package.json +4 -1
  60. package/src/LiquidGlassText/types.ts +1 -1
  61. package/src/NativeTinyui.ts +1 -0
  62. package/src/SFSymbol/index.tsx +0 -5
  63. package/src/SFSymbol/options.ts +0 -2
  64. package/src/SFSymbol/types.ts +1 -18
  65. package/src/TipKit/Popover.tsx +1 -21
  66. package/src/TipKit/TipPopoverNativeComponent.ts +0 -6
  67. package/src/TipKit/TipViewNativeComponent.ts +28 -0
  68. package/src/TipKit/View.tsx +64 -0
  69. package/src/TipKit/index.ts +13 -1
  70. package/src/TipKit/types.ts +19 -20
  71. package/src/TipKit/validation.ts +8 -5
  72. package/src/index.tsx +1 -2
  73. package/ios/SFSymbol/TinyuiSFSymbolEffects.h +0 -9
  74. package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +0 -180
  75. package/ios/SFSymbol/TinyuiSFSymbolImage.h +0 -8
  76. package/ios/SFSymbol/TinyuiSFSymbolImage.mm +0 -141
@@ -15,12 +15,6 @@ export interface NativeTipPopoverProps extends ViewProps {
15
15
  maxDisplayCount?: CodegenTypes.Int32;
16
16
  ignoresDisplayFrequency?: boolean;
17
17
  arrowEdge?: string;
18
- onTipStatusChange?: DirectEventHandler<
19
- Readonly<{ tipId: string; status: string; reason: string }>
20
- >;
21
- onTipVisibleChange?: DirectEventHandler<
22
- Readonly<{ tipId: string; visible: boolean }>
23
- >;
24
18
  onTipAction?: DirectEventHandler<
25
19
  Readonly<{ tipId: string; id: string; title: string }>
26
20
  >;
@@ -0,0 +1,28 @@
1
+ import {
2
+ codegenNativeComponent,
3
+ type CodegenTypes,
4
+ type ViewProps,
5
+ } from 'react-native';
6
+ import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
7
+
8
+ export interface NativeTipViewProps extends ViewProps {
9
+ tipId: string;
10
+ title: string;
11
+ message?: string;
12
+ systemImage?: string;
13
+ actions?: ReadonlyArray<Readonly<{ id: string; title: string }>>;
14
+ enabled?: boolean;
15
+ maxDisplayCount?: CodegenTypes.Int32;
16
+ ignoresDisplayFrequency?: boolean;
17
+ arrowEdge?: string;
18
+ onTipAction?: DirectEventHandler<
19
+ Readonly<{ tipId: string; id: string; title: string }>
20
+ >;
21
+ onTipError?: DirectEventHandler<
22
+ Readonly<{ tipId: string; code: string; message: string }>
23
+ >;
24
+ }
25
+
26
+ export default codegenNativeComponent<NativeTipViewProps>('TinyuiTipView', {
27
+ excludedPlatforms: ['android'],
28
+ });
@@ -0,0 +1,64 @@
1
+ import { forwardRef, type ComponentRef } from 'react';
2
+ import type { View as ReactNativeView } from 'react-native';
3
+
4
+ import { assertComponentEnabled } from '../utils';
5
+ import NativeTipView from './TipViewNativeComponent';
6
+ import type { TipViewProps } from './types';
7
+ import { validateTip } from './validation';
8
+
9
+ export const View = forwardRef<
10
+ ComponentRef<typeof ReactNativeView>,
11
+ TipViewProps
12
+ >(function TipViewRoot(props, ref) {
13
+ assertComponentEnabled('TipKit');
14
+ validateTip(props, 'TipKit.View');
15
+ const {
16
+ tipId,
17
+ title,
18
+ message,
19
+ systemImage,
20
+ actions,
21
+ enabled = true,
22
+ maxDisplayCount,
23
+ ignoresDisplayFrequency = false,
24
+ arrowEdge = 'none',
25
+ onActionPress,
26
+ onError,
27
+ ...viewProps
28
+ } = props;
29
+
30
+ return (
31
+ <NativeTipView
32
+ {...viewProps}
33
+ children={undefined}
34
+ ref={ref}
35
+ collapsable={false}
36
+ tipId={tipId}
37
+ title={title}
38
+ message={message}
39
+ systemImage={systemImage}
40
+ actions={actions}
41
+ enabled={enabled}
42
+ maxDisplayCount={maxDisplayCount ?? 0}
43
+ ignoresDisplayFrequency={ignoresDisplayFrequency}
44
+ arrowEdge={arrowEdge}
45
+ onTipAction={({ nativeEvent }) => {
46
+ if (nativeEvent.tipId === tipId) {
47
+ onActionPress?.({ id: nativeEvent.id, title: nativeEvent.title });
48
+ }
49
+ }}
50
+ onTipError={({ nativeEvent }) => {
51
+ if (nativeEvent.tipId !== tipId) return;
52
+ const error = {
53
+ code: nativeEvent.code,
54
+ message: nativeEvent.message,
55
+ };
56
+ if (onError) onError(error);
57
+ else
58
+ console.warn(`[TinyUI] TipKit.View ${error.code}: ${error.message}`);
59
+ }}
60
+ />
61
+ );
62
+ });
63
+
64
+ View.displayName = 'TipKit.View';
@@ -2,9 +2,21 @@ import { assertComponentEnabled, getNativeTinyui } from '../utils';
2
2
  import type { TipInvalidationReason, TipKitConfiguration } from './types';
3
3
  import { validateTipId } from './validation';
4
4
  import { Popover } from './Popover';
5
+ import { View } from './View';
5
6
 
6
7
  export const TipKit = {
7
8
  Popover,
9
+ View,
10
+ /**
11
+ * Clear all persisted tip, event and parameter records for testing.
12
+ * Call and await this before configure(); rejects if already configured,
13
+ * including after Fast Refresh in the same app process.
14
+ */
15
+ async resetDatastore(): Promise<void> {
16
+ assertComponentEnabled('TipKit');
17
+ await getNativeTinyui().resetTipsDatastore();
18
+ },
19
+
8
20
  /** Configure TipKit once before mounting tips. Repeating the same options is safe. */
9
21
  async configure({
10
22
  displayFrequency = 'daily',
@@ -43,5 +55,5 @@ export type {
43
55
  TipInvalidationReason,
44
56
  TipKitConfiguration,
45
57
  TipPopoverProps,
46
- TipStatus,
58
+ TipViewProps,
47
59
  } from './types';
@@ -11,19 +11,10 @@ export interface TipKitConfiguration {
11
11
 
12
12
  export type TipInvalidationReason = 'actionPerformed' | 'tipClosed';
13
13
 
14
- export type TipStatus =
15
- | { status: 'pending' | 'available' }
16
- | {
17
- status: 'invalidated';
18
- reason:
19
- | TipInvalidationReason
20
- | 'displayCountExceeded'
21
- | 'displayDurationExceeded'
22
- | 'unknown';
23
- };
24
-
25
14
  export interface TipAction {
15
+ /** Unique nonempty identity within this tip, returned by `onActionPress`. */
26
16
  id: string;
17
+ /** Nonempty button label, already localized by the app. */
27
18
  title: string;
28
19
  }
29
20
 
@@ -32,26 +23,34 @@ export interface TipError {
32
23
  message: string;
33
24
  }
34
25
 
35
- export interface TipPopoverProps extends Omit<ViewProps, 'children'> {
36
- /** Anchor content. Its existing touch handlers are preserved. */
37
- children: ReactNode;
26
+ export interface TipContentProps extends Omit<ViewProps, 'children'> {
38
27
  /** Stable identity used by TipKit to persist this tip's history. */
39
28
  tipId: string;
40
29
  title: string;
41
30
  message?: string;
42
31
  systemImage?: string;
32
+ /** Native action buttons, each with its own id and title; TipKit controls their layout. */
43
33
  actions?: readonly TipAction[];
44
34
  /** Allows presentation when TipKit is also eligible. Defaults to true. */
45
35
  enabled?: boolean;
46
36
  /** Omit to leave the number of presentations unlimited. */
47
37
  maxDisplayCount?: number;
48
38
  ignoresDisplayFrequency?: boolean;
49
- /** Preferred arrow edge on the bubble; UIKit may adapt to available space. */
50
- arrowEdge?: 'top' | 'bottom' | 'leading' | 'trailing' | 'auto';
51
- /** TipKit eligibility, independent of whether the bubble is on screen. */
52
- onStatusChange?: (status: TipStatus) => void;
53
- onVisibleChange?: (visible: boolean) => void;
54
- /** Actions do not automatically invalidate the tip. */
39
+ /** Receives the tapped button's id and title; does not automatically invalidate the tip. */
55
40
  onActionPress?: (action: TipAction) => void;
56
41
  onError?: (error: TipError) => void;
57
42
  }
43
+
44
+ export interface TipPopoverProps extends TipContentProps {
45
+ /** Anchor content. Its existing touch handlers are preserved. */
46
+ children: ReactNode;
47
+ /** Preferred arrow edge on the bubble; UIKit may adapt to available space. */
48
+ arrowEdge?: 'top' | 'bottom' | 'leading' | 'trailing' | 'auto';
49
+ }
50
+
51
+ export interface TipViewProps extends TipContentProps {
52
+ /** Inline tips render native content and do not take anchor children. */
53
+ children?: never;
54
+ /** Optional arrow on the inline view. Defaults to no arrow. */
55
+ arrowEdge?: 'top' | 'bottom' | 'leading' | 'trailing' | 'none';
56
+ }
@@ -1,4 +1,4 @@
1
- import type { TipPopoverProps } from './types';
1
+ import type { TipContentProps } from './types';
2
2
 
3
3
  export function validateTipId(tipId: string): void {
4
4
  if (typeof tipId !== 'string' || tipId.trim().length === 0) {
@@ -6,10 +6,13 @@ export function validateTipId(tipId: string): void {
6
6
  }
7
7
  }
8
8
 
9
- export function validateTip(props: TipPopoverProps): void {
9
+ export function validateTip(
10
+ props: TipContentProps,
11
+ component = 'TipKit.Popover'
12
+ ): void {
10
13
  validateTipId(props.tipId);
11
14
  if (typeof props.title !== 'string' || props.title.trim().length === 0) {
12
- throw new Error('[TinyUI] TipKit.Popover title must be a nonempty string.');
15
+ throw new Error(`[TinyUI] ${component} title must be a nonempty string.`);
13
16
  }
14
17
  if (
15
18
  props.maxDisplayCount !== undefined &&
@@ -18,7 +21,7 @@ export function validateTip(props: TipPopoverProps): void {
18
21
  props.maxDisplayCount > 2147483647)
19
22
  ) {
20
23
  throw new Error(
21
- '[TinyUI] TipKit.Popover maxDisplayCount must be a positive 32-bit integer.'
24
+ `[TinyUI] ${component} maxDisplayCount must be a positive 32-bit integer.`
22
25
  );
23
26
  }
24
27
  const ids = new Set<string>();
@@ -31,7 +34,7 @@ export function validateTip(props: TipPopoverProps): void {
31
34
  ids.has(action.id)
32
35
  ) {
33
36
  throw new Error(
34
- '[TinyUI] TipKit.Popover actions need unique nonempty ids and nonempty titles.'
37
+ `[TinyUI] ${component} actions need unique nonempty ids and nonempty titles.`
35
38
  );
36
39
  }
37
40
  ids.add(action.id);
package/src/index.tsx CHANGED
@@ -6,8 +6,8 @@ export type {
6
6
  TipError,
7
7
  TipInvalidationReason,
8
8
  TipPopoverProps,
9
+ TipViewProps,
9
10
  TipKitConfiguration,
10
- TipStatus,
11
11
  } from './TipKit';
12
12
  export type {
13
13
  MenuActionOption,
@@ -36,7 +36,6 @@ export type {
36
36
  SFSymbolProps,
37
37
  SFSymbolRenderingMode,
38
38
  SFSymbolScale,
39
- SFSymbolTextStyle,
40
39
  SFSymbolWeight,
41
40
  } from './SFSymbol';
42
41
 
@@ -1,9 +0,0 @@
1
- #pragma once
2
-
3
- #import <Symbols/Symbols.h>
4
-
5
- NS_ASSUME_NONNULL_BEGIN
6
- NSSymbolEffect * _Nullable TinyuiSFSymbolEffect(NSDictionary *configuration);
7
- NSSymbolEffectOptions *TinyuiSFSymbolOptions(NSDictionary * _Nullable configuration);
8
- NSSymbolContentTransition * _Nullable TinyuiSFSymbolTransition(NSDictionary *configuration);
9
- NS_ASSUME_NONNULL_END
@@ -1,180 +0,0 @@
1
- #if TINYUI_FEATURE_SF_SYMBOL
2
-
3
- #import "TinyuiSFSymbolEffects.h"
4
- #include <cmath>
5
-
6
- // Keep the concrete receiver types so the compiler checks every Symbols API.
7
- #define TINYUI_LAYER_SCOPE(effect) \
8
- if ([scope isEqual:@"byLayer"]) effect = [effect effectWithByLayer]; \
9
- if ([scope isEqual:@"wholeSymbol"]) effect = [effect effectWithWholeSymbol];
10
-
11
- NSSymbolEffect *TinyuiSFSymbolEffect(NSDictionary *configuration)
12
- {
13
- NSString *type = configuration[@"type"];
14
- NSString *scope = configuration[@"scope"];
15
- NSString *direction = configuration[@"direction"];
16
- if ([type isEqual:@"pulse"]) {
17
- NSSymbolPulseEffect *effect = NSSymbolPulseEffect.effect;
18
- TINYUI_LAYER_SCOPE(effect);
19
- return effect;
20
- }
21
- if ([type isEqual:@"bounce"]) {
22
- NSSymbolBounceEffect *effect = NSSymbolBounceEffect.effect;
23
- if ([direction isEqual:@"up"]) effect = NSSymbolBounceEffect.bounceUpEffect;
24
- if ([direction isEqual:@"down"]) effect = NSSymbolBounceEffect.bounceDownEffect;
25
- TINYUI_LAYER_SCOPE(effect);
26
- return effect;
27
- }
28
- if ([type isEqual:@"scale"]) {
29
- NSSymbolScaleEffect *effect = NSSymbolScaleEffect.effect;
30
- if ([direction isEqual:@"up"]) effect = NSSymbolScaleEffect.scaleUpEffect;
31
- if ([direction isEqual:@"down"]) effect = NSSymbolScaleEffect.scaleDownEffect;
32
- TINYUI_LAYER_SCOPE(effect);
33
- return effect;
34
- }
35
- if ([type isEqual:@"appear"]) {
36
- NSSymbolAppearEffect *effect = NSSymbolAppearEffect.effect;
37
- if ([direction isEqual:@"up"]) effect = NSSymbolAppearEffect.appearUpEffect;
38
- if ([direction isEqual:@"down"]) effect = NSSymbolAppearEffect.appearDownEffect;
39
- TINYUI_LAYER_SCOPE(effect);
40
- return effect;
41
- }
42
- if ([type isEqual:@"disappear"]) {
43
- NSSymbolDisappearEffect *effect = NSSymbolDisappearEffect.effect;
44
- if ([direction isEqual:@"up"]) effect = NSSymbolDisappearEffect.disappearUpEffect;
45
- if ([direction isEqual:@"down"]) effect = NSSymbolDisappearEffect.disappearDownEffect;
46
- TINYUI_LAYER_SCOPE(effect);
47
- return effect;
48
- }
49
- if ([type isEqual:@"variableColor"]) {
50
- NSSymbolVariableColorEffect *effect = NSSymbolVariableColorEffect.effect;
51
- if ([configuration[@"iteration"] isEqual:@"iterative"]) effect = [effect effectWithIterative];
52
- if ([configuration[@"iteration"] isEqual:@"cumulative"]) effect = [effect effectWithCumulative];
53
- if ([configuration[@"reversing"] isKindOfClass:NSNumber.class]) {
54
- effect = [configuration[@"reversing"] boolValue]
55
- ? [effect effectWithReversing] : [effect effectWithNonReversing];
56
- }
57
- if ([configuration[@"inactiveLayers"] isEqual:@"hide"]) effect = [effect effectWithHideInactiveLayers];
58
- if ([configuration[@"inactiveLayers"] isEqual:@"dim"]) effect = [effect effectWithDimInactiveLayers];
59
- return effect;
60
- }
61
- if (@available(iOS 18.0, *)) {
62
- if ([type isEqual:@"wiggle"]) {
63
- NSSymbolWiggleEffect *effect = NSSymbolWiggleEffect.effect;
64
- if ([direction isEqual:@"up"]) effect = NSSymbolWiggleEffect.wiggleUpEffect;
65
- if ([direction isEqual:@"down"]) effect = NSSymbolWiggleEffect.wiggleDownEffect;
66
- if ([direction isEqual:@"left"]) effect = NSSymbolWiggleEffect.wiggleLeftEffect;
67
- if ([direction isEqual:@"right"]) effect = NSSymbolWiggleEffect.wiggleRightEffect;
68
- if ([direction isEqual:@"forward"]) effect = NSSymbolWiggleEffect.wiggleForwardEffect;
69
- if ([direction isEqual:@"backward"]) effect = NSSymbolWiggleEffect.wiggleBackwardEffect;
70
- if ([direction isEqual:@"clockwise"]) effect = NSSymbolWiggleEffect.wiggleClockwiseEffect;
71
- if ([direction isEqual:@"counterClockwise"]) effect = NSSymbolWiggleEffect.wiggleCounterClockwiseEffect;
72
- id angle = configuration[@"angle"];
73
- if ([angle isKindOfClass:NSNumber.class] && std::isfinite([angle doubleValue])) {
74
- effect = [NSSymbolWiggleEffect wiggleCustomAngleEffect:[angle doubleValue]];
75
- }
76
- TINYUI_LAYER_SCOPE(effect);
77
- return effect;
78
- }
79
- if ([type isEqual:@"rotate"]) {
80
- NSSymbolRotateEffect *effect = NSSymbolRotateEffect.effect;
81
- if ([direction isEqual:@"clockwise"]) effect = NSSymbolRotateEffect.rotateClockwiseEffect;
82
- if ([direction isEqual:@"counterClockwise"]) effect = NSSymbolRotateEffect.rotateCounterClockwiseEffect;
83
- TINYUI_LAYER_SCOPE(effect);
84
- return effect;
85
- }
86
- if ([type isEqual:@"breathe"]) {
87
- NSSymbolBreatheEffect *effect = NSSymbolBreatheEffect.effect;
88
- if ([configuration[@"style"] isEqual:@"plain"]) effect = NSSymbolBreatheEffect.breathePlainEffect;
89
- if ([configuration[@"style"] isEqual:@"pulse"]) effect = NSSymbolBreatheEffect.breathePulseEffect;
90
- TINYUI_LAYER_SCOPE(effect);
91
- return effect;
92
- }
93
- }
94
- if (@available(iOS 26.0, *)) {
95
- if ([type isEqual:@"drawOn"]) {
96
- NSSymbolDrawOnEffect *effect = NSSymbolDrawOnEffect.effect;
97
- TINYUI_LAYER_SCOPE(effect);
98
- if ([scope isEqual:@"individually"]) effect = [effect effectWithIndividually];
99
- return effect;
100
- }
101
- if ([type isEqual:@"drawOff"]) {
102
- NSSymbolDrawOffEffect *effect = NSSymbolDrawOffEffect.effect;
103
- TINYUI_LAYER_SCOPE(effect);
104
- if ([scope isEqual:@"individually"]) effect = [effect effectWithIndividually];
105
- if ([configuration[@"reversed"] isKindOfClass:NSNumber.class]) {
106
- effect = [configuration[@"reversed"] boolValue]
107
- ? [effect effectWithReversed] : [effect effectWithNonReversed];
108
- }
109
- return effect;
110
- }
111
- }
112
- return nil;
113
- }
114
-
115
- #undef TINYUI_LAYER_SCOPE
116
-
117
- NSSymbolEffectOptions *TinyuiSFSymbolOptions(NSDictionary *configuration)
118
- {
119
- NSSymbolEffectOptions *options = NSSymbolEffectOptions.options;
120
- if (![configuration isKindOfClass:NSDictionary.class]) return options;
121
- id speedValue = configuration[@"speed"];
122
- double speed = [speedValue isKindOfClass:NSNumber.class] ? [speedValue doubleValue] : 1;
123
- if (std::isfinite(speed) && speed > 0) options = [options optionsWithSpeed:speed];
124
- id repeat = configuration[@"repeat"];
125
- if ([repeat isEqual:@NO]) return [options optionsWithNonRepeating];
126
- BOOL countProvided = [repeat isKindOfClass:NSNumber.class];
127
- // JS validates safe integers; cap the conversion at the native integer range.
128
- NSInteger count = countProvided ? (NSInteger)fmin([repeat doubleValue], (double)NSIntegerMax / 2) : 1;
129
- count = MAX(1, count);
130
- id behaviorValue = configuration[@"repeatBehavior"];
131
- NSString *behavior = [behaviorValue isKindOfClass:NSString.class] ? behaviorValue : nil;
132
- id delayValue = configuration[@"repeatDelay"];
133
- BOOL hasDelay = [delayValue isKindOfClass:NSNumber.class];
134
- double delay = hasDelay ? [delayValue doubleValue] : 0;
135
- if (!std::isfinite(delay) || delay < 0) delay = 0;
136
- BOOL forever = [repeat isEqual:@"forever"] || behavior != nil || hasDelay;
137
-
138
- if (@available(iOS 18.0, *)) {
139
- NSSymbolEffectOptionsRepeatBehavior *repetition = nil;
140
- if ([behavior isEqual:@"continuous"]) {
141
- repetition = NSSymbolEffectOptionsRepeatBehavior.behaviorContinuous;
142
- } else if (countProvided) {
143
- repetition = hasDelay
144
- ? [NSSymbolEffectOptionsRepeatBehavior behaviorPeriodicWithCount:count delay:delay]
145
- : [NSSymbolEffectOptionsRepeatBehavior behaviorPeriodicWithCount:count];
146
- } else if (forever) {
147
- repetition = hasDelay
148
- ? [NSSymbolEffectOptionsRepeatBehavior behaviorPeriodicWithDelay:delay]
149
- : NSSymbolEffectOptionsRepeatBehavior.behaviorPeriodic;
150
- }
151
- if (repetition != nil) options = [options optionsWithRepeatBehavior:repetition];
152
- } else {
153
- if (countProvided) options = [options optionsWithRepeatCount:count];
154
- else if (forever) options = [options optionsWithRepeating];
155
- }
156
- return options;
157
- }
158
-
159
- NSSymbolContentTransition *TinyuiSFSymbolTransition(NSDictionary *configuration)
160
- {
161
- NSString *type = configuration[@"type"];
162
- if ([type isEqual:@"automatic"]) return NSSymbolAutomaticContentTransition.transition;
163
- if (![type isEqual:@"replace"] && ![type isEqual:@"magicReplace"]) return nil;
164
-
165
- NSSymbolReplaceContentTransition *transition = NSSymbolReplaceContentTransition.transition;
166
- NSString *direction = configuration[@"direction"];
167
- if ([direction isEqual:@"downUp"]) transition = NSSymbolReplaceContentTransition.replaceDownUpTransition;
168
- if ([direction isEqual:@"upUp"]) transition = NSSymbolReplaceContentTransition.replaceUpUpTransition;
169
- if ([direction isEqual:@"offUp"]) transition = NSSymbolReplaceContentTransition.replaceOffUpTransition;
170
- if ([configuration[@"scope"] isEqual:@"byLayer"]) transition = [transition transitionWithByLayer];
171
- if ([configuration[@"scope"] isEqual:@"wholeSymbol"]) transition = [transition transitionWithWholeSymbol];
172
- if (@available(iOS 18.0, *)) {
173
- if ([type isEqual:@"magicReplace"]) {
174
- return [NSSymbolReplaceContentTransition magicTransitionWithFallback:transition];
175
- }
176
- }
177
- return transition;
178
- }
179
-
180
- #endif
@@ -1,8 +0,0 @@
1
- #pragma once
2
-
3
- #import <UIKit/UIKit.h>
4
-
5
- NS_ASSUME_NONNULL_BEGIN
6
- // Shared by Fabric measurement and the mounted view so their metrics match.
7
- UIImage * _Nullable TinyuiSFSymbolImage(NSDictionary *configuration, BOOL includeColors);
8
- NS_ASSUME_NONNULL_END
@@ -1,141 +0,0 @@
1
- #if TINYUI_FEATURE_SF_SYMBOL
2
-
3
- #import "TinyuiSFSymbolImage.h"
4
- #import <React/RCTConvert.h>
5
- #include <cmath>
6
-
7
- static UIImageSymbolWeight symbolWeight(NSString *value)
8
- {
9
- NSDictionary *weights = @{
10
- @"ultraLight": @(UIImageSymbolWeightUltraLight), @"thin": @(UIImageSymbolWeightThin),
11
- @"light": @(UIImageSymbolWeightLight), @"regular": @(UIImageSymbolWeightRegular),
12
- @"medium": @(UIImageSymbolWeightMedium), @"semibold": @(UIImageSymbolWeightSemibold),
13
- @"bold": @(UIImageSymbolWeightBold), @"heavy": @(UIImageSymbolWeightHeavy),
14
- @"black": @(UIImageSymbolWeightBlack),
15
- };
16
- return (UIImageSymbolWeight)[weights[value ?: @""] integerValue];
17
- }
18
-
19
- static UIImageSymbolScale symbolScale(NSString *value)
20
- {
21
- if ([value isEqual:@"small"]) return UIImageSymbolScaleSmall;
22
- if ([value isEqual:@"medium"]) return UIImageSymbolScaleMedium;
23
- if ([value isEqual:@"large"]) return UIImageSymbolScaleLarge;
24
- if ([value isEqual:@"unspecified"]) return UIImageSymbolScaleUnspecified;
25
- return UIImageSymbolScaleDefault;
26
- }
27
-
28
- static UIFontTextStyle textStyle(NSString *value)
29
- {
30
- return @{
31
- @"extraLargeTitle": UIFontTextStyleExtraLargeTitle,
32
- @"extraLargeTitle2": UIFontTextStyleExtraLargeTitle2,
33
- @"largeTitle": UIFontTextStyleLargeTitle, @"title1": UIFontTextStyleTitle1,
34
- @"title2": UIFontTextStyleTitle2, @"title3": UIFontTextStyleTitle3,
35
- @"headline": UIFontTextStyleHeadline, @"subheadline": UIFontTextStyleSubheadline,
36
- @"body": UIFontTextStyleBody, @"callout": UIFontTextStyleCallout,
37
- @"footnote": UIFontTextStyleFootnote, @"caption1": UIFontTextStyleCaption1,
38
- @"caption2": UIFontTextStyleCaption2,
39
- }[value ?: @""];
40
- }
41
-
42
- UIImage *TinyuiSFSymbolImage(NSDictionary *configuration, BOOL includeColors)
43
- {
44
- if (![configuration isKindOfClass:NSDictionary.class]) return nil;
45
- NSString *name = configuration[@"name"];
46
- if (![name isKindOfClass:NSString.class] || name.length == 0) return nil;
47
-
48
- CGFloat size = [configuration[@"size"] doubleValue];
49
- if (!std::isfinite(size) || size <= 0) size = 17;
50
- UIImageSymbolWeight weight = symbolWeight(configuration[@"weight"]);
51
- UIImageSymbolScale scale = symbolScale(configuration[@"scale"]);
52
- UIFontTextStyle style = textStyle(configuration[@"textStyle"]);
53
- if (style != nil) {
54
- // RN supplies the scale explicitly; use unscaled metrics here to avoid
55
- // applying Dynamic Type twice and to keep Yoga and UIKit in agreement.
56
- UITraitCollection *traits = [UITraitCollection
57
- traitCollectionWithPreferredContentSizeCategory:UIContentSizeCategoryLarge];
58
- UIFont *font = [UIFont preferredFontForTextStyle:style compatibleWithTraitCollection:traits];
59
- size = font.pointSize;
60
- if (weight == UIImageSymbolWeightUnspecified) {
61
- NSNumber *fontWeight = font.fontDescriptor.fontAttributes[UIFontDescriptorTraitsAttribute][UIFontWeightTrait];
62
- weight = UIImageSymbolWeightForFontWeight(fontWeight.doubleValue);
63
- }
64
- }
65
- double fontScale = [configuration[@"fontScale"] doubleValue];
66
- if (std::isfinite(fontScale) && fontScale > 0) size *= fontScale;
67
-
68
- UIImageSymbolConfiguration *symbol = [UIImageSymbolConfiguration
69
- configurationWithPointSize:size weight:weight scale:scale];
70
- NSString *family = configuration[@"fontFamily"];
71
- if ([family isKindOfClass:NSString.class] && family.length > 0) {
72
- UIFont *font = [UIFont fontWithName:family size:size];
73
- if (font != nil) {
74
- symbol = [UIImageSymbolConfiguration configurationWithFont:font scale:scale];
75
- if (weight != UIImageSymbolWeightUnspecified) {
76
- symbol = [symbol configurationByApplyingConfiguration:
77
- [UIImageSymbolConfiguration configurationWithWeight:weight]];
78
- }
79
- }
80
- }
81
-
82
- if (includeColors) {
83
- NSString *mode = configuration[@"renderingMode"];
84
- UIColor *color = [RCTConvert UIColor:configuration[@"color"]] ?: UIColor.labelColor;
85
- UIImageSymbolConfiguration *colors = nil;
86
- if ([mode isEqual:@"monochrome"]) {
87
- colors = UIImageSymbolConfiguration.configurationPreferringMonochrome;
88
- } else if ([mode isEqual:@"multicolor"]) {
89
- colors = UIImageSymbolConfiguration.configurationPreferringMulticolor;
90
- } else if ([mode isEqual:@"hierarchical"]) {
91
- colors = [UIImageSymbolConfiguration configurationWithHierarchicalColor:color];
92
- } else if ([mode isEqual:@"palette"]) {
93
- NSMutableArray<UIColor *> *palette = [NSMutableArray array];
94
- id values = configuration[@"paletteColors"];
95
- if ([values isKindOfClass:NSArray.class]) {
96
- for (id value in values) {
97
- UIColor *paletteColor = [RCTConvert UIColor:value];
98
- if (paletteColor != nil && palette.count < 3) [palette addObject:paletteColor];
99
- }
100
- }
101
- if (palette.count == 0) [palette addObject:color];
102
- colors = [UIImageSymbolConfiguration configurationWithPaletteColors:palette];
103
- }
104
- if (colors != nil) symbol = [symbol configurationByApplyingConfiguration:colors];
105
- }
106
-
107
- if (@available(iOS 26.0, *)) {
108
- NSString *variableMode = configuration[@"variableValueMode"];
109
- UIImageSymbolVariableValueMode variable = UIImageSymbolVariableValueModeAutomatic;
110
- if ([variableMode isEqual:@"color"]) variable = UIImageSymbolVariableValueModeColor;
111
- if ([variableMode isEqual:@"draw"]) variable = UIImageSymbolVariableValueModeDraw;
112
- symbol = [symbol configurationByApplyingConfiguration:
113
- [UIImageSymbolConfiguration configurationWithVariableValueMode:variable]];
114
-
115
- NSString *colorMode = configuration[@"colorRenderingMode"];
116
- UIImageSymbolColorRenderingMode rendering = UIImageSymbolColorRenderingModeAutomatic;
117
- if ([colorMode isEqual:@"flat"]) rendering = UIImageSymbolColorRenderingModeFlat;
118
- if ([colorMode isEqual:@"gradient"]) rendering = UIImageSymbolColorRenderingModeGradient;
119
- symbol = [symbol configurationByApplyingConfiguration:
120
- [UIImageSymbolConfiguration configurationWithColorRenderingMode:rendering]];
121
- }
122
-
123
- BOOL asset = [configuration[@"source"] isEqual:@"asset"];
124
- id variableValue = configuration[@"variableValue"];
125
- UIImage *image;
126
- if ([variableValue isKindOfClass:NSNumber.class]) {
127
- double value = [variableValue doubleValue];
128
- value = std::isfinite(value) ? fmin(1, fmax(0, value)) : 0;
129
- image = asset
130
- ? [UIImage imageNamed:name inBundle:NSBundle.mainBundle variableValue:value withConfiguration:symbol]
131
- : [UIImage systemImageNamed:name variableValue:value withConfiguration:symbol];
132
- } else {
133
- image = asset
134
- ? [UIImage imageNamed:name inBundle:NSBundle.mainBundle withConfiguration:symbol]
135
- : [UIImage systemImageNamed:name withConfiguration:symbol];
136
- }
137
- // Symbol content transitions are undefined for ordinary bitmap assets.
138
- return image.isSymbolImage ? image : nil;
139
- }
140
-
141
- #endif