craft-native 0.0.77 → 0.0.79

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.
@@ -62,9 +62,15 @@ export interface SpawnOptions {
62
62
  cwd?: string;
63
63
  env?: Record<string, string>;
64
64
  /**
65
- * Run the command through a shell. The Craft bridge translates this
66
- * automatically (cmd.exe on Windows, /bin/sh elsewhere). On the Node
67
- * fallback the command is rewritten to `cmd.exe /c <cmd>` on win32.
65
+ * Run `command` as a command line through a shell (cmd.exe on Windows,
66
+ * /bin/sh elsewhere) instead of executing it directly.
67
+ *
68
+ * Off by default, as in Node. Without it `command` is a program and `args`
69
+ * are its arguments, so a `;` or `|` inside an argument reaches the program
70
+ * as a plain character rather than as shell syntax.
71
+ *
72
+ * A shell takes one string, so `args` cannot be passed with it — supplying
73
+ * both is refused rather than silently honouring one of them.
68
74
  */
69
75
  shell?: boolean;
70
76
  }
@@ -101,11 +101,6 @@ export interface WindowCreateOptions {
101
101
  webSidebarMaterialOpacity?: number;
102
102
  /** Titlebar style (macOS) */
103
103
  titlebarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
104
- /** Traffic light position (macOS) */
105
- trafficLightPosition?: {
106
- x: number;
107
- y: number;
108
- };
109
104
  /** Vibrancy effect (macOS) */
110
105
  vibrancy?: 'appearance-based' | 'light' | 'dark' | 'titlebar' | 'selection' | 'menu' | 'popover' | 'sidebar' | 'header' | 'sheet' | 'window' | 'hud' | 'fullscreen-ui' | 'tooltip' | 'content' | 'under-window' | 'under-page';
111
106
  /** Background material (Windows 11) */
@@ -338,12 +333,23 @@ export declare class Window {
338
333
  */
339
334
  setVibrancy(vibrancy: WindowCreateOptions['vibrancy'] | null): Promise<void>;
340
335
  /**
341
- * Set traffic light position (macOS)
342
- */
343
- setTrafficLightPosition(position: {
344
- x: number;
345
- y: number;
346
- }): Promise<void>;
336
+ * Where the platform's window buttons are — read, not written.
337
+ *
338
+ * There was a `setTrafficLightPosition` here, and a `trafficLightPosition`
339
+ * window option beside it. Neither ever moved anything: the host has no
340
+ * handler for the call, and AppKit re-lays out the standard window buttons
341
+ * after any one-shot `setFrameOrigin:`, which is why `macos.zig` leaves them
342
+ * where the window server puts them and says so at length.
343
+ *
344
+ * What a layout actually needs is the opposite direction — where they *are*,
345
+ * which the host measures and publishes on every window:
346
+ *
347
+ * window.craft.windowControls { style, x, y, width, height, ... }
348
+ * --craft-window-controls-width the room to leave, in CSS
349
+ *
350
+ * See `CraftWindowControls`, and `docs/features/window-management.md`.
351
+ */
352
+ get windowControls(): import('../types.js').CraftWindowControls | undefined;
347
353
  /**
348
354
  * Set window level (macOS)
349
355
  */
package/dist/cli.js CHANGED
@@ -4191,7 +4191,7 @@ function craftBinaryNotFoundMessage(triedPath) {
4191
4191
  `);
4192
4192
  }
4193
4193
  // package.json
4194
- var version = "0.0.77";
4194
+ var version = "0.0.79";
4195
4195
 
4196
4196
  // bin/cli.ts
4197
4197
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -2532,8 +2532,8 @@ class Window {
2532
2532
  async setVibrancy(vibrancy) {
2533
2533
  await this._call("setVibrancy", { vibrancy });
2534
2534
  }
2535
- async setTrafficLightPosition(position) {
2536
- await this._call("setTrafficLightPosition", position);
2535
+ get windowControls() {
2536
+ return globalThis.craft?.windowControls;
2537
2537
  }
2538
2538
  async setWindowLevel(level) {
2539
2539
  await this._call("setWindowLevel", { level });
@@ -10745,6 +10745,10 @@ class CraftApp {
10745
10745
  args.push("--titlebar-hidden");
10746
10746
  if (window3?.headless)
10747
10747
  args.push("--headless");
10748
+ if (window3?.keepRunning === true)
10749
+ args.push("--keep-running");
10750
+ else if (window3?.keepRunning === false)
10751
+ args.push("--quit-on-close");
10748
10752
  if (window3?.webSidebarMaterial) {
10749
10753
  args.push("--web-sidebar-material");
10750
10754
  if (window3?.webSidebarWidth)
package/dist/index.js CHANGED
@@ -2302,8 +2302,8 @@ class Window {
2302
2302
  async setVibrancy(vibrancy) {
2303
2303
  await this._call("setVibrancy", { vibrancy });
2304
2304
  }
2305
- async setTrafficLightPosition(position) {
2306
- await this._call("setTrafficLightPosition", position);
2305
+ get windowControls() {
2306
+ return globalThis.craft?.windowControls;
2307
2307
  }
2308
2308
  async setWindowLevel(level) {
2309
2309
  await this._call("setWindowLevel", { level });
@@ -10515,6 +10515,10 @@ class CraftApp {
10515
10515
  args.push("--titlebar-hidden");
10516
10516
  if (window3?.headless)
10517
10517
  args.push("--headless");
10518
+ if (window3?.keepRunning === true)
10519
+ args.push("--keep-running");
10520
+ else if (window3?.keepRunning === false)
10521
+ args.push("--quit-on-close");
10518
10522
  if (window3?.webSidebarMaterial) {
10519
10523
  args.push("--web-sidebar-material");
10520
10524
  if (window3?.webSidebarWidth)
@@ -364,7 +364,12 @@ struct CraftWebView: UIViewRepresentable {
364
364
  func updateUIView(_ webView: WKWebView, context: Context) {}
365
365
 
366
366
  func makeCoordinator() -> Coordinator {
367
- Coordinator(config: config)
367
+ let coordinator = Coordinator(config: config)
368
+ // The Zig dispatcher finds CraftSwiftShim by class name; the shim finds
369
+ // its way back to the live coordinator through this. Weak, because
370
+ // SwiftUI owns the coordinator's lifetime.
371
+ CraftSwiftShim.coordinator = coordinator
372
+ return coordinator
368
373
  }
369
374
 
370
375
  // MARK: - Coordinator (Native Bridge)
@@ -511,8 +516,19 @@ struct CraftWebView: UIViewRepresentable {
511
516
  guard let body = message.body as? [String: Any],
512
517
  let action = body["action"] as? String else { return }
513
518
 
514
- let callbackId = body["callbackId"] as? String
519
+ dispatch(action: action, body: body, callbackId: body["callbackId"] as? String)
520
+ }
515
521
 
522
+ /// The action switch, reachable from two directions: the
523
+ /// WKScriptMessageHandler above, and `CraftSwiftShim.handleAction` when
524
+ /// the Zig dispatcher hands over an action it has not migrated yet.
525
+ ///
526
+ /// A hand-off call arrives with a synthetic callbackId of the form
527
+ /// "zig:<requestId>". The reply helpers recognise that prefix and route
528
+ /// the answer back through the Zig runtime's exported
529
+ /// `craft_ios_deliver_result` instead of evaluating JavaScript here —
530
+ /// one reply path, owned by whichever side received the page's message.
531
+ func dispatch(action: String, body: [String: Any], callbackId: String?) {
516
532
  switch action {
517
533
  case "startListening":
518
534
  if config.enableSpeechRecognition { startSpeechRecognition() }
@@ -2222,41 +2238,33 @@ struct CraftWebView: UIViewRepresentable {
2222
2238
  _progressCallbacks: [],
2223
2239
  _statusCallbacks: [],
2224
2240
 
2241
+ // OTA is not implemented natively. These five used to post
2242
+ // to actions the switch below does not handle, and register
2243
+ // a resolve/reject pair that nothing would ever call — they
2244
+ // also bypassed _createCallback, which is what owns the 30s
2245
+ // timeout, so the promises hung forever rather than
2246
+ // rejecting. An immediate rejection is the honest answer:
2247
+ // an app can catch it, where it could never catch a hang.
2248
+ _unavailable: function(name) {
2249
+ return Promise.reject(new Error(
2250
+ 'craft.ota.' + name + ' is not implemented on this platform'
2251
+ ));
2252
+ },
2225
2253
  configure: function(options) {
2226
2254
  this._config = options;
2227
- window.webkit.messageHandlers.craft.postMessage({action: 'otaConfigure', config: options});
2255
+ console.warn('[craft] ota.configure stored locally; OTA is not implemented natively');
2228
2256
  },
2229
2257
  checkForUpdate: function() {
2230
- var self = window.craft;
2231
- var id = 'cb_' + (++self._callbackId);
2232
- window.webkit.messageHandlers.craft.postMessage({action: 'otaCheckForUpdate', callbackId: id});
2233
- return new Promise(function(resolve, reject) {
2234
- self._callbacks[id] = {resolve: resolve, reject: reject};
2235
- });
2258
+ return window.craft.ota._unavailable('checkForUpdate');
2236
2259
  },
2237
2260
  downloadUpdate: function(options) {
2238
- var self = window.craft;
2239
- var id = 'cb_' + (++self._callbackId);
2240
- window.webkit.messageHandlers.craft.postMessage({action: 'otaDownloadUpdate', options: options || {}, callbackId: id});
2241
- return new Promise(function(resolve, reject) {
2242
- self._callbacks[id] = {resolve: resolve, reject: reject};
2243
- });
2261
+ return window.craft.ota._unavailable('downloadUpdate');
2244
2262
  },
2245
2263
  applyUpdate: function() {
2246
- var self = window.craft;
2247
- var id = 'cb_' + (++self._callbackId);
2248
- window.webkit.messageHandlers.craft.postMessage({action: 'otaApplyUpdate', callbackId: id});
2249
- return new Promise(function(resolve, reject) {
2250
- self._callbacks[id] = {resolve: resolve, reject: reject};
2251
- });
2264
+ return window.craft.ota._unavailable('applyUpdate');
2252
2265
  },
2253
2266
  rollback: function() {
2254
- var self = window.craft;
2255
- var id = 'cb_' + (++self._callbackId);
2256
- window.webkit.messageHandlers.craft.postMessage({action: 'otaRollback', callbackId: id});
2257
- return new Promise(function(resolve, reject) {
2258
- self._callbacks[id] = {resolve: resolve, reject: reject};
2259
- });
2267
+ return window.craft.ota._unavailable('rollback');
2260
2268
  },
2261
2269
  getCurrentBundle: function() {
2262
2270
  // This returns synchronously from stored data
@@ -2434,6 +2442,10 @@ struct CraftWebView: UIViewRepresentable {
2434
2442
  rejectCallback(callbackId, error: "Native result could not be serialized", code: "SERIALIZATION_ERROR")
2435
2443
  return
2436
2444
  }
2445
+ // A hand-off from the Zig dispatcher replies through Zig, which
2446
+ // owns the wire format, the request id, and the escaping. Replying
2447
+ // by JavaScript here as well would give the page two answers.
2448
+ if CraftSwiftShim.deliverResultIfHandOff(id, json: resultStr) { return }
2437
2449
  let script = "window.craft._resolveCallback('\(id)', \(resultStr));"
2438
2450
  DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2439
2451
  }
@@ -2444,7 +2456,17 @@ struct CraftWebView: UIViewRepresentable {
2444
2456
 
2445
2457
  private func rejectCallback(_ callbackId: String?, error: String, code: String = "CRAFT_ERROR") {
2446
2458
  guard let id = callbackId else { return }
2447
- let escapedError = error.replacingOccurrences(of: "'", with: "\\'").replacingOccurrences(of: "\n", with: "\\n")
2459
+ // A hand-off rejection goes back through Zig's error route, so the
2460
+ // page's promise *rejects*. Delivering it as a result would run the
2461
+ // app's then-branch with an error-shaped object — fabricated
2462
+ // success wearing a different hat.
2463
+ if CraftSwiftShim.deliverErrorIfHandOff(id, message: error, code: code) { return }
2464
+ // Backslashes first: escaping ' and \n but not \ let an error
2465
+ // message containing a backslash break out of the string literal.
2466
+ let escapedError = error
2467
+ .replacingOccurrences(of: "\\", with: "\\\\")
2468
+ .replacingOccurrences(of: "'", with: "\\'")
2469
+ .replacingOccurrences(of: "\n", with: "\\n")
2448
2470
  let script = "window.craft._rejectCallback('\(id)', '\(escapedError)', '\(code)');"
2449
2471
  DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2450
2472
  }
@@ -5167,3 +5189,98 @@ extension CraftWebView.Coordinator: WCSessionDelegate {
5167
5189
  }
5168
5190
  }
5169
5191
  }
5192
+
5193
+
5194
+ // MARK: - Zig hand-off shim
5195
+
5196
+ /// The seam the Zig dispatcher hands unmigrated actions through.
5197
+ ///
5198
+ /// Discovery is symmetric and both directions are runtime-only. Zig finds this
5199
+ /// class with `objc_getClass("CraftSwiftShim")`; this class finds Zig's
5200
+ /// delivery exports with `dlsym`. Neither side links the other, so a
5201
+ /// pure-Swift app (no Zig runtime) and a fully-migrated Zig app (no shim work
5202
+ /// left) both build and run without dead dependencies.
5203
+ ///
5204
+ /// The shim decides *what* the answer is, never *how* it reaches the page.
5205
+ /// Replies go back through Zig's `craft_ios_deliver_result` /
5206
+ /// `craft_ios_deliver_error`, which own the wire format, the request id, and
5207
+ /// the escaping. Two components replying to one page by different routes is
5208
+ /// how this codebase accumulated five envelopes.
5209
+ @objc(CraftSwiftShim)
5210
+ final class CraftSwiftShim: NSObject {
5211
+ /// The coordinator serving hand-offs. Set by `makeCoordinator`.
5212
+ static weak var coordinator: CraftWebView.Coordinator?
5213
+
5214
+ private typealias DeliverResultFn = @convention(c) (
5215
+ UnsafePointer<CChar>, UInt, UnsafePointer<CChar>, UInt, Int64
5216
+ ) -> Void
5217
+ private typealias DeliverErrorFn = @convention(c) (
5218
+ UnsafePointer<CChar>, UInt, UnsafePointer<CChar>, UInt,
5219
+ UnsafePointer<CChar>, UInt, Int64
5220
+ ) -> Void
5221
+
5222
+ private static let deliverResult: DeliverResultFn? = {
5223
+ guard let sym = dlsym(dlopen(nil, RTLD_NOW), "craft_ios_deliver_result") else { return nil }
5224
+ return unsafeBitCast(sym, to: DeliverResultFn.self)
5225
+ }()
5226
+
5227
+ private static let deliverError: DeliverErrorFn? = {
5228
+ guard let sym = dlsym(dlopen(nil, RTLD_NOW), "craft_ios_deliver_error") else { return nil }
5229
+ return unsafeBitCast(sym, to: DeliverErrorFn.self)
5230
+ }()
5231
+
5232
+ /// Entry point for the Zig dispatcher. Selector: handleAction:payload:requestId:
5233
+ ///
5234
+ /// Returns false when this shim cannot serve the call — no live
5235
+ /// coordinator, or no Zig runtime to reply through — so Zig answers the
5236
+ /// page with UnknownAction instead of the call vanishing.
5237
+ @objc static func handleAction(_ action: String, payload: String, requestId: Int64) -> Bool {
5238
+ guard let coordinator, deliverResult != nil else { return false }
5239
+
5240
+ let body = ((try? JSONSerialization.jsonObject(with: Data(payload.utf8))) as? [String: Any]) ?? [:]
5241
+
5242
+ // The synthetic callbackId routes this call's reply back through Zig.
5243
+ // It carries the action too, because the reply helpers do not otherwise
5244
+ // know it, and Zig's reply names the action for the page's
5245
+ // action-matching fallback.
5246
+ coordinator.dispatch(action: action, body: body, callbackId: "zig:\(requestId):\(action)")
5247
+ return true
5248
+ }
5249
+
5250
+ /// Route a resolve through Zig when the callbackId marks a hand-off.
5251
+ /// Returns true when the reply has been (or could only be) handled here.
5252
+ static func deliverResultIfHandOff(_ callbackId: String, json: String) -> Bool {
5253
+ guard let (requestId, action) = parseHandOffId(callbackId) else { return false }
5254
+ guard let deliverResult else { return true } // hand-off id but no runtime: drop, never eval JS
5255
+ action.withCString { a in
5256
+ json.withCString { j in
5257
+ deliverResult(a, UInt(strlen(a)), j, UInt(strlen(j)), requestId)
5258
+ }
5259
+ }
5260
+ return true
5261
+ }
5262
+
5263
+ /// Route a rejection through Zig's error path, so the page's promise
5264
+ /// rejects rather than resolving with an error-shaped object.
5265
+ static func deliverErrorIfHandOff(_ callbackId: String, message: String, code: String) -> Bool {
5266
+ guard let (requestId, action) = parseHandOffId(callbackId) else { return false }
5267
+ guard let deliverError else { return true }
5268
+ action.withCString { a in
5269
+ message.withCString { m in
5270
+ code.withCString { c in
5271
+ deliverError(a, UInt(strlen(a)), m, UInt(strlen(m)), c, UInt(strlen(c)), requestId)
5272
+ }
5273
+ }
5274
+ }
5275
+ return true
5276
+ }
5277
+
5278
+ /// "zig:<requestId>:<action>" -> (requestId, action). Nil for ordinary
5279
+ /// page-issued callback ids, which keep their JavaScript reply path.
5280
+ private static func parseHandOffId(_ callbackId: String) -> (Int64, String)? {
5281
+ guard callbackId.hasPrefix("zig:") else { return nil }
5282
+ let rest = callbackId.dropFirst(4)
5283
+ guard let sep = rest.firstIndex(of: ":"), let id = Int64(rest[..<sep]) else { return nil }
5284
+ return (id, String(rest[rest.index(after: sep)...]))
5285
+ }
5286
+ }
package/dist/types.d.ts CHANGED
@@ -73,6 +73,22 @@ export interface WindowOptions {
73
73
  * error rather than a silent no-op.
74
74
  */
75
75
  headless?: boolean;
76
+ /**
77
+ * Keep the process running after the last window closes (macOS).
78
+ *
79
+ * Craft decides this from the shape of the app when it is not set: an app
80
+ * with a tray icon or in menubar-only mode stays running, because outliving
81
+ * its window is what those are for; an ordinary windowed app quits.
82
+ *
83
+ * Set it to `true` for a windowed app that should stay resident and be
84
+ * brought back by clicking the Dock icon, or `false` for a tray app that
85
+ * should genuinely go away when its window closes.
86
+ *
87
+ * Before this existed the answer was AppKit's default, `false`, for every
88
+ * app: closing the last window left a process with no window and no way to
89
+ * get one back.
90
+ */
91
+ keepRunning?: boolean;
76
92
  /**
77
93
  * Remember this window's size and position across launches, under this name
78
94
  * (macOS).
@@ -673,11 +689,46 @@ export interface AppInfo {
673
689
  /**
674
690
  * Notification options
675
691
  */
692
+ /**
693
+ * A button on a notification banner.
694
+ *
695
+ * macOS shows two directly on a banner and puts any others behind an
696
+ * "Options" disclosure, so craft accepts at most four — past that they are
697
+ * a menu the user has to go looking for rather than a choice they can see.
698
+ *
699
+ * Two spellings are accepted. `{ id, label }` is the one to use. `{ action,
700
+ * title }` is what this type has said since before anything implemented it,
701
+ * and it keeps working rather than being deleted out from under whoever wrote
702
+ * against it — the field was published, it just never did anything.
703
+ */
704
+ export type NotificationAction = {
705
+ /** Comes back as `actionId` in `craft.notifications.onAction`. */
706
+ id: string;
707
+ /** The text on the button. */
708
+ label: string;
709
+ } | {
710
+ /** @deprecated Use `id`. */
711
+ action: string;
712
+ /** @deprecated Use `label`. */
713
+ title: string;
714
+ };
676
715
  export interface NotificationOptions {
677
716
  /**
678
717
  * Notification title (required)
679
718
  */
680
719
  title: string;
720
+ /**
721
+ * Buttons on the banner.
722
+ *
723
+ * Pressing one brings the app forward and fires
724
+ * `craft.notifications.onAction` with `{ notificationId, actionId }` —
725
+ * which is what lets a prompt be answered without switching to the app
726
+ * first.
727
+ *
728
+ * Two buttons cannot share an `id`: the response names the button by id,
729
+ * and for an Approve/Deny prompt that name is the entire answer.
730
+ */
731
+ actions?: NotificationAction[];
681
732
  /**
682
733
  * Notification body text
683
734
  */
@@ -694,13 +745,6 @@ export interface NotificationOptions {
694
745
  * - Or any system sound name
695
746
  */
696
747
  sound?: string;
697
- /**
698
- * Action buttons (platform dependent)
699
- */
700
- actions?: Array<{
701
- action: string;
702
- title: string;
703
- }>;
704
748
  /**
705
749
  * Notification tag (for grouping/replacing)
706
750
  */
@@ -916,7 +960,83 @@ export interface CraftMobileAPI {
916
960
  * defines the CraftBridge interface with additional mobile-only features
917
961
  * such as AR, ML, deep links, OTA updates, widgets, and auth persistence.
918
962
  */
963
+ /**
964
+ * Where the platform drew this window's window buttons — the macOS traffic
965
+ * lights, and their equivalents elsewhere.
966
+ *
967
+ * The buttons are the window server's on every desktop window Craft opens that
968
+ * is not frameless: real, correctly styled, wired to the keyboard and to
969
+ * accessibility. A page must never draw replicas beside them, and this is what
970
+ * makes that unnecessary — it says where they are, so a layout can leave room
971
+ * instead of inventing its own.
972
+ *
973
+ * Every number is measured from the live window and re-sent when it changes: a
974
+ * resize, a fullscreen transition, a new document. Listen for the
975
+ * `craft:windowcontrols` event on `window` for layout that CSS cannot express;
976
+ * everything else is better served by the four CSS variables, which the host
977
+ * sets before the document is parsed.
978
+ *
979
+ * The same facts reach CSS as `--craft-window-controls-width` / `-height` /
980
+ * `-inset-x` / `-inset-y` / `-replicas`, and the document as
981
+ * `<html data-craft-window-controls="...">`.
982
+ */
983
+ export interface CraftWindowControls {
984
+ /**
985
+ * `titlebar` — real buttons, in a titlebar above the page. Nothing to do.
986
+ * `overlay` — real buttons, over the page's own top-left corner. Leave room.
987
+ * `custom` — a frameless window: no buttons, and the page's own are the only
988
+ * ones there can be.
989
+ * `none` — no window chrome in this environment at all (iOS, Android).
990
+ */
991
+ style: 'titlebar' | 'overlay' | 'custom' | 'none';
992
+ /** The platform drew real buttons for this window. */
993
+ native: boolean;
994
+ /**
995
+ * ...and they are on screen right now. False in fullscreen, where macOS
996
+ * takes them into an auto-hiding titlebar — which is why a layout should
997
+ * reserve `reserveWidth` rather than a remembered constant.
998
+ */
999
+ visible: boolean;
1000
+ /**
1001
+ * The block's true position, in CSS px from the top-left of the web
1002
+ * viewport. Negative where the buttons are not over the page at all: above
1003
+ * it in a plain titlebar window, to its left in a window whose web content
1004
+ * starts after a native sidebar.
1005
+ */
1006
+ x: number;
1007
+ y: number;
1008
+ width: number;
1009
+ height: number;
1010
+ /**
1011
+ * The room to leave inside the page — the block's far edge, or zero when it
1012
+ * does not reach into the page. `--craft-window-controls-width` / `-height`.
1013
+ */
1014
+ reserveWidth: number;
1015
+ reserveHeight: number;
1016
+ /**
1017
+ * Where the block starts inside the page, zero unless it overlaps.
1018
+ * `--craft-window-controls-inset-x` / `-inset-y`.
1019
+ */
1020
+ insetX: number;
1021
+ insetY: number;
1022
+ /**
1023
+ * The `display` a replica should take: `'none'` wherever real buttons exist
1024
+ * and wherever there is no window to control, and `null` in a frameless
1025
+ * window, where the page's own controls are the real ones. Published as
1026
+ * `--craft-window-controls-replicas`, which is *removed* rather than set when
1027
+ * this is null, so the page's own fallback applies.
1028
+ */
1029
+ replicas: 'none' | null;
1030
+ }
919
1031
  export interface CraftBridgeAPI {
1032
+ /**
1033
+ * Where the platform drew this window's close/minimise/zoom buttons.
1034
+ *
1035
+ * Present in every Craft window, so a UI that has to lay out around them can
1036
+ * ask instead of guessing — and so a UI shared with the browser can tell the
1037
+ * two apart. See `CraftWindowControls`.
1038
+ */
1039
+ windowControls?: CraftWindowControls;
920
1040
  /**
921
1041
  * Trackpad gesture phases (desktop only).
922
1042
  *
@@ -1038,13 +1158,18 @@ export interface CapabilityNamespace {
1038
1158
  export interface CraftCapabilities {
1039
1159
  namespaces: Record<string, CapabilityNamespace>;
1040
1160
  /**
1041
- * Every `craft:*` event channel, and whether anything native emits on it.
1161
+ * Every `craft:*` event channel, and what craft can say about it.
1162
+ *
1163
+ * `'live'` means something in this build took out a permit to emit on it.
1164
+ * `'unknown'` means craft cannot prove it either way — subscribe and see, and
1165
+ * do **not** disable a feature over it.
1042
1166
  *
1043
- * A `false` here means subscribing would work and never fire — which cannot
1044
- * be derived from the action tables, so it is tracked separately by the
1045
- * emitters themselves.
1167
+ * There is deliberately no `'dead'`. Craft cannot establish absence: the
1168
+ * `craft:window:*` names are composed in JavaScript from
1169
+ * `__craftDeliverWindowEvent('focus')`, so no source scan finds the literal
1170
+ * even though the emitter is right there.
1046
1171
  */
1047
- channels: Record<string, boolean>;
1172
+ channels: Record<string, 'live' | 'unknown'>;
1048
1173
  }
1049
1174
  /**
1050
1175
  * The only value types `craft.prefs` stores.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.77",
3
+ "version": "0.0.79",
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>",