@momo-kits/native-kits 0.162.30 → 0.162.31
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.
- package/CLAUDE.md +19 -0
- package/ios/Application/ApplicationEnvironment.swift +14 -6
- package/ios/Application/LanguageSource.swift +93 -0
- package/ios/Application/Localize.swift +73 -4
- package/ios/Application/LocalizeModifier.swift +30 -0
- package/ios/Application/Navigation/NavigationContainer.swift +28 -18
- package/ios/Application/Navigation/Navigator.swift +121 -53
- package/ios/Application/Navigation/NavigatorManager.swift +49 -0
- package/ios/Application/Navigation/component/NavigationFooter.swift +90 -0
- package/ios/Application/StackScreen.swift +19 -28
- package/ios/Badge/Badge.swift +2 -1
- package/ios/Badge/BadgeRibbon.swift +6 -2
- package/ios/Button/Button.swift +41 -61
- package/ios/Chip/Chip.swift +6 -5
- package/ios/Icon/Icon.swift +13 -4
- package/ios/Input/ErrorView.swift +2 -1
- package/ios/Input/Input.swift +9 -2
- package/ios/Input/InputPhoneNumber.swift +2 -1
- package/ios/Input/InputSearch.swift +3 -2
- package/ios/Input/InputTextArea.swift +6 -1
- package/ios/InputMoney/InputMoney.swift +5 -2
- package/ios/InputOTP/InputOTP.swift +2 -1
- package/ios/Layout/GridContext.swift +30 -0
- package/ios/Layout/Item.swift +42 -0
- package/ios/Layout/Section.swift +134 -0
- package/ios/Popup/PopupNotify.swift +2 -1
- package/ios/ProgressInfo/ProgressInfo.swift +4 -3
- package/ios/Rating/Rating.swift +2 -1
- package/ios/Stepper/Stepper.swift +2 -1
- package/ios/SuggestAction/SuggestAction.swift +3 -2
- package/ios/Tag/Tag.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
- package/ios/Title/Title.swift +3 -2
- package/ios/Tooltip/Tooltip.swift +2 -1
- package/ios/Typography/FontScale.swift +103 -0
- package/ios/Typography/FontScaleStore.swift +52 -13
- package/ios/Typography/Text.swift +12 -29
- package/ios/Typography/Typography.swift +66 -86
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +87 -73
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +55 -249
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +90 -82
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +74 -104
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +47 -50
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +63 -7
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +90 -81
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +73 -97
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DemoScreen.swift +420 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +46 -4
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +54 -57
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +80 -45
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +81 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +49 -62
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +66 -116
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +104 -98
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +76 -118
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +73 -86
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +69 -54
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +74 -54
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +73 -20
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +68 -38
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +55 -52
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +51 -4
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +14 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +47 -51
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PlaygroundData.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +67 -93
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +34 -43
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +83 -71
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +55 -23
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +58 -46
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +83 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +41 -12
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +67 -127
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +54 -110
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +69 -79
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +78 -58
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +76 -65
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +82 -127
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +44 -19
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +92 -168
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +79 -27
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +0 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +87 -92
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +94 -163
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +70 -70
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +68 -114
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +3 -1
- package/package.json +2 -2
|
@@ -50,6 +50,7 @@ public func formatNumberToMoney(_ digits: String, currency: String = "đ") -> St
|
|
|
50
50
|
|
|
51
51
|
public struct InputMoney: View {
|
|
52
52
|
@Environment(\.appTheme) private var theme
|
|
53
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
53
54
|
@Binding public var text: String
|
|
54
55
|
|
|
55
56
|
public var floatingValue: String
|
|
@@ -214,7 +215,7 @@ public struct InputMoney: View {
|
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
217
|
.padding(.horizontal, Spacing.M)
|
|
217
|
-
.frame(height:
|
|
218
|
+
.frame(height: fontScale(size == .small ? 48 : 56))
|
|
218
219
|
.background(
|
|
219
220
|
RoundedRectangle(cornerRadius: Radius.S)
|
|
220
221
|
.fill(theme.colors.background.surface)
|
|
@@ -278,7 +279,7 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
278
279
|
let field = UITextField()
|
|
279
280
|
field.delegate = context.coordinator
|
|
280
281
|
field.keyboardType = keyboardType
|
|
281
|
-
field.font = UIFont.systemFont(ofSize:
|
|
282
|
+
field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
|
|
282
283
|
field.textColor = textColor
|
|
283
284
|
field.isEnabled = !isDisabled
|
|
284
285
|
field.tintColor = cursorColor
|
|
@@ -295,6 +296,8 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
295
296
|
|
|
296
297
|
func updateUIView(_ field: UITextField, context: Context) {
|
|
297
298
|
context.coordinator.parent = self
|
|
299
|
+
// Re-applied here, not just in makeUIView, so a font-scale change reaches an existing field.
|
|
300
|
+
field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
|
|
298
301
|
field.isEnabled = !isDisabled
|
|
299
302
|
field.textColor = textColor
|
|
300
303
|
field.tintColor = cursorColor
|
|
@@ -7,12 +7,13 @@ import SwiftUI
|
|
|
7
7
|
private struct OTPCaret: View {
|
|
8
8
|
@Environment(\.appTheme) private var theme
|
|
9
9
|
@State private var visible: Bool = true
|
|
10
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
10
11
|
|
|
11
12
|
var body: some View {
|
|
12
13
|
HStack(spacing: 0) {
|
|
13
14
|
Rectangle()
|
|
14
15
|
.fill(theme.colors.primary)
|
|
15
|
-
.frame(width: 1, height:
|
|
16
|
+
.frame(width: 1, height: fontScale(12))
|
|
16
17
|
.opacity(visible ? 1 : 0)
|
|
17
18
|
.animation(
|
|
18
19
|
.linear(duration: 0.5).repeatForever(autoreverses: true),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Grid metrics published by a layout container (`SectionView`) so span-based children
|
|
4
|
+
/// can size themselves. Mirrors `vn.momo.kits.layout.GridContext`.
|
|
5
|
+
public struct GridContext: Equatable {
|
|
6
|
+
public let numberOfColumns: Int
|
|
7
|
+
public let gutter: CGFloat
|
|
8
|
+
public let sizePerSpan: CGFloat
|
|
9
|
+
|
|
10
|
+
public init(numberOfColumns: Int = 12, gutter: CGFloat = Spacing.M, sizePerSpan: CGFloat = 0) {
|
|
11
|
+
self.numberOfColumns = numberOfColumns
|
|
12
|
+
self.gutter = gutter
|
|
13
|
+
self.sizePerSpan = sizePerSpan
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public func sizeForSpan(_ span: Int) -> CGFloat {
|
|
17
|
+
sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private struct GridContextKey: EnvironmentKey {
|
|
22
|
+
static let defaultValue: GridContext? = nil
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public extension EnvironmentValues {
|
|
26
|
+
var gridContext: GridContext? {
|
|
27
|
+
get { self[GridContextKey.self] }
|
|
28
|
+
set { self[GridContextKey.self] = newValue }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Span-sized cell inside a `SectionView`. Reads the grid published by its container;
|
|
4
|
+
/// without one it just wraps its content. Mirrors `vn.momo.kits.layout.Item`.
|
|
5
|
+
public struct ItemView<Content: View>: View {
|
|
6
|
+
// MARK: Lifecycle
|
|
7
|
+
|
|
8
|
+
public init(
|
|
9
|
+
widthSpan: Int = 12,
|
|
10
|
+
heightSpan: Int = 0,
|
|
11
|
+
@ViewBuilder content: @escaping () -> Content
|
|
12
|
+
) {
|
|
13
|
+
self.widthSpan = widthSpan
|
|
14
|
+
self.heightSpan = heightSpan
|
|
15
|
+
self.content = content
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// MARK: Public
|
|
19
|
+
|
|
20
|
+
public var body: some View {
|
|
21
|
+
// A VStack (not a ZStack) so multiple children flow VERTICALLY instead of stacking
|
|
22
|
+
// on top of each other — matches the React Item's flex-column content.
|
|
23
|
+
VStack(alignment: .leading, spacing: 0) { content() }
|
|
24
|
+
.frame(width: width, height: height, alignment: .topLeading)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: Private
|
|
28
|
+
|
|
29
|
+
private let widthSpan: Int
|
|
30
|
+
private let heightSpan: Int
|
|
31
|
+
private let content: () -> Content
|
|
32
|
+
|
|
33
|
+
@Environment(\.gridContext) private var grid
|
|
34
|
+
|
|
35
|
+
private var width: CGFloat? {
|
|
36
|
+
grid.map { $0.sizeForSpan(widthSpan) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private var height: CGFloat? {
|
|
40
|
+
heightSpan > 0 ? grid.map { $0.sizeForSpan(heightSpan) } : nil
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// 12-column section container: horizontal margin, optional background image, and a
|
|
4
|
+
/// wrapping row of children that read their span size from `\.gridContext`.
|
|
5
|
+
/// Mirrors `vn.momo.kits.layout.Section`.
|
|
6
|
+
public struct SectionView<Content: View>: View {
|
|
7
|
+
// MARK: Lifecycle
|
|
8
|
+
|
|
9
|
+
public init(
|
|
10
|
+
useMargin: Bool = true,
|
|
11
|
+
backgroundImage: String? = nil,
|
|
12
|
+
@ViewBuilder content: @escaping () -> Content
|
|
13
|
+
) {
|
|
14
|
+
self.useMargin = useMargin
|
|
15
|
+
self.backgroundImage = backgroundImage
|
|
16
|
+
self.content = content
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// MARK: Public
|
|
20
|
+
|
|
21
|
+
public var body: some View {
|
|
22
|
+
flow
|
|
23
|
+
.environment(\.gridContext, grid)
|
|
24
|
+
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
25
|
+
.background(widthReader)
|
|
26
|
+
.background(background)
|
|
27
|
+
.padding(.horizontal, useMargin ? Spacing.M : 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: Private
|
|
31
|
+
|
|
32
|
+
private static var columns: Int { 12 }
|
|
33
|
+
private static var gutter: CGFloat { Spacing.M }
|
|
34
|
+
|
|
35
|
+
private let useMargin: Bool
|
|
36
|
+
private let backgroundImage: String?
|
|
37
|
+
private let content: () -> Content
|
|
38
|
+
|
|
39
|
+
/// Width available *inside* the horizontal margin — the Compose `BoxWithConstraints` equivalent.
|
|
40
|
+
@State private var availableWidth: CGFloat = 0
|
|
41
|
+
|
|
42
|
+
private var grid: GridContext {
|
|
43
|
+
let columns = CGFloat(Self.columns)
|
|
44
|
+
// The trailing `- 1 / columns` mirrors Compose: it absorbs rounding so 12 spans
|
|
45
|
+
// plus their gutters never overflow the row by a sub-point.
|
|
46
|
+
let sizePerSpan = (availableWidth - Self.gutter * (columns - 1)) / columns - 1 / columns
|
|
47
|
+
return GridContext(numberOfColumns: Self.columns, gutter: Self.gutter, sizePerSpan: max(0, sizePerSpan))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ViewBuilder private var flow: some View {
|
|
51
|
+
if #available(iOS 16.0, *) {
|
|
52
|
+
FlowRowLayout(gutter: Self.gutter) { content() }
|
|
53
|
+
} else {
|
|
54
|
+
// No `Layout` protocol before iOS 16: children stack instead of wrapping.
|
|
55
|
+
// Full-width (span 12) items — the common case — look identical.
|
|
56
|
+
VStack(alignment: .leading, spacing: Self.gutter) { content() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ViewBuilder private var background: some View {
|
|
61
|
+
if let backgroundImage {
|
|
62
|
+
ImageView(backgroundImage, contentMode: .fill).clipped()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private var widthReader: some View {
|
|
67
|
+
GeometryReader { proxy in
|
|
68
|
+
Color.clear.preference(key: SectionWidthKey.self, value: proxy.size.width)
|
|
69
|
+
}
|
|
70
|
+
.onPreferenceChange(SectionWidthKey.self) { availableWidth = $0 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private struct SectionWidthKey: PreferenceKey {
|
|
75
|
+
static var defaultValue: CGFloat = 0
|
|
76
|
+
|
|
77
|
+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
78
|
+
value = max(value, nextValue())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
|
|
83
|
+
@available(iOS 16.0, *)
|
|
84
|
+
private struct FlowRowLayout: Layout {
|
|
85
|
+
struct Row {
|
|
86
|
+
var items: [(index: Int, size: CGSize)] = []
|
|
87
|
+
var width: CGFloat = 0
|
|
88
|
+
var height: CGFloat = 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let gutter: CGFloat
|
|
92
|
+
|
|
93
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
|
94
|
+
let maxWidth = proposal.width ?? .infinity
|
|
95
|
+
let rows = rows(for: subviews, maxWidth: maxWidth)
|
|
96
|
+
let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
|
|
97
|
+
return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
|
101
|
+
var y = bounds.minY
|
|
102
|
+
for row in rows(for: subviews, maxWidth: bounds.width) {
|
|
103
|
+
var x = bounds.minX
|
|
104
|
+
for item in row.items {
|
|
105
|
+
subviews[item.index].place(
|
|
106
|
+
at: CGPoint(x: x, y: y),
|
|
107
|
+
anchor: .topLeading,
|
|
108
|
+
proposal: ProposedViewSize(item.size)
|
|
109
|
+
)
|
|
110
|
+
x += item.size.width + gutter
|
|
111
|
+
}
|
|
112
|
+
y += row.height + gutter
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
|
|
117
|
+
var rows: [Row] = []
|
|
118
|
+
var current = Row()
|
|
119
|
+
for index in subviews.indices {
|
|
120
|
+
let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
|
|
121
|
+
let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
122
|
+
// Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
|
|
123
|
+
if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
|
|
124
|
+
rows.append(current)
|
|
125
|
+
current = Row()
|
|
126
|
+
}
|
|
127
|
+
current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
128
|
+
current.height = max(current.height, size.height)
|
|
129
|
+
current.items.append((index, size))
|
|
130
|
+
}
|
|
131
|
+
if !current.items.isEmpty { rows.append(current) }
|
|
132
|
+
return rows
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -39,6 +39,7 @@ public struct PopupNotifyProps {
|
|
|
39
39
|
|
|
40
40
|
public struct PopupNotify: View {
|
|
41
41
|
@Environment(\.localize) private var localize
|
|
42
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
42
43
|
|
|
43
44
|
public let props: PopupNotifyProps
|
|
44
45
|
|
|
@@ -49,7 +50,7 @@ public struct PopupNotify: View {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
private var maxDescriptionHeight: CGFloat {
|
|
52
|
-
|
|
53
|
+
fontScale(TypographyStyle.bodyDefaultRegular.lineHeight) * 8.5
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
private var shouldUseColumnButtons: Bool {
|
|
@@ -254,15 +254,16 @@ private struct ProgressInfoVertical: View {
|
|
|
254
254
|
// MARK: - Step icon
|
|
255
255
|
|
|
256
256
|
private struct ProgressInfoStepIcon: View {
|
|
257
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
257
258
|
let index: Int
|
|
258
259
|
let size: ProgressInfoSize
|
|
259
260
|
let useNumber: Bool
|
|
260
261
|
let customIcon: String?
|
|
261
262
|
|
|
262
263
|
var body: some View {
|
|
263
|
-
let containerSize =
|
|
264
|
-
let dotSize =
|
|
265
|
-
let iconSize =
|
|
264
|
+
let containerSize = fontScale(size == .small ? 16 : 24)
|
|
265
|
+
let dotSize = fontScale(size == .small ? 6 : 8)
|
|
266
|
+
let iconSize = fontScale(size == .small ? 12 : 16)
|
|
266
267
|
|
|
267
268
|
ZStack {
|
|
268
269
|
Circle()
|
package/ios/Rating/Rating.swift
CHANGED
|
@@ -15,6 +15,7 @@ public enum RatingSize {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
public struct Rating: View {
|
|
18
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
18
19
|
private let numOfStars: Int
|
|
19
20
|
private let rating: Float
|
|
20
21
|
private let onRatingChange: ((Int) -> Void)?
|
|
@@ -44,7 +45,7 @@ public struct Rating: View {
|
|
|
44
45
|
private func star(at index: Int) -> some View {
|
|
45
46
|
let icon = Icon(
|
|
46
47
|
source: starIcon(at: index),
|
|
47
|
-
size:
|
|
48
|
+
size: fontScale(size.iconSize),
|
|
48
49
|
color: starColor(at: index)
|
|
49
50
|
)
|
|
50
51
|
|
|
@@ -84,6 +84,7 @@ private struct StepperButton: View {
|
|
|
84
84
|
|
|
85
85
|
private struct StepperNumberView: View {
|
|
86
86
|
@Environment(\.appTheme) private var theme
|
|
87
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
87
88
|
|
|
88
89
|
@Binding var text: String
|
|
89
90
|
var size: StepperSize
|
|
@@ -105,7 +106,7 @@ private struct StepperNumberView: View {
|
|
|
105
106
|
MomoText(text, typography: .descriptionDefaultRegular, color: textColor)
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
.font(.system(size:
|
|
109
|
+
.font(.system(size: fontScale(12)))
|
|
109
110
|
.ignoreBoldTextSetting()
|
|
110
111
|
.foregroundColor(textColor)
|
|
111
112
|
.frame(minWidth: specs.numberMinWidth)
|
|
@@ -3,6 +3,7 @@ import Foundation
|
|
|
3
3
|
|
|
4
4
|
public struct SuggestAction: View {
|
|
5
5
|
@Environment(\.appTheme) private var theme
|
|
6
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
6
7
|
|
|
7
8
|
private let message: String
|
|
8
9
|
private let buttonTitle: String
|
|
@@ -32,7 +33,7 @@ public struct SuggestAction: View {
|
|
|
32
33
|
|
|
33
34
|
// Builds an attributed string with `boldMessage` (case-insensitive) emphasized.
|
|
34
35
|
private var annotatedMessage: AttributedString {
|
|
35
|
-
let baseFont = Font.system(size:
|
|
36
|
+
let baseFont = Font.system(size: fontScale(TypographyStyle.bodyDefaultRegular.fontSize), weight: .regular)
|
|
36
37
|
var attributed = AttributedString(message)
|
|
37
38
|
attributed.font = baseFont
|
|
38
39
|
|
|
@@ -44,7 +45,7 @@ public struct SuggestAction: View {
|
|
|
44
45
|
|
|
45
46
|
if let boldRange = Range(range, in: attributed) {
|
|
46
47
|
attributed[boldRange].font = Font.system(
|
|
47
|
-
size:
|
|
48
|
+
size: fontScale(TypographyStyle.bodyDefaultRegular.fontSize),
|
|
48
49
|
weight: .bold
|
|
49
50
|
)
|
|
50
51
|
}
|
package/ios/Tag/Tag.swift
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
3
|
public struct Tag: View {
|
|
4
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
4
5
|
public enum TagSize {
|
|
5
6
|
case large
|
|
6
7
|
case medium
|
|
@@ -71,7 +72,7 @@ public struct Tag: View {
|
|
|
71
72
|
}
|
|
72
73
|
MomoText(label, typography: .labelSMedium, color: labelColor)
|
|
73
74
|
}
|
|
74
|
-
.frame(height:
|
|
75
|
+
.frame(height: fontScale(size.height))
|
|
75
76
|
.padding(.horizontal, Spacing.S)
|
|
76
77
|
.background(
|
|
77
78
|
RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
|
|
@@ -74,7 +74,8 @@ public struct TrustBanner: View {
|
|
|
74
74
|
.scaledToFit()
|
|
75
75
|
.frame(width: 64, height: 64)
|
|
76
76
|
VStack(alignment: .leading){
|
|
77
|
-
Text(applicationEnvironment.config?.trustBanner?.content[localize.currentLanguage]
|
|
77
|
+
Text(applicationEnvironment.config?.trustBanner?.content[localize.currentLanguage]
|
|
78
|
+
?? localize.translateData(defaultBanner.content))
|
|
78
79
|
.foregroundColor(Color(hex: "484848"))
|
|
79
80
|
.lineLimit(2)
|
|
80
81
|
.font(.system(size: 13, weight: Font.Weight.regular))
|
|
@@ -83,7 +84,8 @@ public struct TrustBanner: View {
|
|
|
83
84
|
.padding(.bottom, 8)
|
|
84
85
|
HStack {
|
|
85
86
|
HStack(spacing: 0) {
|
|
86
|
-
Text(applicationEnvironment.config?.trustBanner?.subContent[localize.currentLanguage]
|
|
87
|
+
Text(applicationEnvironment.config?.trustBanner?.subContent[localize.currentLanguage]
|
|
88
|
+
?? localize.translateData(defaultBanner.subContent))
|
|
87
89
|
.foregroundColor(Colors.pink03)
|
|
88
90
|
.font(.system(size: 13, weight: .bold))
|
|
89
91
|
.ignoreBoldTextSetting()
|
package/ios/Title/Title.swift
CHANGED
|
@@ -28,6 +28,7 @@ public enum TitleButtonSize {
|
|
|
28
28
|
|
|
29
29
|
public struct Title: View {
|
|
30
30
|
@Environment(\.appTheme) private var theme
|
|
31
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
31
32
|
|
|
32
33
|
private let title: String
|
|
33
34
|
private let type: TitleType
|
|
@@ -152,10 +153,10 @@ public struct Title: View {
|
|
|
152
153
|
|
|
153
154
|
private var titleText: some View {
|
|
154
155
|
SwiftUI.Text(title)
|
|
155
|
-
.font(.system(size:
|
|
156
|
+
.font(.system(size: fontScale(titleFontSize), weight: titleFontWeight))
|
|
156
157
|
.ignoreBoldTextSetting()
|
|
157
158
|
.foregroundColor(theme.colors.text.default)
|
|
158
|
-
.lineSpacing(
|
|
159
|
+
.lineSpacing(fontScale(titleLineHeight) - fontScale(titleFontSize))
|
|
159
160
|
.lineLimit(titleMaxLines)
|
|
160
161
|
.truncationMode(.tail)
|
|
161
162
|
}
|
|
@@ -469,11 +469,12 @@ private struct TooltipSecondaryButton: View {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
private struct TooltipIconButton: View {
|
|
472
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
472
473
|
let icon: String
|
|
473
474
|
let onPress: () -> Void
|
|
474
475
|
|
|
475
476
|
var body: some View {
|
|
476
|
-
let iconButtonSize =
|
|
477
|
+
let iconButtonSize = fontScale(36)
|
|
477
478
|
SwiftUI.Button(action: onPress) {
|
|
478
479
|
Icon(source: icon, size: 24)
|
|
479
480
|
.frame(width: iconButtonSize, height: iconButtonSize)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// File: ios/Typography/FontScale.swift
|
|
2
|
+
// Created At: 2026-08-06 10:00:00 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: reactive font-scale snapshot injected through the environment
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
/// Immutable snapshot of the kit's font-scale inputs. The formula is a 1:1 port of Compose's
|
|
11
|
+
/// `scaleSize()` (`const/Typography.kt`): the device-width scale and the OS Dynamic Type scale are
|
|
12
|
+
/// computed independently and the larger one wins.
|
|
13
|
+
public struct FontScale: Equatable {
|
|
14
|
+
private static let defaultScreenSize: CGFloat = 375
|
|
15
|
+
private static let maxFontScale: CGFloat = 1.5
|
|
16
|
+
private static let maxDeviceScale: CGFloat = 5
|
|
17
|
+
|
|
18
|
+
public let userScaleRate: CGFloat
|
|
19
|
+
public let useOSScaleRate: Bool
|
|
20
|
+
/// OS Dynamic Type factor at the time the snapshot was taken.
|
|
21
|
+
public let osFontScale: CGFloat
|
|
22
|
+
|
|
23
|
+
public init(userScaleRate: CGFloat = 1, useOSScaleRate: Bool = true, osFontScale: CGFloat = 1) {
|
|
24
|
+
self.userScaleRate = userScaleRate
|
|
25
|
+
self.useOSScaleRate = useOSScaleRate
|
|
26
|
+
self.osFontScale = osFontScale
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// `callAsFunction` so call sites keep reading like Compose's `scaleSize(16)`.
|
|
30
|
+
public func callAsFunction(_ size: CGFloat) -> CGFloat {
|
|
31
|
+
guard useOSScaleRate else { return size * userScaleRate }
|
|
32
|
+
|
|
33
|
+
// Device width is read live rather than snapshotted, matching Compose's
|
|
34
|
+
// getScreenDimensions() read inside scaleSize(), so a rotation or split-view resize counts.
|
|
35
|
+
let deviceScale = UIScreen.main.bounds.width / Self.defaultScreenSize
|
|
36
|
+
|
|
37
|
+
var deviceScaled = size
|
|
38
|
+
var osScaled = size
|
|
39
|
+
if deviceScale > 1 {
|
|
40
|
+
deviceScaled = min(size * deviceScale, size + Self.maxDeviceScale)
|
|
41
|
+
}
|
|
42
|
+
if osFontScale > 1 {
|
|
43
|
+
osScaled = min(size * osFontScale, size * Self.maxFontScale)
|
|
44
|
+
}
|
|
45
|
+
return max(deviceScaled, osScaled)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// `.system(size:)` is a fixed-size font — SwiftUI does not Dynamic-Type it — so the scale has to
|
|
49
|
+
/// be baked into the point size here, and there is no double-scaling to undo.
|
|
50
|
+
public func font(size: CGFloat, weight: Font.Weight = .regular) -> Font {
|
|
51
|
+
.system(size: self(size), weight: weight)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A host-pushed mini-app context wins per field, matching Compose's
|
|
55
|
+
/// `context?.useOSScaleRate ?: KitFontScale.useOSScaleRate`.
|
|
56
|
+
public func overriding(userScaleRate: CGFloat?, useOSScaleRate: Bool?) -> FontScale {
|
|
57
|
+
guard userScaleRate != nil || useOSScaleRate != nil else { return self }
|
|
58
|
+
return FontScale(
|
|
59
|
+
userScaleRate: userScaleRate ?? self.userScaleRate,
|
|
60
|
+
useOSScaleRate: useOSScaleRate ?? self.useOSScaleRate,
|
|
61
|
+
osFontScale: osFontScale
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private struct FontScaleKey: EnvironmentKey {
|
|
67
|
+
/// Computed, never stored: a bare `UIHostingController` root that never got `.momoFontScale()`
|
|
68
|
+
/// still reads the live store instead of a value frozen at first access.
|
|
69
|
+
static var defaultValue: FontScale { FontScaleStore.shared.snapshot }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public extension EnvironmentValues {
|
|
73
|
+
var momoFontScale: FontScale {
|
|
74
|
+
get { self[FontScaleKey.self] }
|
|
75
|
+
set { self[FontScaleKey.self] = newValue }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public extension View {
|
|
80
|
+
/// Injects a Dynamic-Type-observing font scale and re-renders this subtree on change.
|
|
81
|
+
/// Use at every `UIHostingController` root that is not inside a `NavigationContainer`.
|
|
82
|
+
func momoFontScale(_ store: FontScaleStore = .shared, context: MiniAppContext? = nil) -> some View {
|
|
83
|
+
modifier(MomoFontScaleModifier(store: store, context: context))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// `@ObservedObject` is load-bearing, same as `MomoLocalizeModifier`: `@Environment(\.momoFontScale)`
|
|
88
|
+
/// only invalidates when the boxed snapshot changes, which requires *this* body to re-run on publish.
|
|
89
|
+
private struct MomoFontScaleModifier: ViewModifier {
|
|
90
|
+
@ObservedObject var store: FontScaleStore
|
|
91
|
+
let context: MiniAppContext?
|
|
92
|
+
|
|
93
|
+
func body(content: Content) -> some View {
|
|
94
|
+
content.environment(
|
|
95
|
+
\.momoFontScale,
|
|
96
|
+
store.snapshot.overriding(
|
|
97
|
+
userScaleRate: context?.userScaleRate,
|
|
98
|
+
useOSScaleRate: context?.useOSScaleRate
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// AI-GENERATED END: reactive font-scale snapshot injected through the environment
|
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
/// App-wide font-scale
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
///
|
|
7
|
-
///
|
|
8
|
-
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// App-wide font-scale inputs for the SwiftUI kit, and the single place they publish from.
|
|
4
|
+
/// The host writes `update(...)` at startup (from the cache-backed `FontScaleProvider`) and again
|
|
5
|
+
/// whenever the in-app font-size preference changes; the OS Dynamic Type factor is tracked here.
|
|
6
|
+
/// Mirrors Compose's `KitFontScale` + `ProvideOsFontScale` pair, including its live behaviour —
|
|
7
|
+
/// views re-render on change as long as the subtree got `.momoFontScale()` (`NavigationContainer`
|
|
8
|
+
/// applies it). When `useOSScaleRate` is false the kit applies `userScaleRate` instead of the OS factor.
|
|
9
|
+
public final class FontScaleStore: ObservableObject {
|
|
9
10
|
public static let shared = FontScaleStore()
|
|
10
|
-
private init() {}
|
|
11
11
|
|
|
12
|
-
public private(set) var userScaleRate: CGFloat = 1
|
|
13
|
-
public private(set) var useOSScaleRate: Bool = true
|
|
12
|
+
@Published public private(set) var userScaleRate: CGFloat = 1
|
|
13
|
+
@Published public private(set) var useOSScaleRate: Bool = true
|
|
14
|
+
@Published public private(set) var osFontScale: CGFloat = FontScaleStore.currentOsFontScale()
|
|
15
|
+
|
|
16
|
+
private init() {
|
|
17
|
+
NotificationCenter.default.addObserver(
|
|
18
|
+
self,
|
|
19
|
+
selector: #selector(contentSizeCategoryDidChange),
|
|
20
|
+
name: UIContentSizeCategory.didChangeNotification,
|
|
21
|
+
object: nil
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public var snapshot: FontScale {
|
|
26
|
+
FontScale(
|
|
27
|
+
userScaleRate: userScaleRate,
|
|
28
|
+
useOSScaleRate: useOSScaleRate,
|
|
29
|
+
osFontScale: osFontScale
|
|
30
|
+
)
|
|
31
|
+
}
|
|
14
32
|
|
|
15
33
|
public func update(userScaleRate: CGFloat, useOSScaleRate: Bool) {
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
onMain {
|
|
35
|
+
// Re-assigning an unchanged value would still publish and invalidate every kit subtree.
|
|
36
|
+
if self.userScaleRate != userScaleRate { self.userScaleRate = userScaleRate }
|
|
37
|
+
if self.useOSScaleRate != useOSScaleRate { self.useOSScaleRate = useOSScaleRate }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@objc private func contentSizeCategoryDidChange() {
|
|
42
|
+
let next = Self.currentOsFontScale()
|
|
43
|
+
guard next != osFontScale else { return }
|
|
44
|
+
osFontScale = next
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Same factor Compose's iOS `ProvideOsFontScale` derives from
|
|
48
|
+
/// `UIFont.preferredFont(forTextStyle: .body).pointSize / 17`, so both iOS surfaces scale alike.
|
|
49
|
+
static func currentOsFontScale() -> CGFloat {
|
|
50
|
+
let base = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
|
51
|
+
return UIFontMetrics.default.scaledFont(for: base).pointSize / base.pointSize
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Synchronous when already on main — an async hop would defer the change by a frame.
|
|
55
|
+
private func onMain(_ work: @escaping () -> Void) {
|
|
56
|
+
if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) }
|
|
18
57
|
}
|
|
19
58
|
}
|