craft-native 0.0.278 → 0.0.290

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.
@@ -5101,6 +5101,17 @@ private final class CraftNativeTextView: UITextView {
5101
5101
 
5102
5102
  override func layoutSubviews() {
5103
5103
  super.layoutSubviews()
5104
+ // A multiline input grows with its text up to its `maxHeight`, then
5105
+ // scrolls inside itself. Never scrolling, the text past that height
5106
+ // was cut off and could not be reached (a long workout comment).
5107
+ // With scrolling off, contentSize is the view's own height: measure
5108
+ // the text itself.
5109
+ let needed = isEditable ? sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude)).height : 0
5110
+ let overflows = isEditable && bounds.height > 0 && needed > bounds.height + 1
5111
+ if isScrollEnabled != overflows {
5112
+ isScrollEnabled = overflows
5113
+ if overflows, isFirstResponder { scrollRangeToVisible(selectedRange) }
5114
+ }
5104
5115
  let inset = textContainerInset
5105
5116
  let padding = textContainer.lineFragmentPadding
5106
5117
  placeholderLabel.frame = CGRect(
@@ -5117,6 +5128,7 @@ private final class CraftNativeTextView: UITextView {
5117
5128
  }
5118
5129
 
5119
5130
  private func configurePlaceholder() {
5131
+ isScrollEnabled = false
5120
5132
  placeholderLabel.numberOfLines = 0
5121
5133
  placeholderLabel.font = font
5122
5134
  placeholderLabel.textColor = .placeholderText
@@ -5623,6 +5635,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5623
5635
  // A tab prepared in a hidden stack "appears" to UIKit as well; the
5624
5636
  // shell says when it really shows (screenVisibilityChanged).
5625
5637
  if isShownOnScreen {
5638
+ hasAppeared = true
5626
5639
  screenVisibilityChanged(true)
5627
5640
  // A navigation breadcrumb for the crash reporter: the trail a
5628
5641
  // crash report carries is the screens that led to it.
@@ -5694,6 +5707,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5694
5707
  // preview also restores a screen that never lost focus, so restore its
5695
5708
  // chrome even when no new SCREEN_FOCUS event is needed.
5696
5709
  if visible { restoreShellChrome() }
5710
+ // A prepared tab the shell now shows has appeared once its frame is
5711
+ // drawn; what its script asks for until then is there from the start.
5712
+ if visible, !hasAppeared {
5713
+ DispatchQueue.main.async { [weak self] in
5714
+ guard let self, self.isViewLoaded, self.isShownOnScreen else { return }
5715
+ self.hasAppeared = true
5716
+ }
5717
+ }
5697
5718
  if visible, frameWanted { CraftNativeFrameClock.shared.request(self) { [weak self] in self?.fireFrame() } }
5698
5719
  if !visible { CraftNativeFrameClock.shared.cancel(self) }
5699
5720
  guard visible != reportedVisible else { return }
@@ -5780,6 +5801,27 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5780
5801
  super.viewDidLayoutSubviews()
5781
5802
  positionModals()
5782
5803
  emitLayoutEvents()
5804
+ sendViewportIfChanged()
5805
+ }
5806
+
5807
+ /// The width the bundle last heard, so a layout pass at the same size says nothing.
5808
+ private var reportedViewportWidth: CGFloat = -1
5809
+
5810
+ /// The screen's own width once laid out, else its window's: what a web
5811
+ /// page's media queries would measure.
5812
+ private func currentViewportWidth() -> CGFloat {
5813
+ let width = view.bounds.width > 0 ? view.bounds.width : (view.window ?? CraftPresenter.keyWindow())?.bounds.width ?? 0
5814
+ reportedViewportWidth = width
5815
+ return width
5816
+ }
5817
+
5818
+ /// A rotation, iPad split view or a resized Stage Manager window: the
5819
+ /// bundle's width classes (`md:`, `lg:`) follow the screen's new width.
5820
+ private func sendViewportIfChanged() {
5821
+ let width = view.bounds.width
5822
+ guard width > 0, abs(width - reportedViewportWidth) >= 0.5 else { return }
5823
+ reportedViewportWidth = width
5824
+ send(type: "VIEWPORT", payload: ["width": Double(width), "height": Double(view.bounds.height)])
5783
5825
  }
5784
5826
 
5785
5827
  /// Modals cover the screen; their content starts below its bars (the
@@ -5864,6 +5906,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5864
5906
  let appearanceJSON: String
5865
5907
  let sessionTokenKey: String
5866
5908
  let focused: Bool?
5909
+ /// The screen's width in points, for the bundle's `sm:`/`md:`/`lg:` classes.
5910
+ let viewportWidth: Double
5867
5911
  }
5868
5912
 
5869
5913
  private func scriptInputs() -> ScriptInputs {
@@ -5875,7 +5919,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5875
5919
  colorScheme: colorScheme,
5876
5920
  appearanceJSON: appearanceJSON(),
5877
5921
  sessionTokenKey: Self.jsonString(CraftSession.tokenKey(config)),
5878
- focused: reportedVisible
5922
+ focused: reportedVisible,
5923
+ viewportWidth: Double(currentViewportWidth())
5879
5924
  )
5880
5925
  }
5881
5926
 
@@ -5977,6 +6022,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5977
6022
  capabilities: \(capabilityJSON),
5978
6023
  initialAppState: "\(inputs.appState)",
5979
6024
  colorScheme: "\(inputs.colorScheme)",
6025
+ viewportWidth: \(inputs.viewportWidth),
5980
6026
  appearance: \(inputs.appearanceJSON),
5981
6027
  appURL: \(appURL),
5982
6028
  appVersion: \(appVersion),
@@ -6546,6 +6592,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6546
6592
  /// own horizontal pager or chart gesture.
6547
6593
  private(set) var tabSwipeEnabled = true
6548
6594
  private var prefersLargeTitle: Bool?
6595
+ /// Whether the screen has been on screen. Until then, a bar its script
6596
+ /// asks for is simply there in its first frame: a tab opened for the
6597
+ /// first time (HQ's Me) mounts in the window, and its bar and large title
6598
+ /// slid in from the side as the screen appeared.
6599
+ private var hasAppeared = false
6600
+ /// A bar shown or hidden after the screen has appeared animates; before,
6601
+ /// it is in place from the start.
6602
+ var animatesNavigationBarChanges: Bool { hasAppeared && view.window != nil }
6549
6603
  /// Whether this screen opts into an opaque navigation bar filled with its
6550
6604
  /// own background colour. The default keeps UIKit's material appearance.
6551
6605
  private var navigationBarSolid = false
@@ -6624,7 +6678,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6624
6678
  if let hidden = options["hidden"] as? Bool {
6625
6679
  navigationBarHidden = hidden
6626
6680
  if navigationController?.topViewController === self {
6627
- navigationController?.setNavigationBarHidden(hidden, animated: view.window != nil)
6681
+ navigationController?.setNavigationBarHidden(hidden, animated: animatesNavigationBarChanges)
6628
6682
  }
6629
6683
  }
6630
6684
  if options.keys.contains("leftButtons") {
@@ -6642,7 +6696,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6642
6696
  // Asked for after the bar was hidden for a bare root screen.
6643
6697
  if navigationBarHidden == nil, wantsNavigationBar, navigationController?.topViewController === self,
6644
6698
  navigationController?.isNavigationBarHidden == true {
6645
- navigationController?.setNavigationBarHidden(false, animated: view.window != nil)
6699
+ navigationController?.setNavigationBarHidden(false, animated: animatesNavigationBarChanges)
6646
6700
  }
6647
6701
  }
6648
6702
 
@@ -7104,13 +7158,39 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7104
7158
  /// `input`, scrolled into the visible part of the scroll view it is in.
7105
7159
  /// Internal for XCTest.
7106
7160
  func scrollIntoView(_ input: UIView, animated: Bool) {
7107
- var ancestor = input.superview
7108
- while let current = ancestor, !(current is CraftNativeScrollView) { ancestor = current.superview }
7109
- guard let scroll = ancestor as? CraftNativeScrollView, scroll.isVertical else { return }
7110
- let rect = input.convert(input.bounds, to: scroll).insetBy(dx: 0, dy: -16)
7161
+ guard let scroll = enclosingVerticalScroll(of: input) else { return }
7162
+ var rect = input.convert(input.bounds, to: scroll).insetBy(dx: 0, dy: -16)
7163
+ // A field taller than the part the keyboard leaves (a long comment
7164
+ // with no max height) shows its caret rather than its top: scrolled
7165
+ // to its top, the caret at the end of the text stayed under the
7166
+ // keyboard with no way to scroll it out.
7167
+ let visible = scroll.bounds.inset(by: scroll.adjustedContentInset)
7168
+ if rect.height > visible.height, let caret = caretRect(of: input, in: scroll) { rect = caret }
7111
7169
  scroll.scrollRectToVisible(rect, animated: animated)
7112
7170
  }
7113
7171
 
7172
+ /// The vertical scroll view `view` sits in, if any.
7173
+ private func enclosingVerticalScroll(of view: UIView) -> CraftNativeScrollView? {
7174
+ var ancestor = view.superview
7175
+ while let current = ancestor, !(current is CraftNativeScrollView) { ancestor = current.superview }
7176
+ guard let scroll = ancestor as? CraftNativeScrollView, scroll.isVertical else { return nil }
7177
+ return scroll
7178
+ }
7179
+
7180
+ /// A focused input's caret in `scroll`'s coordinates, with a little room
7181
+ /// around it, kept within the input (one scrolling inside itself has its
7182
+ /// caret beyond its own frame until it scrolls to it).
7183
+ private func caretRect(of input: UIView, in scroll: UIScrollView) -> CGRect? {
7184
+ guard input.isFirstResponder, let text = input as? UITextInput,
7185
+ let end = text.selectedTextRange?.end else { return nil }
7186
+ let caret = text.caretRect(for: end)
7187
+ guard caret.minY.isFinite, caret.height > 0 else { return nil }
7188
+ let frame = input.convert(input.bounds, to: scroll)
7189
+ var rect = input.convert(caret, to: scroll)
7190
+ rect.origin.y = min(max(rect.minY, frame.minY), max(frame.minY, frame.maxY - rect.height))
7191
+ return rect.insetBy(dx: 0, dy: -16)
7192
+ }
7193
+
7114
7194
  private func sendAppState(_ state: String) {
7115
7195
  // A screen hears it while it is on top of its stack; the app script,
7116
7196
  // which is on no stack, always.
@@ -8977,7 +9057,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
8977
9057
  if textView.textContainerInset != padding { textView.textContainerInset = padding }
8978
9058
  textView.textContainer.lineFragmentPadding = 0
8979
9059
  }
8980
- textView.isScrollEnabled = false
9060
+ // An input's own scrolling follows its height (CraftNativeTextView).
9061
+ if !(textView is CraftNativeTextView) { textView.isScrollEnabled = false }
8981
9062
  let numberOfLines = Int(number(props["numberOfLines"]) ?? 0)
8982
9063
  if numberOfLines > 0 {
8983
9064
  textView.textContainer.maximumNumberOfLines = numberOfLines
@@ -9846,11 +9927,38 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9846
9927
  let id = ObjectIdentifier(textView)
9847
9928
  inputIdentities[id].map { inputDrafts[$0] = textView.text ?? "" }
9848
9929
  (textView as? CraftNativeTextView)?.refreshPlaceholder()
9930
+ keepCaretVisible(in: textView)
9849
9931
  guard let handler = handlers[id] else { return }
9850
9932
  noteSentText(textView.text ?? "", for: textView)
9851
9933
  send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": textView.text ?? ""]])
9852
9934
  }
9853
9935
 
9936
+ func textViewDidChangeSelection(_ textView: UITextView) {
9937
+ guard textView.isFirstResponder else { return }
9938
+ keepCaretVisible(in: textView)
9939
+ }
9940
+
9941
+ /// The caret, kept where it can be seen as a multiline input grows or the
9942
+ /// caret moves: in the part of the screen's scroll view the keyboard
9943
+ /// leaves. Only the field was scrolled into view, when editing began, so
9944
+ /// a long comment typed at the bottom of a screen went under the keyboard.
9945
+ private func keepCaretVisible(in textView: UITextView) {
9946
+ DispatchQueue.main.async { [weak self, weak textView] in
9947
+ guard let self, let textView, textView.isFirstResponder else { return }
9948
+ // The input grows with its text first: a text view's new lines
9949
+ // are news to the flex layout around it, which sized the scroll
9950
+ // view's content to the old height.
9951
+ textView.invalidateIntrinsicContentSize()
9952
+ CraftNativeFlexLayout.invalidateAncestors(of: textView)
9953
+ self.view.layoutIfNeeded()
9954
+ guard let scroll = self.enclosingVerticalScroll(of: textView),
9955
+ let rect = self.caretRect(of: textView, in: scroll) else { return }
9956
+ let visible = scroll.bounds.inset(by: scroll.adjustedContentInset)
9957
+ guard !visible.contains(rect) else { return }
9958
+ scroll.scrollRectToVisible(rect, animated: false)
9959
+ }
9960
+ }
9961
+
9854
9962
  func textViewDidBeginEditing(_ textView: UITextView) {
9855
9963
  lastFocusedInput = textView
9856
9964
  if keyboardScreenFrame != nil { DispatchQueue.main.async { self.scrollFocusedInputIntoView(animated: true) } }
package/dist/package.d.ts CHANGED
@@ -56,6 +56,12 @@ export interface PackageConfig {
56
56
  installerIdentity?: string;
57
57
  /** Path to the entitlements plist applied when signing the app */
58
58
  entitlements?: string;
59
+ /** Entitlements for bundled helper executables, separate from the main app. */
60
+ helperEntitlements?: string;
61
+ /** Keychain containing the application and installer signing identities. */
62
+ signingKeychain?: string;
63
+ /** Privacy manifest copied to `Contents/Resources/PrivacyInfo.xcprivacy`. */
64
+ privacyManifest?: string;
59
65
  /** Path to a `.provisionprofile` embedded as `Contents/embedded.provisionprofile` */
60
66
  provisioningProfile?: string;
61
67
  /** Build number (`CFBundleVersion`). Defaults to `version`. */
@@ -208,12 +214,16 @@ export declare function codesignArguments(opts: {
208
214
  identity: string;
209
215
  entitlements?: string;
210
216
  hardenedRuntime?: boolean;
217
+ deep?: boolean;
218
+ identifier?: string;
219
+ keychain?: string;
211
220
  }): string[];
212
221
  /** Build the `productbuild` argument list for a Mac App Store submission package. */
213
222
  export declare function productbuildArguments(opts: {
214
223
  appBundlePath: string;
215
224
  outputPath: string;
216
225
  identity: string;
226
+ keychain?: string;
217
227
  }): string[];
218
228
  /** Build the `xcrun notarytool submit` argument list for a DMG or PKG. */
219
229
  export declare function notarytoolArguments(opts: {
package/dist/types.d.ts CHANGED
@@ -725,6 +725,11 @@ export interface AppInfo {
725
725
  * Platform (macos, linux, windows)
726
726
  */
727
727
  platform: string;
728
+ /** Whether platform file access is restricted to explicit user grants. */
729
+ sandboxed?: boolean;
730
+ bundleId?: string;
731
+ bundlePath?: string;
732
+ executablePath?: string;
728
733
  }
729
734
  /**
730
735
  * Notification options
@@ -1226,6 +1231,7 @@ export interface CraftBridgeAPI {
1226
1231
  /**
1227
1232
  * Screen-sharing and screen-recording detection (macOS)
1228
1233
  */
1234
+ timers?: CraftTimersAPI;
1229
1235
  screenSharing?: CraftScreenSharingAPI;
1230
1236
  /**
1231
1237
  * Trigger one haptic tap on a Force Touch trackpad (macOS), as
@@ -2345,3 +2351,10 @@ declare global {
2345
2351
  craft?: CraftBridgeAPI & CraftEventEmitter;
2346
2352
  }
2347
2353
  }
2354
+ /** Native background deadlines, independent of hidden WebKit DOM timers. */
2355
+ export interface CraftTimersAPI {
2356
+ /** Wait up to one day. Aborting rejects with AbortError and releases native resources. */
2357
+ sleep(delayMs: number, options?: {
2358
+ signal?: AbortSignal;
2359
+ }): Promise<void>;
2360
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.278",
3
+ "version": "0.0.290",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",