@whetware/react-native-stroke-text 0.0.2

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 (82) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +21 -0
  3. package/NitroStrokeText.podspec +31 -0
  4. package/README.md +45 -0
  5. package/android/CMakeLists.txt +29 -0
  6. package/android/build.gradle +142 -0
  7. package/android/fix-prefab.gradle +51 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/src/main/AndroidManifest.xml +2 -0
  10. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  11. package/android/src/main/java/com/margelo/nitro/stroketext/HybridStrokeTextView.kt +107 -0
  12. package/android/src/main/java/com/margelo/nitro/stroketext/NitroStrokeTextPackage.kt +39 -0
  13. package/android/src/main/java/com/margelo/nitro/stroketext/StrokeTextView.kt +429 -0
  14. package/ios/Bridge.h +8 -0
  15. package/ios/HybridStrokeTextView.swift +114 -0
  16. package/ios/StrokeTextColor.swift +120 -0
  17. package/ios/StrokeTextView.swift +326 -0
  18. package/ios/StrokedTextLabel.swift +72 -0
  19. package/lib/StrokeText.d.ts +3 -0
  20. package/lib/StrokeText.js +109 -0
  21. package/lib/StrokeText.web.d.ts +3 -0
  22. package/lib/StrokeText.web.js +106 -0
  23. package/lib/index.d.ts +2 -0
  24. package/lib/index.js +1 -0
  25. package/lib/index.web.d.ts +2 -0
  26. package/lib/index.web.js +1 -0
  27. package/lib/specs/StrokeTextView.nitro.d.ts +37 -0
  28. package/lib/specs/StrokeTextView.nitro.js +1 -0
  29. package/lib/types.d.ts +17 -0
  30. package/lib/types.js +1 -0
  31. package/nitro.json +24 -0
  32. package/nitrogen/generated/.gitattributes +1 -0
  33. package/nitrogen/generated/android/NitroStrokeText+autolinking.cmake +83 -0
  34. package/nitrogen/generated/android/NitroStrokeText+autolinking.gradle +27 -0
  35. package/nitrogen/generated/android/NitroStrokeTextOnLoad.cpp +46 -0
  36. package/nitrogen/generated/android/NitroStrokeTextOnLoad.hpp +25 -0
  37. package/nitrogen/generated/android/c++/JHybridStrokeTextViewSpec.cpp +308 -0
  38. package/nitrogen/generated/android/c++/JHybridStrokeTextViewSpec.hpp +117 -0
  39. package/nitrogen/generated/android/c++/JStrokeTextAlign.hpp +67 -0
  40. package/nitrogen/generated/android/c++/JStrokeTextDecorationLine.hpp +64 -0
  41. package/nitrogen/generated/android/c++/JStrokeTextEllipsizeMode.hpp +64 -0
  42. package/nitrogen/generated/android/c++/JStrokeTextFontStyle.hpp +58 -0
  43. package/nitrogen/generated/android/c++/JStrokeTextTransform.hpp +64 -0
  44. package/nitrogen/generated/android/c++/views/JHybridStrokeTextViewStateUpdater.cpp +156 -0
  45. package/nitrogen/generated/android/c++/views/JHybridStrokeTextViewStateUpdater.hpp +49 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/HybridStrokeTextViewSpec.kt +209 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/NitroStrokeTextOnLoad.kt +35 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/StrokeTextAlign.kt +26 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/StrokeTextDecorationLine.kt +25 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/StrokeTextEllipsizeMode.kt +25 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/StrokeTextFontStyle.kt +23 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/StrokeTextTransform.kt +25 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/views/HybridStrokeTextViewManager.kt +70 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/stroketext/views/HybridStrokeTextViewStateUpdater.kt +23 -0
  55. package/nitrogen/generated/ios/NitroStrokeText+autolinking.rb +60 -0
  56. package/nitrogen/generated/ios/NitroStrokeText-Swift-Cxx-Bridge.cpp +33 -0
  57. package/nitrogen/generated/ios/NitroStrokeText-Swift-Cxx-Bridge.hpp +177 -0
  58. package/nitrogen/generated/ios/NitroStrokeText-Swift-Cxx-Umbrella.hpp +58 -0
  59. package/nitrogen/generated/ios/NitroStrokeTextAutolinking.mm +33 -0
  60. package/nitrogen/generated/ios/NitroStrokeTextAutolinking.swift +26 -0
  61. package/nitrogen/generated/ios/c++/HybridStrokeTextViewSpecSwift.cpp +11 -0
  62. package/nitrogen/generated/ios/c++/HybridStrokeTextViewSpecSwift.hpp +271 -0
  63. package/nitrogen/generated/ios/c++/views/HybridStrokeTextViewComponent.mm +232 -0
  64. package/nitrogen/generated/ios/swift/HybridStrokeTextViewSpec.swift +81 -0
  65. package/nitrogen/generated/ios/swift/HybridStrokeTextViewSpec_cxx.swift +620 -0
  66. package/nitrogen/generated/ios/swift/StrokeTextAlign.swift +52 -0
  67. package/nitrogen/generated/ios/swift/StrokeTextDecorationLine.swift +48 -0
  68. package/nitrogen/generated/ios/swift/StrokeTextEllipsizeMode.swift +48 -0
  69. package/nitrogen/generated/ios/swift/StrokeTextFontStyle.swift +40 -0
  70. package/nitrogen/generated/ios/swift/StrokeTextTransform.swift +48 -0
  71. package/nitrogen/generated/shared/c++/HybridStrokeTextViewSpec.cpp +72 -0
  72. package/nitrogen/generated/shared/c++/HybridStrokeTextViewSpec.hpp +128 -0
  73. package/nitrogen/generated/shared/c++/StrokeTextAlign.hpp +88 -0
  74. package/nitrogen/generated/shared/c++/StrokeTextDecorationLine.hpp +84 -0
  75. package/nitrogen/generated/shared/c++/StrokeTextEllipsizeMode.hpp +84 -0
  76. package/nitrogen/generated/shared/c++/StrokeTextFontStyle.hpp +76 -0
  77. package/nitrogen/generated/shared/c++/StrokeTextTransform.hpp +84 -0
  78. package/nitrogen/generated/shared/c++/views/HybridStrokeTextViewComponent.cpp +388 -0
  79. package/nitrogen/generated/shared/c++/views/HybridStrokeTextViewComponent.hpp +138 -0
  80. package/nitrogen/generated/shared/json/StrokeTextViewConfig.json +35 -0
  81. package/package.json +124 -0
  82. package/react-native.config.js +16 -0
@@ -0,0 +1,326 @@
1
+ import UIKit
2
+
3
+ final class StrokeTextView: UIView {
4
+ private let label = StrokedTextLabel()
5
+
6
+ var text: String = "" {
7
+ didSet { updateText() }
8
+ }
9
+
10
+ var color: UIColor = StrokeTextView.defaultTextColor() {
11
+ didSet { label.textColor = color }
12
+ }
13
+
14
+ var strokeColor: UIColor = .clear {
15
+ didSet { label.outlineColor = strokeColor }
16
+ }
17
+
18
+ var strokeWidth: CGFloat = 0 {
19
+ didSet { label.outlineWidth = strokeWidth }
20
+ }
21
+
22
+ var fontSize: CGFloat = 14 {
23
+ didSet { updateFont() }
24
+ }
25
+
26
+ var fontWeight: String = "400" {
27
+ didSet { updateFont() }
28
+ }
29
+
30
+ var fontFamily: String? = nil {
31
+ didSet { updateFont() }
32
+ }
33
+
34
+ var fontStyle: StrokeTextFontStyle = .normal {
35
+ didSet { updateFont() }
36
+ }
37
+
38
+ var allowFontScaling: Bool = true {
39
+ didSet { updateFont() }
40
+ }
41
+
42
+ var maxFontSizeMultiplier: CGFloat? = nil {
43
+ didSet { updateFont() }
44
+ }
45
+
46
+ var lineHeight: CGFloat? = nil {
47
+ didSet { updateText() }
48
+ }
49
+
50
+ var letterSpacing: CGFloat? = nil {
51
+ didSet { updateText() }
52
+ }
53
+
54
+ var textAlign: StrokeTextAlign = .auto {
55
+ didSet { updateText() }
56
+ }
57
+
58
+ var textDecorationLine: StrokeTextDecorationLine = .none {
59
+ didSet { updateText() }
60
+ }
61
+
62
+ var textTransform: StrokeTextTransform = .none {
63
+ didSet { updateText() }
64
+ }
65
+
66
+ var numberOfLines: Int = 0 {
67
+ didSet {
68
+ label.numberOfLines = numberOfLines
69
+ updateLineBreakMode()
70
+ }
71
+ }
72
+
73
+ var ellipsizeMode: StrokeTextEllipsizeMode = .tail {
74
+ didSet { updateLineBreakMode() }
75
+ }
76
+
77
+ var paddingInsets: UIEdgeInsets = .zero {
78
+ didSet {
79
+ label.textInsets = paddingInsets
80
+ invalidateMeasurements()
81
+ }
82
+ }
83
+
84
+ override init(frame: CGRect) {
85
+ super.init(frame: frame)
86
+ isOpaque = false
87
+ addSubview(label)
88
+ label.textInsets = paddingInsets
89
+ updateLineBreakMode()
90
+ }
91
+
92
+ @available(*, unavailable)
93
+ required init?(coder: NSCoder) {
94
+ fatalError("init(coder:) has not been implemented")
95
+ }
96
+
97
+ override func layoutSubviews() {
98
+ super.layoutSubviews()
99
+ label.frame = bounds
100
+ label.preferredMaxLayoutWidth = max(
101
+ 0,
102
+ bounds.width - paddingInsets.left - paddingInsets.right
103
+ )
104
+ }
105
+
106
+ override func sizeThatFits(_ size: CGSize) -> CGSize {
107
+ return label.sizeThatFits(size)
108
+ }
109
+
110
+ override var intrinsicContentSize: CGSize {
111
+ return label.intrinsicContentSize
112
+ }
113
+
114
+ private func updateFont() {
115
+ let baseSize = max(1, fontSize)
116
+ let scaledSize = scaleTypography(baseSize)
117
+ let weight = fontWeight.toUIFontWeight()
118
+ let italic = fontStyle == .italic
119
+
120
+ label.font = resolveFont(family: fontFamily, size: scaledSize, weight: weight, italic: italic)
121
+ updateText()
122
+ }
123
+
124
+ private func updateText() {
125
+ let transformed = textTransform.apply(to: text)
126
+
127
+ let paragraphStyle = NSMutableParagraphStyle()
128
+ paragraphStyle.alignment = textAlign.toNSTextAlignment()
129
+ if let lh = lineHeight, lh > 0 {
130
+ let scaledLineHeight = scaleTypography(lh)
131
+ paragraphStyle.minimumLineHeight = scaledLineHeight
132
+ paragraphStyle.maximumLineHeight = scaledLineHeight
133
+ }
134
+
135
+ var attributes: [NSAttributedString.Key: Any] = [
136
+ .paragraphStyle: paragraphStyle,
137
+ .font: label.font as Any,
138
+ ]
139
+
140
+ if let ls = letterSpacing {
141
+ attributes[.kern] = scaleTypography(ls)
142
+ }
143
+
144
+ switch textDecorationLine {
145
+ case .underline:
146
+ attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
147
+ case .lineThrough:
148
+ attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
149
+ case .underlineLineThrough:
150
+ attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
151
+ attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
152
+ case .none:
153
+ break
154
+ }
155
+
156
+ label.attributedText = NSAttributedString(string: transformed, attributes: attributes)
157
+ invalidateMeasurements()
158
+ }
159
+
160
+ private func updateLineBreakMode() {
161
+ if numberOfLines <= 0 {
162
+ label.lineBreakMode = .byWordWrapping
163
+ invalidateMeasurements()
164
+ return
165
+ }
166
+
167
+ switch ellipsizeMode {
168
+ case .head:
169
+ label.lineBreakMode = .byTruncatingHead
170
+ case .middle:
171
+ label.lineBreakMode = .byTruncatingMiddle
172
+ case .clip:
173
+ label.lineBreakMode = .byClipping
174
+ case .tail:
175
+ label.lineBreakMode = .byTruncatingTail
176
+ @unknown default:
177
+ label.lineBreakMode = .byTruncatingTail
178
+ }
179
+
180
+ invalidateMeasurements()
181
+ }
182
+
183
+ private func scaleTypography(_ value: CGFloat) -> CGFloat {
184
+ guard value > 0, allowFontScaling else { return value }
185
+ let scaled = UIFontMetrics.default.scaledValue(for: value)
186
+ guard let maxMultiplier = maxFontSizeMultiplier, maxMultiplier > 0 else { return scaled }
187
+ return min(scaled, value * maxMultiplier)
188
+ }
189
+
190
+ private func resolveFont(
191
+ family: String?,
192
+ size: CGFloat,
193
+ weight: UIFont.Weight,
194
+ italic: Bool
195
+ ) -> UIFont {
196
+ let trimmedFamily = family?.trimmingCharacters(in: .whitespacesAndNewlines)
197
+
198
+ if let name = trimmedFamily, !name.isEmpty {
199
+ // 1) Treat as a concrete font name (PostScript name).
200
+ if let fontByName = UIFont(name: name, size: size) {
201
+ return italic ? fontByName.withItalicTrait(size: size) : fontByName
202
+ }
203
+
204
+ // 2) Treat as a family name and pick the closest matching variant.
205
+ let familyName =
206
+ UIFont.familyNames.first(where: { $0.caseInsensitiveCompare(name) == .orderedSame }) ?? name
207
+ let candidates = UIFont.fontNames(forFamilyName: familyName).compactMap { UIFont(name: $0, size: size) }
208
+ if let best = bestFontMatch(in: candidates, size: size, weight: weight, italic: italic) {
209
+ return best
210
+ }
211
+ }
212
+
213
+ // 3) Fallback to system font.
214
+ let system = UIFont.systemFont(ofSize: size, weight: weight)
215
+ return italic ? system.withItalicTrait(size: size) : system
216
+ }
217
+
218
+ private func bestFontMatch(
219
+ in fonts: [UIFont],
220
+ size: CGFloat,
221
+ weight: UIFont.Weight,
222
+ italic: Bool
223
+ ) -> UIFont? {
224
+ guard !fonts.isEmpty else { return nil }
225
+
226
+ let targetWeight = Double(weight.rawValue)
227
+ var best: UIFont = fonts[0]
228
+ var bestScore = Double.greatestFiniteMagnitude
229
+
230
+ for font in fonts {
231
+ let traits = font.fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any]
232
+ let fontWeight = (traits?[.weight] as? CGFloat) ?? 0
233
+ let isItalic = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
234
+
235
+ let italicPenalty = isItalic == italic ? 0.0 : 1000.0
236
+ let weightPenalty = Swift.abs(Double(fontWeight) - targetWeight)
237
+ let score = italicPenalty + weightPenalty
238
+
239
+ if score < bestScore {
240
+ bestScore = score
241
+ best = font
242
+ }
243
+ }
244
+
245
+ return italic ? best.withItalicTrait(size: size) : best
246
+ }
247
+
248
+ private func invalidateMeasurements() {
249
+ setNeedsLayout()
250
+ invalidateIntrinsicContentSize()
251
+ setNeedsDisplay()
252
+ }
253
+
254
+ static func defaultTextColor() -> UIColor {
255
+ if #available(iOS 13.0, *) {
256
+ return .label
257
+ }
258
+ return .black
259
+ }
260
+ }
261
+
262
+ private extension String {
263
+ func toUIFontWeight() -> UIFont.Weight {
264
+ let lower = trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
265
+ switch lower {
266
+ case "ultralight", "100": return .ultraLight
267
+ case "thin", "200": return .thin
268
+ case "light", "300": return .light
269
+ case "normal", "regular", "400": return .regular
270
+ case "medium", "500": return .medium
271
+ case "semibold", "600": return .semibold
272
+ case "bold", "700": return .bold
273
+ case "heavy", "800": return .heavy
274
+ case "black", "900": return .black
275
+ default:
276
+ if let value = Int(lower) {
277
+ switch value {
278
+ case ...150: return .ultraLight
279
+ case 151...250: return .thin
280
+ case 251...350: return .light
281
+ case 351...450: return .regular
282
+ case 451...550: return .medium
283
+ case 551...650: return .semibold
284
+ case 651...750: return .bold
285
+ case 751...850: return .heavy
286
+ default: return .black
287
+ }
288
+ }
289
+ return .regular
290
+ }
291
+ }
292
+ }
293
+
294
+ private extension UIFont {
295
+ func withItalicTrait(size: CGFloat) -> UIFont {
296
+ let descriptor = fontDescriptor
297
+ let traits = descriptor.symbolicTraits.union(.traitItalic)
298
+ guard let italicDescriptor = descriptor.withSymbolicTraits(traits) else { return self }
299
+ return UIFont(descriptor: italicDescriptor, size: size)
300
+ }
301
+ }
302
+
303
+ private extension StrokeTextAlign {
304
+ func toNSTextAlignment() -> NSTextAlignment {
305
+ switch self {
306
+ case .left: return .left
307
+ case .right: return .right
308
+ case .center: return .center
309
+ case .justify: return .justified
310
+ case .auto: return .natural
311
+ @unknown default: return .natural
312
+ }
313
+ }
314
+ }
315
+
316
+ private extension StrokeTextTransform {
317
+ func apply(to input: String) -> String {
318
+ switch self {
319
+ case .uppercase: return input.uppercased()
320
+ case .lowercase: return input.lowercased()
321
+ case .capitalize: return input.capitalized
322
+ case .none: return input
323
+ @unknown default: return input
324
+ }
325
+ }
326
+ }
@@ -0,0 +1,72 @@
1
+ import UIKit
2
+
3
+ final class StrokedTextLabel: UILabel {
4
+ var textInsets: UIEdgeInsets = .zero {
5
+ didSet { invalidateMeasurements() }
6
+ }
7
+
8
+ var outlineColor: UIColor = .clear {
9
+ didSet { setNeedsDisplay() }
10
+ }
11
+
12
+ var outlineWidth: CGFloat = 0 {
13
+ didSet { invalidateMeasurements() }
14
+ }
15
+
16
+ override init(frame: CGRect) {
17
+ super.init(frame: frame)
18
+ numberOfLines = 0
19
+ lineBreakMode = .byWordWrapping
20
+ clipsToBounds = false
21
+ layer.masksToBounds = false
22
+ }
23
+
24
+ @available(*, unavailable)
25
+ required init?(coder: NSCoder) {
26
+ fatalError("init(coder:) has not been implemented")
27
+ }
28
+
29
+ override func drawText(in rect: CGRect) {
30
+ let insetRect = rect.inset(by: textInsets)
31
+
32
+ guard outlineWidth > 0, let ctx = UIGraphicsGetCurrentContext() else {
33
+ super.drawText(in: insetRect)
34
+ return
35
+ }
36
+
37
+ let fillColor = textColor
38
+
39
+ ctx.setLineWidth(outlineWidth)
40
+ ctx.setLineJoin(.round)
41
+
42
+ ctx.setTextDrawingMode(.stroke)
43
+ textColor = outlineColor
44
+ super.drawText(in: insetRect)
45
+
46
+ ctx.setTextDrawingMode(.fill)
47
+ textColor = fillColor
48
+ super.drawText(in: insetRect)
49
+ }
50
+
51
+ override func textRect(
52
+ forBounds bounds: CGRect,
53
+ limitedToNumberOfLines numberOfLines: Int
54
+ ) -> CGRect {
55
+ let insetBounds = bounds.inset(by: textInsets)
56
+ var rect = super.textRect(forBounds: insetBounds, limitedToNumberOfLines: numberOfLines)
57
+ // Match React Native <Text> behavior (top-aligned). UILabel may otherwise vertically center
58
+ // text when the bounds are taller than the rendered text.
59
+ rect.origin.y = insetBounds.origin.y
60
+ rect.origin.x -= textInsets.left
61
+ rect.origin.y -= textInsets.top
62
+ rect.size.width += textInsets.left + textInsets.right
63
+ rect.size.height += textInsets.top + textInsets.bottom
64
+ return rect
65
+ }
66
+
67
+ private func invalidateMeasurements() {
68
+ setNeedsLayout()
69
+ invalidateIntrinsicContentSize()
70
+ setNeedsDisplay()
71
+ }
72
+ }
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { StrokeTextProps } from './types';
3
+ export declare function StrokeText({ text, children, style, hybridRef, ...rest }: StrokeTextProps): React.JSX.Element;
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { callback, getHostComponent } from 'react-native-nitro-modules';
4
+ import StrokeTextViewConfig from '../nitrogen/generated/shared/json/StrokeTextViewConfig.json';
5
+ const NativeStrokeTextView = getHostComponent('StrokeTextView', () => StrokeTextViewConfig);
6
+ function resolveText(text, children) {
7
+ if (typeof text === 'string')
8
+ return text;
9
+ if (typeof children === 'string')
10
+ return children;
11
+ return '';
12
+ }
13
+ function toNumber(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
15
+ }
16
+ function firstNumber(...values) {
17
+ for (const value of values) {
18
+ const n = toNumber(value);
19
+ if (n != null)
20
+ return n;
21
+ }
22
+ return undefined;
23
+ }
24
+ function toFontWeightString(value) {
25
+ if (typeof value === 'string')
26
+ return value;
27
+ if (typeof value === 'number')
28
+ return `${value}`;
29
+ return undefined;
30
+ }
31
+ function toColorString(value) {
32
+ if (typeof value === 'string')
33
+ return value;
34
+ if (typeof value === 'number') {
35
+ const c = value >>> 0;
36
+ const a = ((c >>> 24) & 0xff) / 255;
37
+ const r = (c >>> 16) & 0xff;
38
+ const g = (c >>> 8) & 0xff;
39
+ const b = c & 0xff;
40
+ return `rgba(${r},${g},${b},${a})`;
41
+ }
42
+ return undefined;
43
+ }
44
+ function warnOnInvalidChildren(children) {
45
+ if (!__DEV__)
46
+ return;
47
+ if (children == null)
48
+ return;
49
+ if (typeof children === 'string')
50
+ return;
51
+ // eslint-disable-next-line no-console
52
+ console.warn('[StrokeText] Children must be a string. Use the `text` prop instead.');
53
+ }
54
+ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
55
+ warnOnInvalidChildren(children);
56
+ const resolvedText = resolveText(text, children);
57
+ const flattened = StyleSheet.flatten(style);
58
+ const { padding, paddingVertical, paddingHorizontal, paddingTop, paddingRight, paddingBottom, paddingLeft, ...nativeProps } = rest;
59
+ const { color: styleColor, fontSize: styleFontSize, fontWeight: styleFontWeight, fontFamily: styleFontFamily, fontStyle: styleFontStyle, lineHeight: styleLineHeight, letterSpacing: styleLetterSpacing, textAlign: styleTextAlign, textDecorationLine: styleTextDecorationLine, textTransform: styleTextTransform, opacity: styleOpacity, includeFontPadding: styleIncludeFontPadding, padding: stylePadding, paddingVertical: stylePaddingVertical, paddingHorizontal: stylePaddingHorizontal, paddingTop: stylePaddingTop, paddingRight: stylePaddingRight, paddingBottom: stylePaddingBottom, paddingLeft: stylePaddingLeft, ...containerStyle } = flattened ?? {};
60
+ const strokeWidth = Math.max(0, nativeProps.strokeWidth ?? 0);
61
+ const strokeInset = Math.ceil(strokeWidth) / 2;
62
+ const baseTop = firstNumber(paddingTop, stylePaddingTop, paddingVertical, stylePaddingVertical, padding, stylePadding) ?? 0;
63
+ const baseRight = firstNumber(paddingRight, stylePaddingRight, paddingHorizontal, stylePaddingHorizontal, padding, stylePadding) ?? 0;
64
+ const baseBottom = firstNumber(paddingBottom, stylePaddingBottom, paddingVertical, stylePaddingVertical, padding, stylePadding) ?? 0;
65
+ const baseLeft = firstNumber(paddingLeft, stylePaddingLeft, paddingHorizontal, stylePaddingHorizontal, padding, stylePadding) ?? 0;
66
+ const effectiveNumberOfLines = nativeProps.numberOfLines != null && nativeProps.numberOfLines > 0
67
+ ? nativeProps.numberOfLines
68
+ : undefined;
69
+ const effectiveEllipsizeMode = effectiveNumberOfLines == null
70
+ ? undefined
71
+ : nativeProps.ellipsizeMode ?? 'tail';
72
+ const effectiveIncludeFontPadding = nativeProps.includeFontPadding ?? styleIncludeFontPadding ?? false;
73
+ return (React.createElement(View, { style: [styles.container, containerStyle] },
74
+ React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, allowFontScaling: nativeProps.allowFontScaling, maxFontSizeMultiplier: nativeProps.maxFontSizeMultiplier, style: [
75
+ style,
76
+ {
77
+ paddingTop: baseTop,
78
+ paddingRight: baseRight,
79
+ paddingBottom: baseBottom,
80
+ paddingLeft: baseLeft,
81
+ },
82
+ effectiveIncludeFontPadding == null
83
+ ? null
84
+ : { includeFontPadding: effectiveIncludeFontPadding },
85
+ styles.hiddenText,
86
+ ] }, resolvedText),
87
+ React.createElement(NativeStrokeTextView, { ...nativeProps, text: resolvedText, color: nativeProps.color ?? toColorString(styleColor), fontSize: nativeProps.fontSize ?? toNumber(styleFontSize), fontWeight: nativeProps.fontWeight ?? toFontWeightString(styleFontWeight), fontFamily: nativeProps.fontFamily ?? styleFontFamily, fontStyle: nativeProps.fontStyle ?? styleFontStyle, lineHeight: nativeProps.lineHeight ?? toNumber(styleLineHeight), letterSpacing: nativeProps.letterSpacing ?? toNumber(styleLetterSpacing), textAlign: nativeProps.textAlign ?? styleTextAlign, textDecorationLine: nativeProps.textDecorationLine ?? styleTextDecorationLine, textTransform: nativeProps.textTransform ?? styleTextTransform, opacity: nativeProps.opacity ?? toNumber(styleOpacity), includeFontPadding: effectiveIncludeFontPadding, numberOfLines: nativeProps.numberOfLines, ellipsizeMode: effectiveEllipsizeMode, paddingTop: baseTop, paddingRight: baseRight, paddingBottom: baseBottom, paddingLeft: baseLeft, hybridRef: hybridRef ? callback(hybridRef) : undefined, pointerEvents: "none", style: [
88
+ styles.overlay,
89
+ strokeInset === 0
90
+ ? null
91
+ : {
92
+ top: -strokeInset,
93
+ right: -strokeInset,
94
+ bottom: -strokeInset,
95
+ left: -strokeInset,
96
+ },
97
+ ] })));
98
+ }
99
+ const styles = StyleSheet.create({
100
+ container: {
101
+ alignSelf: 'flex-start',
102
+ },
103
+ overlay: {
104
+ ...StyleSheet.absoluteFillObject,
105
+ },
106
+ hiddenText: {
107
+ opacity: 0,
108
+ },
109
+ });
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { StrokeTextProps } from './types';
3
+ export declare function StrokeText({ text, children, style, ...rest }: StrokeTextProps): React.JSX.Element;
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ function resolveText(text, children) {
4
+ if (typeof text === 'string')
5
+ return text;
6
+ if (typeof children === 'string')
7
+ return children;
8
+ return '';
9
+ }
10
+ function toNumber(value) {
11
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
12
+ }
13
+ function firstNumber(...values) {
14
+ for (const value of values) {
15
+ const n = toNumber(value);
16
+ if (n != null)
17
+ return n;
18
+ }
19
+ return undefined;
20
+ }
21
+ export function StrokeText({ text, children, style, ...rest }) {
22
+ const resolvedText = resolveText(text, children);
23
+ const flattened = StyleSheet.flatten(style);
24
+ const { color: styleColor, fontSize: styleFontSize, fontWeight: styleFontWeight, fontFamily: styleFontFamily, fontStyle: styleFontStyle, lineHeight: styleLineHeight, letterSpacing: styleLetterSpacing, textAlign: styleTextAlign, textDecorationLine: styleTextDecorationLine, textTransform: styleTextTransform, opacity: styleOpacity, padding: stylePadding, paddingVertical: stylePaddingVertical, paddingHorizontal: stylePaddingHorizontal, paddingTop: stylePaddingTop, paddingRight: stylePaddingRight, paddingBottom: stylePaddingBottom, paddingLeft: stylePaddingLeft, ...containerStyle } = flattened ?? {};
25
+ const textStyle = {
26
+ fontSize: rest.fontSize ?? styleFontSize,
27
+ fontWeight: (rest.fontWeight ?? styleFontWeight),
28
+ fontFamily: rest.fontFamily ?? styleFontFamily,
29
+ fontStyle: rest.fontStyle ?? styleFontStyle,
30
+ lineHeight: rest.lineHeight ?? styleLineHeight,
31
+ letterSpacing: rest.letterSpacing ?? styleLetterSpacing,
32
+ textAlign: rest.textAlign ?? styleTextAlign,
33
+ textDecorationLine: rest.textDecorationLine ?? styleTextDecorationLine,
34
+ textTransform: rest.textTransform ?? styleTextTransform,
35
+ opacity: rest.opacity ?? styleOpacity,
36
+ };
37
+ const fillColor = rest.color ?? styleColor ?? '#000';
38
+ const strokeColor = rest.strokeColor ?? 'transparent';
39
+ const strokeWidth = Math.max(0, rest.strokeWidth ?? 0);
40
+ const strokeInset = Math.ceil(strokeWidth) / 2;
41
+ const baseTop = firstNumber(rest.paddingTop, stylePaddingTop, rest.paddingVertical, stylePaddingVertical, rest.padding, stylePadding) ?? 0;
42
+ const baseRight = firstNumber(rest.paddingRight, stylePaddingRight, rest.paddingHorizontal, stylePaddingHorizontal, rest.padding, stylePadding) ?? 0;
43
+ const baseBottom = firstNumber(rest.paddingBottom, stylePaddingBottom, rest.paddingVertical, stylePaddingVertical, rest.padding, stylePadding) ?? 0;
44
+ const baseLeft = firstNumber(rest.paddingLeft, stylePaddingLeft, rest.paddingHorizontal, stylePaddingHorizontal, rest.padding, stylePadding) ?? 0;
45
+ const effectiveNumberOfLines = rest.numberOfLines != null && rest.numberOfLines > 0
46
+ ? rest.numberOfLines
47
+ : undefined;
48
+ const effectiveEllipsizeMode = effectiveNumberOfLines == null ? undefined : rest.ellipsizeMode ?? 'tail';
49
+ return (React.createElement(View, { style: [styles.container, containerStyle] },
50
+ React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, style: [
51
+ textStyle,
52
+ {
53
+ paddingTop: baseTop,
54
+ paddingRight: baseRight,
55
+ paddingBottom: baseBottom,
56
+ paddingLeft: baseLeft,
57
+ },
58
+ styles.hiddenText,
59
+ ] }, resolvedText),
60
+ React.createElement(Text, { pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, style: [
61
+ textStyle,
62
+ {
63
+ color: fillColor,
64
+ paddingTop: baseTop + strokeInset,
65
+ paddingRight: baseRight + strokeInset,
66
+ paddingBottom: baseBottom + strokeInset,
67
+ paddingLeft: baseLeft + strokeInset,
68
+ },
69
+ strokeInset === 0 ? null : { maxWidth: 'none' },
70
+ // react-native-web uses `box-sizing: border-box` globally; with ellipsizing enabled
71
+ // (`overflow: hidden` + `text-overflow: ellipsis`), the extra stroke padding can reduce
72
+ // the content box by a couple pixels and cause false-positive ellipses for some fonts.
73
+ // Use `content-box` so padding doesn't shrink the text's available width.
74
+ strokeInset === 0 ? null : { boxSizing: 'content-box' },
75
+ strokeWidth > 0 && strokeColor !== 'transparent'
76
+ ? {
77
+ WebkitTextStrokeWidth: `${strokeWidth}px`,
78
+ WebkitTextStrokeColor: strokeColor,
79
+ WebkitTextFillColor: fillColor,
80
+ textStroke: `${strokeWidth}px ${strokeColor}`,
81
+ textFillColor: fillColor,
82
+ paintOrder: 'stroke fill',
83
+ }
84
+ : null,
85
+ styles.overlay,
86
+ strokeInset === 0
87
+ ? null
88
+ : {
89
+ top: -strokeInset,
90
+ right: -strokeInset,
91
+ bottom: -strokeInset,
92
+ left: -strokeInset,
93
+ },
94
+ ] }, resolvedText)));
95
+ }
96
+ const styles = StyleSheet.create({
97
+ container: {
98
+ alignSelf: 'flex-start',
99
+ },
100
+ overlay: {
101
+ ...StyleSheet.absoluteFillObject,
102
+ },
103
+ hiddenText: {
104
+ opacity: 0,
105
+ },
106
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { StrokeText } from './StrokeText.js';
2
+ export type { StrokeTextAlign, StrokeTextDecorationLine, StrokeTextFontStyle, StrokeTextMethods, StrokeTextNativeProps, StrokeTextProps, StrokeTextTransform, StrokeTextView, } from './types.js';
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ export { StrokeText } from './StrokeText.js';
@@ -0,0 +1,2 @@
1
+ export { StrokeText } from './StrokeText.web.js';
2
+ export type { StrokeTextAlign, StrokeTextDecorationLine, StrokeTextFontStyle, StrokeTextMethods, StrokeTextNativeProps, StrokeTextProps, StrokeTextTransform, StrokeTextView, } from './types.js';
@@ -0,0 +1 @@
1
+ export { StrokeText } from './StrokeText.web.js';
@@ -0,0 +1,37 @@
1
+ import type { HybridView, HybridViewMethods, HybridViewProps } from 'react-native-nitro-modules';
2
+ export type StrokeTextAlign = 'auto' | 'left' | 'right' | 'center' | 'justify';
3
+ export type StrokeTextDecorationLine = 'none' | 'underline' | 'line-through' | 'underline line-through';
4
+ export type StrokeTextTransform = 'none' | 'uppercase' | 'lowercase' | 'capitalize';
5
+ export type StrokeTextFontStyle = 'normal' | 'italic';
6
+ export type StrokeTextEllipsizeMode = 'head' | 'middle' | 'tail' | 'clip';
7
+ export interface StrokeTextProps extends HybridViewProps {
8
+ text: string;
9
+ color?: string;
10
+ strokeColor?: string;
11
+ strokeWidth?: number;
12
+ fontSize?: number;
13
+ fontWeight?: string;
14
+ fontFamily?: string;
15
+ fontStyle?: StrokeTextFontStyle;
16
+ lineHeight?: number;
17
+ letterSpacing?: number;
18
+ textAlign?: StrokeTextAlign;
19
+ textDecorationLine?: StrokeTextDecorationLine;
20
+ textTransform?: StrokeTextTransform;
21
+ opacity?: number;
22
+ allowFontScaling?: boolean;
23
+ maxFontSizeMultiplier?: number;
24
+ includeFontPadding?: boolean;
25
+ numberOfLines?: number;
26
+ ellipsizeMode?: StrokeTextEllipsizeMode;
27
+ padding?: number;
28
+ paddingVertical?: number;
29
+ paddingHorizontal?: number;
30
+ paddingTop?: number;
31
+ paddingRight?: number;
32
+ paddingBottom?: number;
33
+ paddingLeft?: number;
34
+ }
35
+ export interface StrokeTextMethods extends HybridViewMethods {
36
+ }
37
+ export type StrokeTextView = HybridView<StrokeTextProps, StrokeTextMethods>;
@@ -0,0 +1 @@
1
+ export {};